first commit

This commit is contained in:
faviem
2026-04-01 17:15:17 +01:00
commit be395dce23
2474 changed files with 558561 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ path: '', loadChildren: () => import('./home/home.module').then(h => h.HomeModule) },
{ path: 'principale', loadChildren: () => import('./manage/manage.module').then(m => m.ManageModule) },
{ path: 'core', loadChildren: () => import('./office/office.module').then(o => o.OfficeModule) },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

View File

@@ -0,0 +1,11 @@
<!-- NG-ZORRO -->
<div *ngIf="isActionInProgress" class="loading-spin">
<div aria-valuemax="100" aria-valuemin="0" class="bp4-spinner bp4-intent-primary" role="progressbar">
<div class="bp4-spinner-animation">
</div>
</div>
</div>
<router-outlet></router-outlet>

35
src/app/app.component.ts Normal file
View File

@@ -0,0 +1,35 @@
import { Component, ChangeDetectorRef } from '@angular/core';
import { GlobalService } from './global.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
isActionInProgress = true;
constructor(
private globalService: GlobalService,
private cdref: ChangeDetectorRef,
private router: Router
) { }
ngOnInit(): void {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
ngAfterContentChecked() {
this.cdref.detectChanges();
}
}

78
src/app/app.module.ts Normal file
View File

@@ -0,0 +1,78 @@
import { APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NZ_I18N } from 'ng-zorro-antd/i18n';
import { fr_FR } from 'ng-zorro-antd/i18n';
import { registerLocaleData } from '@angular/common';
import fr from '@angular/common/locales/fr';
import { FormsModule } from '@angular/forms';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TokenStorage } from './utilitaire/token-storage';
import { NzConfig, provideNzConfig } from 'ng-zorro-antd/core/config';
import { JwtHelperService } from '@auth0/angular-jwt';
import { ErrorInterceptor } from './utilitaire/error-interceptor';
import { JwtInterceptor } from './utilitaire/jwt-interceptor';
import { SQLiteService } from './services/sqlite.service';
import { DetailService } from './services/detail.service';
import { DatabaseService } from './services/database.service';
import { InitializeAppService } from './services/initialize.app.service';
import { MigrationService } from './services/migrations.service';
import { NzModalService } from 'ng-zorro-antd/modal';
registerLocaleData(fr);
const ngZorroConfig: NzConfig = {
message: { nzTop: 80 },
notification: { nzTop: 240 }
};
export function initializeFactory(init: InitializeAppService) {
return () => init.initializeApp();
}
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule,
BrowserAnimationsModule,
],
providers: [
{ provide: NZ_I18N, useValue: fr_FR },
TokenStorage,
JwtHelperService,
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
provideNzConfig(ngZorroConfig),
SQLiteService,
DetailService,
DatabaseService,
InitializeAppService,
{
provide: APP_INITIALIZER,
useFactory: initializeFactory,
deps: [InitializeAppService],
multi: true
},
MigrationService,
NzModalService,
],
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule { }

102
src/app/camera.service.ts Normal file
View File

@@ -0,0 +1,102 @@
import { Injectable } from '@angular/core';
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
import { FilePicker } from '@capawesome/capacitor-file-picker';
import * as pako from 'pako';
import { FileService } from './services/file.service';
@Injectable({
providedIn: 'root'
})
export class CameraService {
constructor(
private fileService: FileService
) {
}
async takePhoto(): Promise<any> {
const photo = await Camera.getPhoto({
//resultType: CameraResultType.Uri,
resultType: CameraResultType.Base64,
source: CameraSource.Camera,
quality: 100,
//width : 800,
//height : 600,
});
const fileName = new Date().getTime() + '.'+ photo.format;
const filePath = 'InfoRFU_camera_' + fileName;
const resp = {
name: filePath,
filepath: 'image/jpeg',
webviewPath: photo.webPath,
blobData: photo.base64String ? photo.base64String.replace('data:image/jpeg;base64', ''): '',
base64Data: 'data:image/jpeg;base64,'+ (photo.base64String ? photo.base64String : '') //taille 512 Ko
};
await this.fileService.createFolder();
await this.fileService.write(resp.base64Data, resp.name);
return resp;
}
async choisirFichier(): Promise<any> {
const result = await FilePicker.pickFiles({
types: ['application/pdf', 'image/*'],
limit: 1,
readData: true,
});
const resp = {
name: 'InfoRfu_disque_' +(new Date().getTime()) + '_' + result.files[0].name.slice(-6),
filepath: result.files[0].mimeType,
base64Data: 'data:'+result.files[0].mimeType+';base64,'+result.files[0].data,
blobData: result.files[0].data
};
await this.fileService.createFolder();
await this.fileService.write(resp.base64Data, resp.name);
return resp;
}
compressData(data: string): string {
const compressed = pako.deflate(data, {
raw: true
});
return btoa(String.fromCharCode(...compressed)); // Encode en base64
}
async base64FromPath(path: string): Promise<string> {
const response = await fetch(path);
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
if (typeof reader.result === 'string') {
resolve(reader.result);
} else {
reject('method did not return a string');
}
};
reader.readAsDataURL(blob);
});
}
async getFileBse64FromPath(filePath: string): Promise<string> {
const file = await Filesystem.readFile({
path: filePath,
directory: Directory.Data
});
const imageUrl = 'data:image/jpeg;base64,' + file.data;
return imageUrl;
}
}

173
src/app/crud.service.ts Normal file
View File

@@ -0,0 +1,173 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class CrudService {
url: string = environment.backend;
constructor(
private http: HttpClient
) { }
getAll(paramURL: string): Observable<any[]> {
return this.http.get<any>(`${this.url}/${paramURL}`);
}
save(paramURL: string, data: any): Observable<any> {
return this.http.post<any>(`${this.url}/${paramURL}`, data);
}
getByPost(paramURL: string, data: any): Observable<any> {
return this.http.post<any>(`${this.url}/${paramURL}`, data);
}
update(paramURL: string, data: any): Observable<any> {
return this.http.put<any>(`${this.url}/${paramURL}/${data.id}`, data);
}
updateWithoutId(paramURL: string, data: any): Observable<any> {
return this.http.put<any>(`${this.url}/${paramURL}`, data);
}
deleteElement(paramURL: string, id: number): Observable<any> {
return this.http.delete<any>(`${this.url}/${paramURL}/${id}`);
}
doGetAction(paramURL: string, id: number): Observable<any> {
return this.http.get<any>(`${this.url}/${paramURL}/${id}`);
}
getById(paramURL: string, id: number): Observable<Object> {
return this.http.get(`${this.url}/${paramURL}/id/${id}`);
}
rapportBlocListByStructure(type: string, structureId: number): Observable<any> {
return this.http.get(`${this.url}/rapport/structure/blocs/${type}/${structureId}`, { responseType: 'blob' });
}
rapportGetEnqueteListByBloc(type: string, blocId: number): Observable<any> {
return this.http.get(`${this.url}/rapport/bloc/enquetes/${type}/${blocId}`, { responseType: 'blob' });
}
rapportPostEnqueteListByBloc(type: string, requestFiltre: any): Observable<any> {
return this.http.post(`${this.url}/rapport/filtre/enquetes/${type}`, requestFiltre, { responseType: 'blob' });
}
getPosition(): Promise<any> {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resp => {
resolve({ lng: resp.coords.longitude, lat: resp.coords.latitude });
},
err => {
reject(err);
});
});
}
saveFile(payload: any, upload: any): Observable<Object> {
const formData = new FormData();
formData.append('Content-Type', 'multipart/form-data');
formData.append('file', upload as File);
/*formData.append('idTypePiece', payload.idTypePiece as string);
formData.append('reference', payload.reference as string);
formData.append('dateEtablissement', payload.dateEtablissement as string);
formData.append('dateExpiration', payload.dateExpiration as string);*/
return this.http.post(`${this.url}/parcelle-geom/create-from-geojsonfile?reference=${payload.reference}&description=${payload.description}`, formData);
}
getDataTableFrenchLocaleText(): any {
return {
// Messages d'erreur et états
noRowsToShow: 'Aucune ligne à afficher',
loadingOoo: 'Chargement...',
errorLoadingOoo: 'Erreur lors du chargement des données',
// Pagination
page: 'Page',
to: 'à',
of: 'sur',
nextPage: 'Page suivante',
lastPage: 'Dernière page',
firstPage: 'Première page',
previousPage: 'Page précédente',
pageSizeSelectorLabel: 'Lignes par page :',
// Filtres
equals: 'égal à',
notEqual: 'différent de',
lessThan: 'inférieur à',
lessThanOrEqual: 'inférieur ou égal à',
greaterThan: 'supérieur à',
greaterThanOrEqual: 'supérieur ou égal à',
inRange: 'dans la plage',
contains: 'contient',
notContains: 'ne contient pas',
startsWith: 'commence par',
endsWith: 'se termine par',
filterOoo: 'Filtrer...',
applyFilter: 'Appliquer',
clearFilter: 'Effacer',
andCondition: 'ET',
orCondition: 'OU',
// Menu de colonnes
pinColumn: 'Épingler la colonne',
pinLeft: 'Épingler à gauche',
pinRight: 'Épingler à droite',
noPin: 'Ne pas épingler',
valueColumn: 'Colonne de valeur',
groupBy: 'Grouper par',
ungroupBy: 'Dégrouper',
resetColumns: 'Réinitialiser les colonnes',
expandAll: 'Tout développer',
collapseAll: 'Tout réduire',
toolPanelButton: 'Panneau doutils',
export: 'Exporter',
csvExport: 'Exporter en CSV',
excelExport: 'Exporter en Excel',
// Tri
sortAscending: 'Trier par ordre croissant',
sortDescending: 'Trier par ordre décroissant',
sortUnsort: 'Annuler le tri',
// Édition
enterValue: 'Entrer une valeur',
copy: 'Copier',
copyWithHeaders: 'Copier avec en-têtes',
paste: 'Coller',
cut: 'Couper',
// Groupes de lignes
group: 'Groupe',
ungroup: 'Dégrouper',
// Autres libellés courants
selectAll: 'Tout sélectionner',
searchOoo: 'Rechercher...',
blanks: '(Vide)',
notBlank: '(Non vide)',
rowGroupColumnsEmptyMessage: 'Glissez ici pour grouper',
pivotColumnsEmptyMessage: 'Glissez ici pour pivoter',
pivotMode: 'Mode pivot',
pivotOff: 'Désactiver pivot',
pivotOn: 'Activer pivot',
pivotChartTitle: 'Graphique pivot',
chartRangeTitle: 'Plage du graphique',
// Messages d'erreur avancés (optionnels)
aggregationWithoutAggregationFunction: 'Agrégation sans fonction dagrégation',
pivotColumnNotSet: 'Colonne pivot non définie',
// Ajoute-en selon tes besoins
};
}
}

View File

@@ -0,0 +1,48 @@
import { Injectable } from '@angular/core';
import * as XLSX from 'xlsx';
import * as FileSaver from 'file-saver';
@Injectable({
providedIn: 'root'
})
export class ExcelExportService {
constructor() { }
private s2ab(s: string): ArrayBuffer {
const buf = new ArrayBuffer(s.length);
const view = new Uint8Array(buf);
for (let i = 0; i !== s.length; ++i) {
view[i] = s.charCodeAt(i) & 0xFF;
}
return buf;
}
/**
* Exports JSON data to an XLSX file.
* @param data The array of JSON objects to export.
* @param fileName The desired name for the Excel file (without extension).
* @param sheetName The name of the sheet within the Excel file (defaults to 'Sheet1').
*/
exportAsExcelFile(data: any[], fileName: string, sheetName: string = 'Sheet1'): void {
if (!data || data.length === 0) {
console.warn('No data to export.');
return;
}
const ws: XLSX.WorkSheet = XLSX.utils.json_to_sheet(data);
const wb: XLSX.WorkBook = { Sheets: { [sheetName]: ws }, SheetNames: [sheetName] };
const excelBuffer: any = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
this.saveAsExcelFile(excelBuffer, fileName);
}
private saveAsExcelFile(buffer: any, fileName: string): void {
const data: Blob = new Blob([buffer], { type: 'application/octet-stream' });
FileSaver.saveAs(data, fileName + '_export_' + new Date().getTime() + '.xlsx');
}
exportDataEnqueteParcelle(): any[] {
return [];
}
}

14
src/app/filter-pipe.ts Normal file
View File

@@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({name: 'filter'})
export class FilterPipe implements PipeTransform {
transform(value: any, searchText: any): any {
if (!searchText) { return value; }
return value.filter((data: any) => this.matchValue(data, searchText));
}
matchValue(data: any, value: any) {
return Object.values(data).findIndex((element) => element && element?.toString()?.indexOf(value) > -1) > -1;
}
}

View File

@@ -0,0 +1,20 @@
import { Observable } from 'rxjs';
import { CrudService } from './crud.service';
import { inject, Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, ResolveFn, RouterStateSnapshot } from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class GlobalResolver implements Resolve<any> {
constructor(private service: CrudService) { }
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any> | any {
let myParam = route.data['resolvedata'];
return this.service.getAll(myParam);
}
}

202
src/app/global.service.ts Normal file
View File

@@ -0,0 +1,202 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class GlobalService {
private _loding$ = new BehaviorSubject<boolean>(false);
private _paramData$ = new BehaviorSubject<any[]>([]);
private _bloc$ = new BehaviorSubject<any>(null);
private _paramURL$ = new BehaviorSubject<string>('/office');
private _enquete$ = new BehaviorSubject<any>(null);
private _commentaireEnquete$ = new BehaviorSubject<any>(null);
private _infoMap$ = new BehaviorSubject<any>(null);
private _enqueteCheckData$ = new BehaviorSubject<any[]>([]);
constructor() {
}
public getLodingSuccess(): Observable<boolean> {
return this._loding$;
}
public setLodingSuccess(data: boolean): void {
this._loding$.next(data);
}
public getParamData(): Observable<any[]> {
return this._paramData$;
}
public setParamData(data: any[]): void {
console.log('data next ==> ', data.length);
this._paramData$.next(data);
}
public getBloc(): Observable<any> {
return this._bloc$;
}
public setBloc(data: any): void {
this._bloc$.next(data);
}
public getEnquete(): Observable<any> {
return this._enquete$;
}
public setEnquete(data: any): void {
this._enquete$.next(data);
}
public getCommentaireEnquete(): Observable<any> {
return this._commentaireEnquete$;
}
public setCommentaireEnquete(data: any): void {
this._commentaireEnquete$.next(data);
}
public getParamURL(): Observable<string> {
return this._paramURL$;
}
public setParamURL(data: string): void {
this._paramURL$.next(data);
}
public getInfoMap(): Observable<any> {
return this._infoMap$;
}
public setInfoMap(data: any): void {
this._infoMap$.next(data);
}
public getEnqueteCheckData(): Observable<any[]> {
return this._enqueteCheckData$;
}
public setEnqueteCheckData(data: any[]): void {
this._enqueteCheckData$.next(data);
}
public getModuleList(): any[] {
return [
{
title: 'Tableau de bord',
description: 'Suivi global des enquêtes foncières fiscales',
detail: "Tableau de bord pour suivre l'avancement des enquêtes fiscales.",
icone: 'team.svg',
classIcon: 'div-icon-dash-dark-header',
classIconBody: 'div-icon-dash-dark',
link: '/dashboard',
color: '#2c2c2c',
dash: true
},
{
title: 'Module référence',
description: "Gestion des données de référence",
detail: "Traitement des données de référence : type parcelle etc.",
icone: 'recouvrement.svg',
classIcon: 'div-icon-dash-dark-header',
classIconBody: 'div-icon-dash-dark',
link: '/core/reference/sommaire-reference',
color: 'rgb(20 97 59)',
dash: true
},
{
title: 'Module liquidation',
description: "Gestion des données de liquidation",
detail: "Traitement des données de liquidation : taxe TFU etc.",
icone: 'solutions-white.svg',
classIcon: 'div-icon-dash-dark-header',
classIconBody: 'div-icon-dash-dark',
link: '/core/liquidation/sommaire-liquidation',
color: 'rgb(145, 66, 66)',
dash: true
},
{
title: 'Module Consultation',
description: 'Dossier en cours sur le module consultation',
detail: 'Vous pouvez accéder au module consultation : consultation des parcelles, des bâtiments etc.',
icone: 'gear-user.svg',
classIcon: 'div-icon-dash-header',
classIconBody: 'div-icon-dash',
link: '/core/consultation/sommaire-consultation',
color: 'rgb(35, 114, 121)',
dash: false
},
{
title: 'Module Cartographie',
description: 'Dossier en cours sur le module cartographie',
detail: 'Vous pouvez accéder au module cartographie : visualisation cartographique des immeubles etc.',
icone: 'tiers.svg',
classIcon: 'div-icon-dash-header',
classIconBody: 'div-icon-dash',
link: '/core/cartographie/data/sommaire-cartographie',
color: '#1a5890',
dash: false
},
{
title: 'Module Enregistrement',
description: 'Dossier en cours sur le module enregistrement',
detail: 'Vous pouvez accéder au module enregistrement : création des parcelles, des bâtiments',
icone: 'solutions-white.svg',
classIcon: 'div-icon-dash-header',
classIconBody: 'div-icon-dash',
link: '/core/enregistrement/sommaire-enregistrement',
color: 'rgb(20 97 59)',
dash: false
},
{
title: 'Module Recherche avancée',
description: 'Dossier en cours sur le module recherche avancée',
detail: 'Vous pouvez accéder au module recherche avancée : recherche de parcelles, de bâtiments etc.',
icone: 'search-white.svg',
classIcon: 'div-icon-dash-header',
classIconBody: 'div-icon-dash',
link: '/core/recherche-avancee/avis-contribuable',
color: 'rgb(205, 145, 28)',
dash: false
},
{
title: 'Module Administration',
description: 'Dossier en cours sur le module utilisateur',
detail: 'Vous pouvez accéder au traitement des dossiers du module Administration : nouvel utilisateur, désactivation, etc.',
icone: 'team.svg',
classIcon: 'div-icon-dash-header',
classIconBody: 'div-icon-dash',
link: '/core/utilisateur/sommaire',
color: 'rgb(97 92 20)',
dash: false
},
{
title: 'Module Organisation',
description: 'Dossier en cours sur le module organisation',
detail: 'Vous pouvez accéder au traitement des dossiers du module Organisation : création des secteurs, des équipes etc.',
icone: 'calculator.svg',
classIcon: 'div-icon-dash-header',
classIconBody: 'div-icon-dash',
link: '/core/programmation/sommaire-organisation',
color: 'rgb(32, 56, 100)',
dash: false
},
];
}
}

View File

@@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { LoginComponent } from './login/login.component';
import { SingupComponent } from './singup/singup.component';
import { RequestPasswordComponent } from './request-password/request-password.component';
const routes: Routes = [
{path: '', component: HomeComponent,
children: [
{ path: '', component: LoginComponent },
{ path: 'login', component: LoginComponent },
{ path: 'singup', component: SingupComponent },
{ path: 'request-password', component: RequestPasswordComponent },
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomeRoutingModule { }

View File

@@ -0,0 +1,56 @@
.parent {
display: flex;
justify-content: center; /* Centrage horizontal */
/*align-items: center; Centrage vertical */
height: 100%; /* Ou une hauteur fixe, ex: 400px */
margin-left: 30%;
flex-direction: column; /* ← Les enfants s'empilent verticalement */
}
.title-login-logo {
margin-left: 30%;
margin-top: 5% !important;
color: #c1ffc1;
}
.flag-container {
width: 100%;
height: 8px;
margin-bottom: 0;
bottom: 0;
margin-top: -2%;
}
.flag {
padding: 0;
height: 100%;
width: 100%;
margin-left: auto;
margin-right: auto;
list-style-type: none;
}
.flag>li:first-child {
background: RGB(16, 135, 87);
}
.flag>li {
height: 100%;
margin: 0;
padding: 0;
width: 33.33%;
display: inline-block;
box-sizing: border-box;
vertical-align: middle;
float: left;
}
.flag>li:first-child+li {
background: RGB(255, 190, 0);
width: 33.34%;
}
.flag li:first-child+li+li {
background: RGB(235, 0, 0);
}

View File

@@ -0,0 +1,54 @@
<div class="row" style="background: #0f3625;background-image: url(/assets/fond-login.2790fbe0.webp);background-repeat: no-repeat;background-position: left center;background-size: cover;">
<div class="col-md-6">
<div class="title-login-logo">
<div class="display-block">
<!--<h3>FISCAD</h3>-->
<img src="assets/logo-2.8886fece.png" alt="" style="width: 57%;">
</div>
</div>
<div class="parent" style="margin-top: -18%;">
<div class="row">
<div class="col-md-12">
<div class="display-block" style="font-weight: bold;">
<p style="font-size: 2.7em;color:white;line-height: 1.5em;"> Bienvenue sur </p>
</div>
</div>
</div>
<div class="display-block">
<h2 style="color: #fc0;display: inline;font-size: 8.3em;font-weight: 600;"> SIGIBé </h2> <br>
<span style="color: white;font-style: italic;font-size: 4em;line-height: 10px;">Foncier <span style="height: 1px;color: #fc0;"> &nbsp; &nbsp; &nbsp; &nbsp;</span></span>
</div>
<div class="display-block" style="margin-top: 15px;">
<h6 class="display-block" style="color:hsla(0,0%,100%,.478);letter-spacing: .3px;opacity: .8;font-style: italic;"> Système de Gestion Unifiée des Impôts Fonciers </h6>
</div>
</div>
</div>
<div class="col-md-6">
<router-outlet></router-outlet>
</div>
</div>
<!--<div class="row">
<div class="col-md-4">
</div>
<div class="col-md-4 text-center">
<img src="assets/static/fond_carte_login.png" alt="" style="width: 110px;margin-top: -35%;">
</div>
<div class="col-md-4">
</div>
</div>-->
<div class="flag-container" style="margin-top: -0.5%;">
<ul class="flag">
<li></li>
<li></li>
<li></li>
</ul>
</div>

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent {
}

View File

@@ -0,0 +1,30 @@
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HomeRoutingModule } from './home-routing.module';
import { HomeComponent } from './home.component';
import { LoginComponent } from './login/login.component';
import { SingupComponent } from './singup/singup.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { SharedModule } from '../shared/shared.module';
import { RequestPasswordComponent } from './request-password/request-password.component';
@NgModule({
declarations: [
HomeComponent,
LoginComponent,
SingupComponent,
RequestPasswordComponent
],
imports: [
CommonModule,
HomeRoutingModule,
FormsModule,
ReactiveFormsModule,
SharedModule,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HomeModule { }

View File

@@ -0,0 +1,14 @@
.parent {
display: flex;
justify-content: center; /* Centrage horizontal */
/*align-items: center; Centrage vertical */
height: 100%; /* Ou une hauteur fixe, ex: 400px */
margin-left: 35%;
flex-direction: column; /* ← Les enfants s'empilent verticalement */
}
.title-login-logo {
margin-left: 35%;
margin-top: 5% !important;
color: #c1ffc1;
}

View File

@@ -0,0 +1,119 @@
<div class="container-fluid page-body-wrapper full-page-wrapper auth-page">
<div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one">
<div class="row w-100">
<div class="col-lg-8 mx-auto">
<div class="auto-form-wrapper">
<div nz-row class="text-center">
<div nz-col nzSpan="24">
<img src="assets/static/images/logo-impot.jfif" alt=""
style="width: 25%;margin-top: -30%;border-radius: 50%;">
</div>
</div>
<div nz-row>
<div nz-col nzSpan="24" class="my-3 text-center">
<h3 nz-typography class="text-primary mb-4"> <strong> CONNEXION </strong></h3>
</div>
</div>
<form [formGroup]="validateForm" *ngIf="validateForm && isPasswordForm == false" role="form"
class="php-email-form">
<nz-alert nzType="error" nzMessage="Erreur" nzCloseable class="mb-15"
[nzDescription]="contenu" nzShowIcon *ngIf="contenu != ''"
(nzOnClose)="createNotification('', '')">
</nz-alert>
<div class="did-floating-label-content">
<input class="did-floating-input" type="email" id="username"
formControlName="username" placeholder="">
<label class="did-floating-label"> <span nz-icon nzType="user"></span> Entrer Votre
identifiant </label>
</div>
<div class="did-floating-label-content" style="margin-top: 8%;">
<input class="did-floating-input" type="password" id="password"
formControlName="password" placeholder="">
<label class="did-floating-label"> <span nz-icon nzType="lock"></span> Entrer Votre
mot de
passe </label>
</div>
<div nz-row class="mb-20">
<div nz-col nzSpan="24">
<button nz-button nzType="primary" nzBlock class="shadow" class="min-heigth-45"
(click)="submit()" [disabled]="isActionInProgress">
{{ isActionInProgress == false ? 'ME CONNECTER' : 'CONNEXION EN COURS ...' }}
</button>
</div>
</div>
<nz-divider nzText="OU"></nz-divider>
<div nz-row class="mb-20">
<div nz-col nzSpan="12" class="text-center"
style="border-right: solid 2px #801C06;">
<button nz-button nzType="primary" nzType="link" class="min-heigth-45"
(click)="goto('/singup')" style="font-size: 12px;">
S'inscrire
</button>
</div>
<div nz-col nzSpan="12" class="text-center">
<button nz-button nzType="primary" nzType="link" class="min-heigth-45"
(click)="goto('/request-password')" style="font-size: 12px;">
Mot de passe oublié ?
</button>
</div>
</div>
</form>
<form [formGroup]="passwordForm" *ngIf="passwordForm && isPasswordForm == true" role="form"
class="php-email-form">
<nz-alert nzType="success" nzMessage="Information" class="mb-15"
[nzDescription]="passwordChangeMessage" nzShowIcon>
</nz-alert>
<nz-alert nzType="error" nzMessage="Erreur" nzCloseable class="mb-15"
[nzDescription]="contenu" nzShowIcon *ngIf="contenu != ''"
(nzOnClose)="createNotification('', '')">
</nz-alert>
<div class="did-floating-label-content" style="margin-top: 8%;">
<input class="did-floating-input" type="password" id="password"
formControlName="password" placeholder="" (blur)="confirmationPassword()">
<label class="did-floating-label"> <span nz-icon nzType="lock"></span> Nouveau mot
de passe </label>
</div>
<div class="did-floating-label-content">
<input class="did-floating-input" type="password" id="confirmation"
formControlName="confirmation" placeholder="" (blur)="confirmationPassword()">
<label class="did-floating-label"> <span nz-icon nzType="user"></span> Confirmation
du mot de passe </label>
</div>
<div nz-row class="mb-20">
<div nz-col nzSpan="24">
<button nz-button nzType="primary" nzBlock class="shadow" class="min-heigth-45"
(click)="changerPassword()" [disabled]="isActionInProgress">
{{ isActionInProgress == false ? 'VALIDER' : 'VALIDATION EN COURS ...' }}
</button>
</div>
</div>
</form>
</div>
<br>
<p class="footer-text text-center my-3" style="color:hsla(0,0%,100%,.478);font-size: 11px;"> <strong> Copyright © 2026 ABS Technology. Tous
droits réservés.
</strong></p>
</div>
</div>
</div>
<!-- content-wrapper ends -->
</div>

View File

@@ -0,0 +1,177 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
import { GlobalService } from 'src/app/global.service';
import { HttpErrorResponse } from '@angular/common/http';
import { CrudService } from 'src/app/crud.service';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzMessageService } from 'ng-zorro-antd/message';
import { JwtHelperService } from '@auth0/angular-jwt';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
validateForm!: FormGroup;
passwordForm!: FormGroup;
passwordShow = false;
redirectURL = null;
isActionInProgress = false;
isPasswordForm = false;
passwordChangeMessage = '';
type = 'info';
contenu: string = '';
constructor(
private fb: FormBuilder,
private router: Router,
private tokenStorage: TokenStorage,
private route: ActivatedRoute,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
) {
}
ngOnInit(): void {
this.validateForm = this.fb.group({
username: [null, [Validators.required]],
password: [null, [Validators.required]],
remember: [false]
});
this.passwordForm = this.fb.group({
confirmation: [null, [Validators.required]],
password: [null, [Validators.required]]
});
}
createNotification(type: string, content: string): void {
this.type = type;
this.contenu = content;
}
goto(url: string): void {
this.router.navigate([url]);
}
submit(): void {
for (const i in this.validateForm?.controls) {
this.validateForm?.controls[i].markAsDirty();
this.validateForm?.controls[i].updateValueAndValidity();
}
if (this.validateForm?.valid) {
this.isActionInProgress = true;
this.isPasswordForm = false;
const formData = this.validateForm?.value;
delete formData.remember;
this.crudService.save('auth/login', formData).subscribe(
(data: any) => {
if (data.success == true) {
/* if (this.isRoles(['ROLE_ENQUETEUR'], data.object?.token) == true) {
this.modal.error({
nzTitle: 'Erreur !',
nzContent: "Votre profil n'est pas éligible pour vous connecter."
});
} else {*/
this.tokenStorage.saveToken(data.object?.token);
this.router.navigate(['/principale']);
//}
} else {
if (data.object == null) {
this.createNotification('error', data.message);
} else {
/* if (this.isRoles(['ROLE_ENQUETEUR'], data.object?.token) == true) {
this.modal.error({
nzTitle: 'Erreur !',
nzContent: "Votre profil n'est pas éligible pour vous connecter."
});
} else {
this.tokenStorage.saveToken(data.object?.token);
this.passwordChangeMessage = data.message;
this.isPasswordForm = true;
this.validateForm.reset();
}*/
}
}
this.isActionInProgress = false;
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.isActionInProgress = false;
}
);
} else {
this.createNotification('error', 'Veuillez renseigner obligatoirement vos identifiants.');
}
}
changerPassword(): void {
for (const i in this.passwordForm?.controls) {
this.passwordForm?.controls[i].markAsDirty();
this.passwordForm?.controls[i].updateValueAndValidity();
}
if (this.passwordForm?.valid) {
this.isActionInProgress = true;
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodedToken = helper.decodeToken(token ? token : '');
console.log(decodedToken);
const formData = this.passwordForm?.value;
this.crudService.save('user/change-password', { "password": formData.password, "username": decodedToken?.sub }).subscribe(
(data: any) => {
if (data.success == true) {
this.tokenStorage.signOut();
this.message.create('error', data.message);
location.reload();
} else {
this.modal.success({
nzTitle: 'Erreur',
nzContent: data.message
});
}
this.isActionInProgress = false;
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.isActionInProgress = false;
}
);
} else {
this.createNotification('error', 'Veuillez renseigner obligatoirement les champs.');
}
}
confirmationPassword(): void {
const formData = this.passwordForm?.value;
if (formData.password != null && formData.confirmation != null
&& formData.password.trim() != '' && formData.confirmation.trim() != '' &&
formData.confirmation.trim() != formData.password) {
this.passwordForm?.get('confirmation')?.setValue(null);
this.message.create('error', `Erreur dans la confirmation du mot de passe.`);
}
}
isRoles(params: any[], token: string): boolean {
const helper = new JwtHelperService();
const decodedToken = helper.decodeToken(token);
console.log(decodedToken);
if (decodedToken && decodedToken.user != null && decodedToken.user.avoirFonctions.length > 0) {
return params.indexOf(decodedToken.user.avoirFonctions[0]?.nom) > -1;
}
return false;
}
}

View File

@@ -0,0 +1,80 @@
<div class="container-fluid page-body-wrapper full-page-wrapper auth-page">
<div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one">
<div class="row w-100">
<div class="col-lg-8 mx-auto">
<div class="auto-form-wrapper">
<div nz-row class="text-center">
<div nz-col nzSpan="24">
<img src="assets/static/images/logo-impot.jfif" alt=""
style="width: 25%;margin-top: -30%;border-radius: 50%;">
</div>
</div>
<div nz-row>
<div nz-col nzSpan="24" class="my-3 text-center">
<h3 nz-typography class="text-primary mb-4"> <strong> DEMANDE DE RÉINITIALISATION </strong>
</h3>
</div>
</div>
<div nz-row>
<div nz-col nzSpan="24" class="bg-white">
<form [formGroup]="validateForm" *ngIf="validateForm" role="form">
<nz-alert nzType="error" nzMessage="Erreur" nzCloseable class="mb-10"
[nzDescription]="contenu" nzShowIcon *ngIf="contenu != ''"
(nzOnClose)="createNotification('', '')">
</nz-alert>
<nz-alert nzType="warning" class="mb-10"
[nzDescription]="'Soumettez le formulaire suivant pour effectuer votre demande de réinitialisation de mot de passe'">
</nz-alert>
<div class="did-floating-label-content">
<input class="did-floating-input" type="email" id="email" formControlName="email"
placeholder="">
<label class="did-floating-label"> <span nz-icon nzType="mail"></span> Entrer
Votre adresse email </label>
</div>
</form>
</div>
</div>
<div nz-row class="mb-20">
<div nz-col nzSpan="24">
<button nz-button nzType="primary" nzBlock class="shadow" class="min-heigth-45"
(click)="changerPassword()" [disabled]="isActionInProgress">
{{ isActionInProgress == false ? 'RÉINITIALISER' : 'DEMANDE EN COURS ...' }}
</button>
</div>
</div>
<nz-divider nzText="OU"></nz-divider>
<div nz-row class="mb-20">
<div nz-col nzSpan="12" class="text-center" style="border-right: solid 2px #801C06;">
<button nz-button nzType="primary" nzType="link" class="min-heigth-45"
(click)="goto('/login')" style="font-size: 12px;">
Se connecter
</button>
</div>
<div nz-col nzSpan="12" class="text-center">
<button nz-button nzType="primary" nzType="link" class="min-heigth-45"
(click)="goto('/singup')" style="font-size: 12px;">
S'inscrire
</button>
</div>
</div>
</div>
<br>
<p class="footer-text text-center my-3" style="color:hsla(0,0%,100%,.478);font-size: 11px;"> <strong>
Copyright © 2026 ABS Technology. Tous
droits réservés.
</strong></p>
</div>
</div>
</div>
<!-- content-wrapper ends -->
</div>

View File

@@ -0,0 +1,100 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
import { GlobalService } from 'src/app/global.service';
import { CrudService } from 'src/app/crud.service';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzMessageService } from 'ng-zorro-antd/message';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-request-password',
templateUrl: './request-password.component.html',
styleUrls: ['./request-password.component.css']
})
export class RequestPasswordComponent implements OnInit {
validateForm!: FormGroup;
passwordShow = false;
actionProgress = false;
redirectURL = null;
isActionInProgress = false;
type = 'info';
contenu: string = '';
constructor(
private fb: FormBuilder,
private router: Router,
private tokenStorage: TokenStorage,
private route: ActivatedRoute,
private globalService: GlobalService,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
) { }
ngOnInit(): void {
this.isActionInProgress = false;
this.validateForm = this.fb.group({
email: [null, [Validators.required]]
});
}
createNotification(type: string, content: string): void {
this.type = type;
this.contenu = content;
}
goto(url: string): void {
this.router.navigate([url]);
}
changerPassword(): void {
for (const i in this.validateForm?.controls) {
this.validateForm?.controls[i].markAsDirty();
this.validateForm?.controls[i].updateValueAndValidity();
}
const formData = this.validateForm?.value;
if (this.validateForm?.valid) {
this.isActionInProgress = true;
this.crudService.getAll('demande-reinitialisation-mp/create?usernamrOrEmail=' + formData.email).subscribe(
(data: any) => {
if (data.success == true) {
this.tokenStorage.signOut();
this.message.create('success', 'Opération effectuée avec succès');
this.modal.success({
nzTitle: 'Information',
nzContent: data.message,
nzOnOk: ()=> {
this.router.navigate(['/']);
},
nzOnCancel: ()=> {
this.router.navigate(['/']);
}
});
} else {
this.message.create('error', data.message);
this.createNotification('', data.message)
}
this.isActionInProgress = false;
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.isActionInProgress = false;
}
);
} else {
this.createNotification('', 'Formulaire invalid.')
/*this.modal.error({
nzTitle: 'Erreur',
nzContent: 'Formulaire invalid.'
});*/
}
}
}

View File

View File

@@ -0,0 +1,123 @@
<div class="container-fluid page-body-wrapper full-page-wrapper auth-page">
<div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one">
<div class="row w-100">
<div class="col-lg-10 mx-auto">
<div class="auto-form-wrapper">
<div nz-row class="text-center">
<div nz-col nzSpan="24">
<img src="assets/static/images/logo-impot.jfif" alt=""
style="width: 20%;margin-top: -20%;border-radius: 50%;">
</div>
</div>
<div nz-row>
<div nz-col nzSpan="24" class="my-3 text-center">
<h3 nz-typography class="text-primary mb-4"> <strong> INSCRIPTION </strong></h3>
</div>
</div>
<form [formGroup]="validateForm" *ngIf="validateForm" role="form" class="php-email-form">
<nz-alert nzType="error" nzMessage="Erreur" nzCloseable class="mb-15" [nzDescription]="contenu"
nzShowIcon *ngIf="contenu != ''" (nzOnClose)="createNotification('', '')">
</nz-alert>
<div nz-row>
<div nz-col nzSpan="12" class="p-1">
<div class="did-floating-label-content">
<input class="did-floating-input" type="text" id="nom"
formControlName="nom" placeholder="">
<label class="did-floating-label"> Nom <span class="text-danger"> * </span> </label>
</div>
</div>
<div nz-col nzSpan="12" class="p-1">
<div class="did-floating-label-content">
<input class="did-floating-input" type="text" id="prenom"
formControlName="prenom" placeholder="">
<label class="did-floating-label"> Prénom(s) <span class="text-danger"> * </span> </label>
</div>
</div>
</div>
<div nz-row>
<div nz-col nzSpan="12" class="p-1">
<div class="did-floating-label-content">
<input class="did-floating-input" type="text" id="telephone"
formControlName="telephone" placeholder="">
<label class="did-floating-label"> Téléphone <span class="text-danger"> * </span> </label>
</div>
</div>
<div nz-col nzSpan="12" class="p-1">
<div class="did-floating-label-content">
<input class="did-floating-input" type="email" id="email"
formControlName="email" placeholder="">
<label class="did-floating-label"> Email <span class="text-danger"> * </span> </label>
</div>
</div>
</div>
<div nz-row>
<div nz-col nzSpan="12" class="p-1">
<div class="did-floating-label-content">
<input class="did-floating-input" type="password" id="password"
formControlName="password" placeholder="" (blur)="confirmationPassword()">
<label class="did-floating-label"> Mot de passe <span class="text-danger"> * </span> </label>
</div>
</div>
<div nz-col nzSpan="12" class="p-1">
<div class="did-floating-label-content">
<input class="did-floating-input" type="password" id="confirmation"
formControlName="confirmation" placeholder="" (blur)="confirmationPassword()">
<label class="did-floating-label"> Confirmation <span class="text-danger"> * </span> </label>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear
nzPlaceHolder="Selectionner le centre de gestion"
formControlName="structureId">
<nz-option *ngFor="let item of structureList" [nzLabel]="item.nom" [nzValue]="item.id"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -13px;"> Sélectionnez le centre de gestion <span class="text-danger"> *</span> </label>
</div>
</div>
</div>
</form>
<div nz-row class="mb-20">
<div nz-col nzSpan="24" class="p-1">
<button nz-button nzType="primary" [disabled]="isActionInProgress"
nzBlock class="shadow" class="min-heigth-45" (click)="submit()">
{{ isActionInProgress == false ? "M'INSCRIRE" : loadingMessage }}
</button>
</div>
</div>
<nz-divider nzText="OU"></nz-divider>
<div nz-row class="mb-20">
<div nz-col nzSpan="12" class="text-center" style="border-right: solid 2px #801C06;">
<button nz-button nzType="primary" nzType="link" class="min-heigth-45"
(click)="goto('/login')" style="font-size: 12px;">
Se connecter
</button>
</div>
<div nz-col nzSpan="12" class="text-center">
<button nz-button nzType="primary" nzType="link" class="min-heigth-45"
(click)="goto('/request-password')" style="font-size: 12px;">
Mot de passe oublié ?
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- content-wrapper ends -->
</div>

View File

@@ -0,0 +1,140 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
import { GlobalService } from 'src/app/global.service';
import { HttpErrorResponse } from '@angular/common/http';
import { CrudService } from 'src/app/crud.service';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzMessageService } from 'ng-zorro-antd/message';
@Component({
selector: 'app-singup',
templateUrl: './singup.component.html',
styleUrls: ['./singup.component.css']
})
export class SingupComponent implements OnInit {
validateForm!: FormGroup;
passwordShow = false;
redirectURL = null;
structureList: any[] = [];
isActionInProgress = false;
loadingMessage ='';
type = 'info';
contenu: string = '';
constructor(
private fb: FormBuilder,
private router: Router,
private tokenStorage: TokenStorage,
private route: ActivatedRoute,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
) {
}
ngOnInit(): void {
this.validateForm = this.fb.group({
nom: [null, [Validators.required]],
prenom: [null, [Validators.required]],
telephone: [null, [Validators.required]],
email: [null, [Validators.required]],
password: [null, [Validators.required]],
confirmation: [null, [Validators.required]],
structureId: [null, [Validators.required]]
});
this.list();
}
createNotification(type: string, content: string): void {
this.type = type;
this.contenu = content;
}
compareFn = (o1: any, o2: any) => (o1 && o2 ? o1.id === o2.id : o1 === o2);
list(): void {
this.isActionInProgress = true;
this.loadingMessage = 'Chargement des données ...';
this.structureList = [];
this.crudService.getAll('structure/all').subscribe(
(data: any) => {
this.structureList = data != null ? data.object : [];
this.isActionInProgress = false;
this.loadingMessage = '';
},
(error: HttpErrorResponse) => {
console.log('OK');
this.isActionInProgress = false;
this.loadingMessage = '';
}
);
}
goto(url: string): void {
this.router.navigate([url]);
}
confirmationPassword(): void {
const formData = this.validateForm?.value;
if (formData.password != null && formData.confirmation != null
&& formData.password.trim() != '' && formData.confirmation.trim() != '' &&
formData.confirmation.trim() != formData.password) {
this.validateForm?.get('confirmation')?.setValue(null);
this.message.create('error', `Erreur dans la confirmation du mot de passe.`);
}
}
submit(): void {
for (const i in this.validateForm?.controls) {
this.validateForm?.controls[i].markAsDirty();
this.validateForm?.controls[i].updateValueAndValidity();
}
if (this.validateForm?.valid) {
this.isActionInProgress = true;
this.loadingMessage = 'INSCRIPTION EN COURS ...';
const formData = this.validateForm?.value;
this.crudService.save('auth/signup', formData).subscribe(
(data: any) => {
if (data.success == true) {
this.modal.success({
nzTitle: 'Succès !',
nzContent: data.message,
nzOnOk: ()=> {
this.router.navigate(['/']);
},
nzOnCancel: ()=> {
this.router.navigate(['/']);
}
});
} else {
this.createNotification('error', data.message);
}
this.isActionInProgress = false;
this.loadingMessage = '';
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.isActionInProgress = false;
this.loadingMessage = '';
}
);
} else {
this.createNotification('error', 'Veuillez renseigner correctement tous les champs.');
/*this.modal.error({
nzTitle: 'Erreur !',
nzContent: 'Veuillez renseigner obligatoirement vos identifiants.'
});*/
}
}
}

View File

@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ManageComponent } from './manage.component';
import { MonCompteComponent } from '../shared/mon-compte/mon-compte.component';
import { ResetPasswordComponent } from '../shared/reset-password/reset-password.component';
const routes: Routes = [
{
path: '', component: ManageComponent,
children: [
{ path: 'mon-compte', component: MonCompteComponent },
{ path: 'reset-password', component: ResetPasswordComponent },
]
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ManageRoutingModule { }

View File

@@ -0,0 +1,9 @@
.badge-success {
background: #04cd6761;
color: #14613b;
}
.badge-danger {
background: #ff001829;
color: #fe0118;
}

View File

@@ -0,0 +1,367 @@
<nav class="navbar default-layout col-lg-12 col-12 p-0 fixed-top d-flex flex-row"
style="background: #ffff;height: 80px!important;box-shadow: rgba(0, 0, 0, 0.15) 5px 5px 30px 0px;">
<div class="text-center navbar-brand-wrapper d-flex align-items-top justify-content-center"
style="align-items: center;">
<a class="navbar-brand brand-logo" href="">
<img src="assets/logo2.png" alt="logo" style="height: 50px!important;" />
</a>
<a class="navbar-brand brand-logo-mini" href="">
<img src="assets/logo2.png" alt="logo" style="height: 50px !important;" />
</a>
<a href="#">
<img src="assets/sigibe/menu-black.svg" alt="logo" style="height: 16px;margin-left: 10px;" />
</a>
</div>
<div class="navbar-menu-wrapper d-flex align-items-center">
<button class="btn btn-primary btn-fw btn-select-module" style="height: 40px;background: #2c2c2c;border: none;"
nz-popover nzPopoverTitle="Modules applicatifs" [(nzPopoverVisible)]="visible"
(nzPopoverVisibleChange)="change($event)" nzPopoverTrigger="click" [nzPopoverContent]="contentTemplate"
nzPopoverPlacement="rightBottom">
<i class="mdi mdi-settings" style="font-size: 15px;"> </i>
<span> Sélectionner un module </span>
</button>
<ng-template #contentTemplate>
<nz-list>
<nz-list-item *ngFor="let item of moduleList" class="review-list-item"
(click)="selectModuleAccess(item)">
<div class="p-2 {{item.classIcon}}"
[ngStyle]="{ backgroundColor: item && item.color ? item.color : '' }">
<img src="assets/sigibe/{{ item.icone }}" alt="">
</div>
<div style="line-height: 5px;">
<h3 style="font-size: 12px;margin-left: 10px;color: #3cb22a;"> {{ item.title }} </h3>
<h6 style="font-size: 12px;margin-left: 10px;color: rgb(99, 99, 99);font-weight: 900;">
{{ item.description }}
</h6>
<!--<p style="font-size: 11px;margin-left: 10px;color: rgb(99, 99, 99);line-height: 16px;margin-bottom: 0px;">
{{ item.detail }}
</p>-->
</div>
</nz-list-item>
</nz-list>
</ng-template>
<ul class="navbar-nav navbar-nav-right">
<!--<li class="nav-item">
<a class="nav-link count-indicator ico-header"
(click)="goto('/administration/enquete/lot-validation-rejet')">
<i class="mdi mdi-check"></i>
<span class="count icon-notify" *ngIf="enqueteCheckList.length > 0">
{{ enqueteCheckList.length }}
</span>
</a>
</li>-->
<li class="nav-item dropdown">
<a class="nav-link count-indicator dropdown-toggle ico-header" id="notificationDropdown"
data-toggle="dropdown" style="margin-right: 30px;">
<i class="mdi mdi-bell"></i>
<span class="count icon-notify" style="width: auto !important; min-width: 18px; padding-right: 5px; padding-left: 5px;" *ngIf="getTotalNotification() > 0">
<span> {{ getTotalNotification() }} </span>
</span>
</a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown preview-list"
aria-labelledby="notificationDropdown" *ngIf="getTotalNotification() > 0">
<a class="dropdown-item">
<p class="mb-0 font-weight-normal float-left">
Vous avez <span style="margin-left: 0;font-weight: bold;"
[ngClass]="getTotalNotification() > 0 ? 'badge badge-danger': 'badge badge-success'">
{{ getTotalNotification() }} </span> notifications en attentes
</p>
<span class="badge badge-pill badge-warning float-right"
(click)="goto('/core/enregistrement/fiche-validation-enquete-parcelle')" style="cursor: pointer;">
Voir tout
</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item preview-item" (click)="goto('/core/enregistrement/fiche-validation-enquete-parcelle')">
<div class="preview-thumbnail">
<div class="preview-icon bg-success">
<i class="mdi mdi-map-marker-outline mx-0"></i>
</div>
</div>
<div class="preview-item-content">
<h6 class="preview-subject font-weight-medium text-dark">
Parcelles
</h6>
<p class="font-weight-light small-text">
<span style="margin-left: 0;font-weight: bold;"
[ngClass]="notifEnqueteList?.nombreEnqueteParcelle > 0 ? 'badge badge-danger': 'badge badge-success'">
{{ notifEnqueteList?.nombreEnqueteParcelle ?? 0 }} </span>
enquêtes parcelles en attente
</p>
</div>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item preview-item" (click)="goto('/core/enregistrement/fiche-validation-enquete-batiment')">
<div class="preview-thumbnail">
<div class="preview-icon bg-success">
<i class="mdi mdi-home mx-0"></i>
</div>
</div>
<div class="preview-item-content">
<h6 class="preview-subject font-weight-medium text-dark">
Bâtiments
</h6>
<p class="font-weight-light small-text">
<span style="margin-left: 0;font-weight: bold;"
[ngClass]="notifEnqueteList?.nombreEnqueteBatiment > 0 ? 'badge badge-danger': 'badge badge-success'">
{{ notifEnqueteList?.nombreEnqueteBatiment ?? 0 }} </span>
enquêtes bâtiments en attente
</p>
</div>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item preview-item" (click)="goto('/core/enregistrement/fiche-validation-enquete-unite-logement')">
<div class="preview-thumbnail">
<div class="preview-icon bg-success">
<i class="mdi mdi-grid mx-0"></i>
</div>
</div>
<div class="preview-item-content">
<h6 class="preview-subject font-weight-medium text-dark">
Unités de logement
</h6>
<p class="font-weight-light small-text">
<span style="margin-left: 0;font-weight: bold;"
[ngClass]="notifEnqueteList?.nombreEnqueteUniteLogement > 0 ? 'badge badge-danger': 'badge badge-success'">
{{ notifEnqueteList?.nombreEnqueteUniteLogement ?? 0 }} </span>
enquêtes unités de logement en attente
</p>
</div>
</a>
</div>
</li>
<li class="nav-item dropdown d-none d-xl-inline-block">
<a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown"
aria-expanded="false">
<span class="profile-text"
style="color: #333;">{{ user ? (user.nom | uppercase) : 'Inconnu' }}</span>
<img class="img-xs rounded-circle" src="assets/static/avatar.png" alt="Profile image">
</a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown">
<a class="dropdown-item p-0">
<div class="d-flex border-bottom">
<div class="py-3 px-4 d-flex align-items-center justify-content-center"
(click)="goto('/principale/reset-password')">
<i class="mdi mdi-settings mr-0 text-gray"></i>
</div>
<div (click)="goto('/principale/mon-compte')"
class="py-3 px-4 d-flex align-items-center justify-content-center border-left border-right">
<i class="mdi mdi-account-outline mr-0 text-gray"></i>
</div>
<div class="py-3 px-4 d-flex align-items-center justify-content-center"
(click)="goto('/principale')">
<i class="mdi mdi-chart-line mr-0 text-gray"></i>
</div>
</div>
</a>
<a class="dropdown-item mt-2" routerLinkActive="active-menu-link-user"
[routerLink]="['/core/mon-compte']">
<i class="mdi mdi-account-outline mr-2 text-gray"></i> Mon compte
</a>
<a class="dropdown-item" routerLinkActive="active-menu-link-user"
[routerLink]="['/core/reset-password']">
<i class="mdi mdi-settings mr-2 text-gray"></i> Changer mot de passe
</a>
<a class="dropdown-item" routerLinkActive="active-menu-link-user"
[routerLink]="['/administration/dashbord']">
<i class="mdi mdi-chart-line mr-2 text-gray"></i> Tableau de bord
</a>
<a class="dropdown-item" (click)="singOut()">
<i class="mdi mdi-close mr-2 text-gray"></i> Déconnexion
</a>
</div>
</li>
</ul>
<button class="navbar-toggler navbar-toggler-right d-lg-none align-self-center" type="button"
data-toggle="offcanvas">
<span class="mdi mdi-menu"></span>
</button>
</div>
</nav>
<!-- partial -->
<div class="row" style="background-color: rgb(20 97 59);margin-top: 75px;">
<div class="col-md-5" style="padding: 25px;">
<h3 class="text-secondary ml-5"
style="font-size: 11px;text-transform: uppercase;margin-top: 2%;color: rgba(255, 255, 255, 0.61) !important;">
Bienvenue dans votre espace personnel sécurisé</h3>
<h1 class="text-white ml-5" style="font-size: 11px;line-height: 1.5;">
Utilisateur connecté : DÉVELOPPEMENT ADMIN<br>
Fonction sélectionnée : Développement UNIQUEMENT - Administrateur technique</h1>
</div>
<div class="col-md-5">
</div>
<div class="col-md-2" style=" padding-left: 3.1%;">
<h3 class="text-secondary"
style="font-size: 10px;text-transform: uppercase;margin-top: 8%;color: rgba(255, 255, 255, 0.61) !important;">
Dèrnières connexions</h3>
<span class="text-white" style="font-size: 10px;display:block;line-height: 1.5;">23h22 lundi 26 janvier
2026</span>
<span class="text-white" style="font-size: 10px;display:block;line-height: 1.5;">23h22 lundi 26 janvier
2026</span>
<span class="text-white" style="font-size: 10px;display:block;line-height: 1.5;">23h22 lundi 26 janvier
2026</span>
<span class="text-white" style="font-size: 10px;display:block;line-height: 1.5;">23h22 lundi 26 janvier
2026</span>
<span class="text-white" style="font-size: 10px;display:block;line-height: 1.5;">23h22 lundi 26 janvier
2026</span>
</div>
</div>
<div class="row" style="background: #f5f5f5ff;">
<div class="col-md-12" style="padding: 2% 5%;display: flex;">
<input type="text" class="search-menu-dash mt-2 mb-2"
placeholder="Rechercher un contribuable pour accéder à son dossier ..." nz-popover nzPopoverTrigger="click"
[nzPopoverContent]="contentTemplateSearch" nzPopoverPlacement="leftBottom">
<button class="btn-search-input">
<img src="assets/sigibe/search-white.svg" alt="">
</button>
<ng-template #contentTemplateSearch>
<strong> Tapez au moins 3 caractères. </strong>
</ng-template>
</div>
</div>
<!--<div class="row" style="background: #0d633c59;">-->
<div class="row">
<!--<div class="col-md-2 center-vertical text-center">
<img src="assets/static/fond_carte_login.png" alt="" class="img-left-menu">
</div>-->
<div class="col-md-12" style="padding: 2% 5%;">
<div class="row">
<div class="col-md-12 p-5">
<h1 class="titrePageDashboard"> Mon tableau de bord </h1>
<div class="row">
<div class="col-md-4 div4-menu-dashboard" *ngFor="let item of moduleListDash()"
(click)="selectModuleAccess(item)">
<div class="p-2 {{ item.classIconBody }}"
[ngStyle]="{ backgroundColor: item ? item.color : '' }">
<img src="assets/sigibe/{{ item.icone }}" alt="" style="height: 20px;">
</div>
<div>
<h3 style="font-size: 12px;margin-left: 10px;color: #3cb22a;;"> {{ item.title }} </h3>
<h6 style="font-size: 13px;margin-left: 10px;color: #333;font-weight: 900;">
{{ item.description }} </h6>
<p style="font-size: 11px;margin-left: 10px;color: rgb(99, 99, 99);"> {{ item.detail }}
</p>
</div>
</div>
<!--<div class="col-md-4" style="display: flex;">
<div class="p-2 div-icon-dash" style="background: rgb(44 44 44) !important">
<img src="assets/sigibe/team.svg" alt="" style="height: 20px;">
</div>
<div>
<h3 style="font-size: 12px;margin-left: 10px;color: #3cb22a;;"> Tableau de bord </h3>
<h6 style="font-size: 13px;margin-left: 10px;color: #333;font-weight: 900;"> Suivi global
des enquêtes foncières fiscales </h6>
<p style="font-size: 11px;margin-left: 10px;color: rgb(99, 99, 99);"> Tableau de bord pour
suivre l'avancement des enquêtes fiscales. </p>
</div>
</div>
<div class="col-md-4" style="display: flex;">
<div class="p-2 div-icon-dash" style="background: rgb(44 44 44) !important">
<img src="assets/sigibe/team.svg" alt="" style="height: 20px;">
</div>
<div>
<h3 style="font-size: 12px;margin-left: 10px;color: #3cb22a;"> Tableau de bord </h3>
<h6 style="font-size: 13px;margin-left: 10px;color: #333;font-weight: 900;"> Suivi de
l'exploitation </h6>
<p style="font-size: 11px;margin-left: 10px;color: rgb(99, 99, 99);"> Tableau de bord pour
suivre l'exploitation des données fiscales. </p>
</div>
</div>-->
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 p-5">
<h1 class="titrePageDashboard"> Accès dossiers en cours </h1>
<div class="row">
<div class="col-md-4 div4-menu-dashboard mb-5" *ngFor="let item of moduleListExploitation()"
(click)="selectModuleAccess(item)">
<div class="p-2 {{ item.classIconBody }}"
[ngStyle]="{ backgroundColor: item && item.color ? item.color : '' }">
<img src="assets/sigibe/{{ item.icone }}" alt="" style="height: 20px;">
</div>
<div>
<h3 style="font-size: 12px;margin-left: 10px;color: #3cb22a;"> {{ item.title }} </h3>
<h6 style="font-size: 13px;margin-left: 10px;color: #333;font-weight: 900;">
{{ item.description }} </h6>
<p style="font-size: 11px;margin-left: 10px;color: rgb(99, 99, 99);"> {{ item.detail }}
</p>
</div>
</div>
</div>
</div>
</div>
<div class="row" style="margin-top: -3% !important;">
<div class="col-md-12 p-5">
<h1 class="titrePageDashboard"> Accès rapide </h1>
<div class="formulaire">
<div>
<h2 style="font-size: 16px;">Accès rapide à tous les dossiers</h2>
<p style="font-size: 11px;color: rgb(99, 99, 99);">Saisissez la référence d'un dossier et
cliquez sur le bouton "Rechercher". Vous serez
redirigé vers un autre dossier du même type.</p>
</div>
<form [formGroup]="rechercheForm" *ngIf="rechercheForm" style="width: 100%;">
<div class="row">
<div class="col-md-12 mt-3">
<div class="did-floating-label-content">
<nz-select nzShowSearch nzAllowClear
nzPlaceHolder="Selectionner le critère de recherche" formControlName="type">
<nz-option *ngFor="let item of typeList" [nzLabel]="item.nom"
[nzValue]="item.value"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -10px;"> Critère de recherche
</label>
</div>
</div>
<div class="col-md-12 mt-3">
<div class="did-floating-label-content me-1">
<input class="did-floating-input" type="text" id="reference"
formControlName="reference" placeholder="">
<label class="did-floating-label"> Saisissez la référence du dossier à consulter
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<button class="btn btn-primary btn-fw btn-select-module"
style="height: 40px;background: #2c2c2c;border: none;float: right;">
<span> Rechercher </span>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<app-footer></app-footer>

View File

@@ -0,0 +1,136 @@
import { Component, ChangeDetectorRef, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { GlobalService } from '../global.service';
import { TokenStorage } from '../utilitaire/token-storage';
import { JwtHelperService } from '@auth0/angular-jwt';
import { firstValueFrom } from 'rxjs';
import { CrudService } from '../crud.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { link } from 'fs';
@Component({
selector: 'app-manage',
templateUrl: './manage.component.html',
styleUrls: ['./manage.component.css']
})
export class ManageComponent implements OnInit {
user: any = null;
enqueteList: any[] = [];
commentaireList: any[] = [];
enqueteCheckList: any[] = [];
rechercheForm?: FormGroup;
typeList = [
{ nom: 'Numéro NPI du contribuable', value: 'npi' },
{ nom: 'Numéro IFU du contribuable', value: 'ifu' },
{ nom: 'Numéro du titre foncier de la parcelle', value: 'tf' },
{ nom: 'Numéro de la parcelle au cadastre', value: 'nup' },
{ nom: 'Numéro provisoire de la parcelle au cadastre', value: 'nupp' },
{ nom: 'Quartier, Ilot, Parcelle', value: 'qip' },
];
visible: boolean = false;
moduleList: any[] = [];
currentModule: any = null;
notifEnqueteList: any = null;
constructor(
private globalService: GlobalService,
private cdref: ChangeDetectorRef,
private router: Router,
private tokenStorage: TokenStorage,
private crudService: CrudService,
private fb: FormBuilder,
) {
this.currentModule = this.tokenStorage.getModule();
console.log('this.currentModule ==>', this.currentModule);
}
ngOnInit(): void {
this.crudService.getAll(
`statistique/nombre-enquete/par-objet/en-cours`
).subscribe({
next: (data: any) => {
console.log('notif ===>', data);
this.notifEnqueteList = data && data.object ? data.object : null;
}});
this.moduleList = this.globalService.getModuleList();
this.rechercheForm = this.fb.group({
type: [null, [Validators.required]],
reference: [null]
});
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
console.log(this.user);
this.globalService.getEnqueteCheckData().subscribe(
(data: any) => {
this.enqueteCheckList = data;
});
}
getTotalNotification(): number {
if(this.notifEnqueteList)
return (this.notifEnqueteList.nombreEnqueteBatiment + this.notifEnqueteList.nombreEnqueteParcelle + this.notifEnqueteList.nombreEnqueteUniteLogement);
return 0;
}
singOut(): void {
this.tokenStorage.signOut();
this.router.navigate(['/']);
}
goto(url: string): void {
this.router.navigate([url]);
}
isOneRoles(param: any): boolean {
if (this.user != null) {
return this.user.roles ? this.user.roles.indexOf(param) > -1 : false;
}
return false;
}
isRoles(params: any[]): boolean {
if (this.user != null) {
return params.indexOf(this.user.roles[0]?.nom) > -1;
}
return false;
}
clickMe(): void {
this.visible = false;
}
change(value: boolean): void {
console.log(value);
}
selectModuleAccess(module: any): void {
console.log('module ==>', module);
this.tokenStorage.saveModule(JSON.stringify(module));
this.router.navigate([module.link]);
}
moduleListDash(): any[] {
return this.moduleList.filter(m => m.dash == true);
}
moduleListExploitation(): any[] {
return this.moduleList.filter(m => m.dash == false);
}
}

View File

@@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ManageRoutingModule } from './manage-routing.module';
import { ManageComponent } from './manage.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { SharedModule } from '../shared/shared.module';
@NgModule({
declarations: [
ManageComponent
],
imports: [
CommonModule,
ManageRoutingModule,
FormsModule,
ReactiveFormsModule,
SharedModule,
]
})
export class ManageModule { }

View File

@@ -0,0 +1,5 @@
export class LoginRequest {
password!: string;
username!: string;
}

5
src/app/models/role.ts Executable file
View File

@@ -0,0 +1,5 @@
export class Role {
id?: number;
name?: string;
libelle?: string;
}

19
src/app/models/user.ts Executable file
View File

@@ -0,0 +1,19 @@
import { Role } from './role';
export interface User {
email: string;
id: number;
nom: string;
prenom: string;
roles: Role[];
username: string;
profil: string;
tel: string;
role: string;
createdAt: string;
estSupprimer: boolean;
updatedAt: string;
createdBy: number;
updatedBy: number;
}

View File

@@ -0,0 +1,49 @@
<div class="row" [ngClass]="isActionInProgress ? 'hidden-for-loading': 'visible-for-loading'">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4 class="card-title" style="font-size: 18px!important;font-weight: 900;">BLOCS</h4>
<p class="card-description">
Liste des différents blocs (découpage des zones) assignés
</p>
<div class="table-responsive">
<table class="table table-striped" datatable [dtOptions]="dtOptions" [dtTrigger]="dtTrigger">
<thead>
<tr>
<th>
Code
</th>
<th>
Nom
</th>
<th>
Secteur
</th>
<th>
Découpage
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let todo of blocsList; let i=index" class="border-bottom-light">
<td style="padding: 15px !important;">
{{ todo.cote }}
</td>
<td>
{{ todo.nom }}
</td>
<td>
{{ todo.secteur.nom }}
</td>
<td>
{{ todo.arrondissement && todo.arrondissement.commune ? todo.arrondissement.commune.nom : '-' }} / {{ todo.arrondissement ? todo.arrondissement.nom : '-' }} {{ todo.quartier ? ' / '+ todo.quartier.nom : '' }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,109 @@
import { Component, ChangeDetectionStrategy, OnInit, ViewChild } from '@angular/core';
import { DataTableDirective } from 'angular-datatables';
import { Subject } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { CrudService } from 'src/app/crud.service';
import { GlobalService } from 'src/app/global.service';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
import { JwtHelperService } from '@auth0/angular-jwt';
@Component({
selector: 'app-bloc-by-structure',
templateUrl: './bloc-by-structure.component.html',
styleUrls: ['./bloc-by-structure.component.css']
})
export class BlocByStructureComponent implements OnInit {
@ViewChild(DataTableDirective, { static: false })
dtElement?: DataTableDirective;
dtOptions: DataTables.Settings = {};
dtTrigger: Subject<any> = new Subject<any>();
blocsList: any[] = [];
isActionInProgress: boolean = false;
user: any = null;
constructor(
private crudService: CrudService,
private globalService: GlobalService,
private tokenStorage: TokenStorage,
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
ngOnInit(): void {
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
console.log(this.user);
this.globalService.setLodingSuccess(false);
this.dtOptions = {
pagingType: 'full_numbers',
pageLength: 10,
processing: true,
language: {
search: "Rechercher&nbsp;:",
emptyTable: "Aucune donnée disponible",
lengthMenu: "Afficher _MENU_ &eacute;l&eacute;ments",
info: "Affichage de l'&eacute;lement _START_ &agrave; _END_ sur _TOTAL_ &eacute;l&eacute;ments",
infoEmpty: "Affichage de l'&eacute;lement 0 &agrave; 0 sur 0 &eacute;l&eacute;ments",
infoFiltered: "(filtr&eacute; de _MAX_ &eacute;l&eacute;ments au total)",
paginate: {
first: "<i class='menu-icon mdi mdi-chevron-double-left'></i>",
previous: "<i class='menu-icon mdi mdi-chevron-left'></i>",
next: "<i class='menu-icon mdi mdi-chevron-right'></i>",
last: "<i class='menu-icon mdi mdi-chevron-double-right'></i>"
},
}
};
this.list();
}
ngAfterViewInit(): void {
this.dtTrigger.next(this.dtOptions);
}
ngOnDestroy(): void {
this.dtTrigger.unsubscribe();
}
list(): void {
this.globalService.setLodingSuccess(true);
this.blocsList = [];
this.refreshDataTable();
if(this.user && this.user.structure) {
this.crudService.getAll('bloc/list-by-structure?idStructure='+this.user.structure?.id).subscribe(
(data: any) => {
this.blocsList = data != null ? data.object : [];
this.blocsList = [...this.blocsList];
this.refreshDataTable();
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
console.log('OK');
this.globalService.setLodingSuccess(false);
}
);
}
}
refreshDataTable(): void {
this.dtElement?.dtInstance.then((dtInstance: DataTables.Api) => {
// Destroy the table first
dtInstance.destroy();
// Call the dtTrigger to rerender again
this.dtTrigger.next(null);
});
}
}

View File

@@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BlocQuartierComponent } from './bloc-quartier.component';
import { BlocByStructureComponent } from './bloc-by-structure/bloc-by-structure.component';
const routes: Routes = [
{ path: 'gestion-bloc', component: BlocQuartierComponent },
{ path: 'bloc-by-structure', component: BlocByStructureComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class BlocQuartierRoutingModule { }

View File

@@ -0,0 +1,184 @@
<div class="row" [ngClass]="isActionInProgress ? 'hidden-for-loading': 'visible-for-loading'">
<div class="col-lg-12 grid-margin stretch-card" *ngIf="isForm == true">
<div class="card">
<div class="card-body">
<h4 class="card-title" style="font-size: 18px!important;font-weight: 900;">Formulaire</h4>
<nz-divider></nz-divider>
<form [formGroup]="blocForm" *ngIf="blocForm">
<div class="row">
<!--<div class="col-lg-6">
<div class="did-floating-label-content mt-3">
<input class="did-floating-input" type="text" id="cote"
formControlName="cote" placeholder="">
<label class="did-floating-label"> Code du bloc <span class="text-danger"> *</span> </label>
</div>
</div>-->
<div class="col-lg-6">
<div class="did-floating-label-content">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Selectionner le secteur"
formControlName="secteur" [compareWith]="compareFn"
(ngModelChange)="checkSecteurDecoupageBySecteur($event)">
<nz-option *ngFor="let item of secteurList" [nzLabel]="item.nom"
[nzValue]="item"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Secteur d'intervention
<span class="text-danger"> *</span> </label>
</div>
</div>
<div class="col-lg-6">
<div class="did-floating-label-content">
<input class="did-floating-input" type="text" id="nom"
formControlName="nom" placeholder="">
<label class="did-floating-label"> Nom du bloc <span class="text-danger"> *</span> </label>
</div>
</div>
<!--<div class="col-lg-4">
<div class="did-floating-label-content">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Selectionner le quartier"
formControlName="quartier" [compareWith]="compareFn">
<nz-option *ngFor="let item of quartierList" [nzLabel]="item.nom"
[nzValue]="item"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Quartier </label>
</div>
</div>-->
</div>
<div class="row mt-3">
<div class="col-lg-12">
<div class="did-floating-label-content">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Selectionner le découpage"
formControlName="secteurDecoupage" [compareWith]="compareFn"
(ngModelChange)="checkSecteurDecoupage($event)">
<nz-option *ngFor="let item of secteurDecoupageList" [nzLabel]="(item.arrondissement ? item.arrondissement.commune.nom +' / ' : '') + ' '+ item.arrondissement.nom + (item.quartier ? ' / '+ item.quartier.nom : '')"
[nzValue]="item"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Découpage territorial </label>
</div>
</div>
</div>
<button class="btn btn-secondary btn-fw mr-2"
(click)="showHideForm(false)" [disabled]="isActionInProgress">
Fermer
</button>
<button class="btn btn-primary btn-fw" (click)="saveForm()"
[disabled]="isActionInProgress && arrondissementPaylod == null">
{{ isActionInProgress == false ? 'Enregistrer' : 'Opération en cours ...' }}
</button>
</form>
</div>
</div>
</div>
</div>
<div class="row" [ngClass]="isActionInProgress ? 'hidden-for-loading': 'visible-for-loading'">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<div class="row">
<!--<div class="col-lg-6">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear
nzPlaceHolder="Selectionner la commune" (ngModelChange)="filterArrondissementByCommune($event)"
[(ngModel)]="communePaylod" [compareWith]="compareFn">
<nz-option *ngFor="let item of communeList" [nzLabel]="item.nom" [nzValue]="item"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Commune <span class="text-danger"> *</span> </label>
</div>
</div>-->
<div class="col-lg-12">
<!--<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear
nzPlaceHolder="Selectionner l'arrondissement" (ngModelChange)="listQuartierByArrondissement($event)"
[(ngModel)]="arrondissementPaylod" [compareWith]="compareFn">
<nz-option *ngFor="let item of arrondissementList" [nzLabel]="item.nom" [nzValue]="item"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Arrondissement <span class="text-danger"> *</span> </label>
</div>-->
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Selectionner le centre d'impôt"
(ngModelChange)="listBlocsAndSecteurByStructure($event)" [(ngModel)]="structurePaylod"
[compareWith]="compareFn">
<nz-option *ngFor="let item of structureList" [nzLabel]="item.nom"
[nzValue]="item"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Centre d'impôt
<span class="text-danger"> *</span> </label>
</div>
</div>
</div>
<nz-divider></nz-divider>
<h4 class="card-title" style="font-size: 18px!important;font-weight: 900;">BLOCS</h4>
<p class="card-description">
Liste des différents blocs (découpage des zones) pour la gestion des enquêtes
</p>
<div class="table-responsive">
<table class="table table-striped"
datatable [dtOptions]="dtOptions"
[dtTrigger]="dtTrigger">
<thead>
<tr>
<th>
Code
</th>
<th>
Nom
</th>
<th>
Secteur
</th>
<th>
Découpage
</th>
<th class="text-center">
<button type="button" class="btn btn-icons btn-rounded btn-primary mr-1"
[disabled]="structurePaylod == null"
(click)="showHideForm(true);makeForm(null)">
<i class="mdi mdi-plus btn-icon-modify"></i>
</button>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let todo of blocList; let i=index" class="border-bottom-light">
<td>
{{ todo.coteq ? todo.coteq : todo.cote }}
</td>
<td>
{{ todo.nom }}
</td>
<td>
{{ todo.secteur ? todo.secteur.nom : '-' }}
</td>
<td>
{{ todo.arrondissement && todo.arrondissement.commune ? todo.arrondissement.commune.nom : '-' }} / {{ todo.arrondissement ? todo.arrondissement.nom : '-' }} {{ todo.quartier ? ' / '+ todo.quartier.nom : '' }}
</td>
<!--<td class="text-center">
<label class="badge badge-info" *ngIf="todo.quartiers.length > 0"> {{ todo.quartiers.length }}</label>
<label class="badge badge-danger" *ngIf="todo.quartiers.length == 0"> {{ todo.quartiers.length }}</label>
</td>-->
<td class="text-center">
<button type="button" class="btn btn-icons btn-rounded btn-success mr-1"
(click)="makeForm(todo)">
<i class="mdi mdi-pencil btn-icon-modify"></i>
</button>
<button type="button" class="btn btn-icons btn-rounded btn-danger"
(click)="showDeleteConfirm(todo, i)">
<i class="mdi mdi-delete btn-icon-modify"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,367 @@
import { Component, ChangeDetectionStrategy, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DataTableDirective } from 'angular-datatables';
import { forkJoin, Subject } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { CrudService } from 'src/app/crud.service';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzMessageService } from 'ng-zorro-antd/message';
import { GlobalService } from 'src/app/global.service';
export interface Bloc {
id: number;
cote: string;
nom: string;
arrondissement: any;
structure: any;
quartier: any;
secteurDecoupage: any;
secteur: any;
quartiers: any[];
}
@Component({
selector: 'app-bloc-quartier',
templateUrl: './bloc-quartier.component.html',
styleUrls: ['./bloc-quartier.component.css']
})
export class BlocQuartierComponent implements OnInit {
@ViewChild(DataTableDirective, { static: false })
dtElement?: DataTableDirective;
dtOptions: DataTables.Settings = {};
dtTrigger: Subject<any> = new Subject<any>();
isForm = false;
quartierList: any[] = [];
arrondissementList: any[] = [];
arrondissementFilteredList: any[] = [];
communeList: any[] = [];
structureList: any[] = [];
secteurList: any[] = [];
secteurDecoupageList: any[] = [];
blocList: any[] = [];
arrondissementPaylod: any = null;
communePaylod: any = null;
structurePaylod: any = null;
blocForm?: FormGroup;
isActionInProgress: boolean = false;
constructor(
private fb: FormBuilder,
private router: Router,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
private globalService: GlobalService
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
ngOnInit(): void {
this.globalService.setLodingSuccess(false);
this.dtOptions = {
pagingType: 'full_numbers',
pageLength: 10,
processing: true,
language: {
search: "Rechercher&nbsp;:",
emptyTable: "Aucune donnée disponible",
lengthMenu: "Afficher _MENU_ &eacute;l&eacute;ments",
info: "Affichage de l'&eacute;lement _START_ &agrave; _END_ sur _TOTAL_ &eacute;l&eacute;ments",
infoEmpty: "Affichage de l'&eacute;lement 0 &agrave; 0 sur 0 &eacute;l&eacute;ments",
infoFiltered: "(filtr&eacute; de _MAX_ &eacute;l&eacute;ments au total)",
paginate: {
first: "<i class='menu-icon mdi mdi-chevron-double-left'></i>",
previous: "<i class='menu-icon mdi mdi-chevron-left'></i>",
next: "<i class='menu-icon mdi mdi-chevron-right'></i>",
last: "<i class='menu-icon mdi mdi-chevron-double-right'></i>"
},
}
};
this.makeForm(null);
this.list();
}
ngAfterViewInit(): void {
this.dtTrigger.next(this.dtOptions);
}
compareFn = (o1: any, o2: any) => (o1 && o2 ? o1.id === o2.id : o1 === o2);
makeForm(bloc: Bloc | null): void {
this.blocForm = this.fb.group({
id: [bloc != null ? bloc.id : null],
cote: [bloc != null ? bloc.cote : null],
nom: [bloc != null ? bloc.nom : null,
[Validators.required]],
arrondissement: [bloc != null ? bloc.arrondissement : null],
quartier: [bloc != null ? bloc.quartier : null],
structure: [bloc != null ? bloc.structure : null],
secteur: [bloc != null ? bloc.secteur : null, [Validators.required]],
secteurDecoupage: [bloc != null ? bloc.secteurDecoupage : null],
quartiers: [bloc != null ? bloc.quartiers : []],
});
if (bloc != null) {
this.showHideForm(true);
}
}
resetForm(e: MouseEvent): void {
e.preventDefault();
this.blocForm?.reset();
for (const key in this.blocForm?.controls) {
this.blocForm?.controls[key].markAsPristine();
this.blocForm?.controls[key].updateValueAndValidity();
}
this.makeForm(null);
}
ngOnDestroy(): void {
this.dtTrigger.unsubscribe();
}
checkSecteurDecoupageBySecteur(value: any): void {
this.secteurDecoupageList = [];
if(value) {
this.secteurDecoupageList = value.secteurDecoupages;
}
}
checkSecteurDecoupage(value: any): void {
this.blocForm?.get('arrondissement')?.setValue(null);
this.blocForm?.get('quartier')?.setValue(null);
if(value) {
this.blocForm?.get('arrondissement')?.setValue(value.arrondissement);
this.blocForm?.get('quartier')?.setValue(value.quartier);
}
}
list(): void {
this.globalService.setLodingSuccess(true);
/*this.communeList = [];
this.arrondissementList = [];*/
this.structureList = [];
//const $quartiers = this.crudService.getAll('quartier/all');
//const $communes = this.crudService.getAll('commune/all');
//const $arrondissements = this.crudService.getAll('arrondissement/commune/58');
const $structures = this.crudService.getAll('structure/all');
forkJoin([$structures]).subscribe(([structures]) => {
// All data available
console.log(structures);
//console.log(communes);
//const dataCommune: any = communes;
const dataStructures: any = structures;
this.structureList = dataStructures.object;
//this.communeList = dataCommune.object;
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
console.log('OK');
this.globalService.setLodingSuccess(false);
});
}
/*filterArrondissementByCommune(value: any): void {
console.log('value ==> ', value)
this.communePaylod = value;
this.listQuartierByArrondissement(null);
this.arrondissementFilteredList = [];
if (this.communePaylod != null) {
this.arrondissementFilteredList = this.arrondissementList.filter((element) => element.commune != null && element.commune.id == this.communePaylod.id);
}
}
listQuartierByArrondissement(value: any): void {
this.arrondissementPaylod = value;
this.quartierList = [];
this.structureList = [];
this.blocList = [];
this.refreshDataTable();
if (this.arrondissementPaylod != null) {
this.globalService.setLodingSuccess(true);
const $quartiers = this.crudService.getAll('quartier/arrondissement/' + this.arrondissementPaylod?.id);
const $blocs = this.crudService.getAll('bloc/list-by-arrondissement?idArrondissement='+ this.arrondissementPaylod?.id);
const $structures = this.crudService.getAll('structure/all-by-arrondissement?arrondissementId='+ this.arrondissementPaylod?.id);
forkJoin([$quartiers, $blocs, $structures]).subscribe(([quartiers, blocs, structures]) => {
// All data available
console.log(quartiers);
console.log(blocs);
const dataQuartier: any = quartiers;
const dataBlocs: any = blocs;
const dataStructures: any = structures;
this.quartierList = dataQuartier.object;
this.blocList = dataBlocs.object;
this.structureList = dataStructures.object;
this.quartierList = [...this.quartierList];
this.refreshDataTable();
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
console.log('OK');
this.globalService.setLodingSuccess(false);
});
}
}*/
listBlocsAndSecteurByStructure(value: any): void {
this.structurePaylod = value;
console.log("value ===> ", value);
this.secteurList = [];
this.secteurDecoupageList = [];
this.blocList = [];
this.blocForm?.reset();
this.refreshDataTable();
if (this.structurePaylod != null) {
this.globalService.setLodingSuccess(true);
this.crudService.getAll('secteur/by-structure-id/' + this.structurePaylod?.id).subscribe(
(data: any) => {
this.secteurList = data != null ? data.object : [];
},
(error: HttpErrorResponse) => {
console.log('OK');
this.globalService.setLodingSuccess(false);
}
);
const $blocs = this.crudService.getAll('bloc/list-by-structure?idStructure='+ this.structurePaylod?.id);
forkJoin([$blocs]).subscribe(([blocs]) => {
// All data available
console.log(blocs);
const dataBlocs: any = blocs;
this.blocList = dataBlocs.object;
this.blocList = [...this.blocList];
this.refreshDataTable();
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
console.log('OK');
this.globalService.setLodingSuccess(false);
});
}
}
refreshDataTable(): void {
this.dtElement?.dtInstance.then((dtInstance: DataTables.Api) => {
// Destroy the table first
dtInstance.destroy();
// Call the dtTrigger to rerender again
this.dtTrigger.next(null);
});
}
showDeleteConfirm(element: any, index: number): void {
this.modal.confirm({
nzTitle: 'Confirmez-vous ?',
nzContent: 'suppression de <strong style="color: red;">' + element.nom + '</strong>',
nzOkText: 'Oui',
nzOkType: 'primary',
nzOkDanger: true,
nzOnOk: () => {
this.globalService.setLodingSuccess(true);
this.crudService.deleteElement('bloc/delete', element.id).subscribe(
(data: any) => {
this.quartierList.splice(index, 1);
this.refreshDataTable();
this.message.create('success', `Suppression effectuée avec succès.`);
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.globalService.setLodingSuccess(false);
}
);
},
nzCancelText: 'Non',
nzOnCancel: () => console.log('Cancel')
});
}
showHideForm(value: boolean): void {
this.isForm = value;
if (!value) {
this.makeForm(null);
}
}
saveForm(): void {
for (const i in this.blocForm?.controls) {
this.blocForm?.controls[i].markAsDirty();
this.blocForm?.controls[i].updateValueAndValidity();
}
if (this.blocForm?.valid) {
this.isActionInProgress = true;
const formData = this.blocForm?.value;
formData.structure = this.structurePaylod;
if (
formData.id == null ||
formData.id == undefined ||
formData.id == ''
) {
this.globalService.setLodingSuccess(true);
this.crudService.save('bloc/create', formData).subscribe(
(data: any) => {
this.blocList.unshift(data.object);
this.message.create('success', `Enregistrement effectué avec succès.`);
this.makeForm(null);
this.refreshDataTable();
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.globalService.setLodingSuccess(false);
}
);
} else {
const i = this.blocList.findIndex(
(element) => element.id == formData.id
);
this.globalService.setLodingSuccess(true);
this.crudService.update('bloc/update', formData).subscribe(
(data: any) => {
this.blocList.splice(i, 1);
this.blocList.unshift(data.object);
this.message.create('success', `Modification effectuée avec succès.`);
this.makeForm(null);
this.refreshDataTable();
this.showHideForm(false);
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.globalService.setLodingSuccess(false);
}
);
}
} else {
this.modal.error({
nzTitle: 'Erreur',
nzContent: 'Formulaire invalid. Un ou plusieurs champs sont vides...'
});
}
}
}

View File

@@ -0,0 +1,35 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DataTablesModule } from 'angular-datatables';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NzDividerModule } from 'ng-zorro-antd/divider';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { BlocQuartierRoutingModule } from './bloc-quartier-routing.module';
import { BlocQuartierComponent } from './bloc-quartier.component';
import { BlocByStructureComponent } from './bloc-by-structure/bloc-by-structure.component';
@NgModule({
declarations: [
BlocQuartierComponent,
BlocByStructureComponent
],
imports: [
CommonModule,
BlocQuartierRoutingModule,
FormsModule,
ReactiveFormsModule,
NzDividerModule,
NzButtonModule,
NzSelectModule,
DataTablesModule,
]
})
export class BlocQuartierModule { }

View File

@@ -0,0 +1,22 @@
<div class="row" [ngStyle]="{ backgroundColor: module ? module.color : '' }" id="formulaire">
<div class="col-md-5" style="padding: 35px;margin-bottom: 2%;">
<div style="margin-left: 16%;">
<h3 class="text-secondary"
style="font-size: 11px;text-transform: uppercase;color: rgba(255, 255, 255, 0.61) !important;">
Module Cartographie </h3>
<h1 class="text-white" style="font-size: 16px;line-height: 1.5;">
Dossier en cours sur le module cartographie</h1>
</div>
</div>
<div class="col-md-2">
</div>
<div class="col-md-5" style="padding-left: 3.1%;">
<img src="assets/sigibe/logo-mef-white.png" alt=""
style="width: 200px;margin-top: 4%;float: right;margin-right: 20%;">
</div>
</div>
<div style="margin-left: 6%;margin-right: 6%;background-color: rgb(255 255 255 / 75%);">
<router-outlet></router-outlet>
</div>

View File

@@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
@Component({
selector: 'app-cartographie-router',
templateUrl: './cartographie-router.component.html',
styleUrls: ['./cartographie-router.component.css']
})
export class CartographieRouterComponent {
module: any = null;
constructor(
private tokenStorage: TokenStorage,
private router: Router,
) {
}
ngOnInit(): void {
this.module = JSON.parse(this.tokenStorage.getModule());
}
}

View File

@@ -0,0 +1,58 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CartographieComponent } from './cartographie.component';
import { ChargerFichierComponent } from './charger-fichier/charger-fichier.component';
import { DetailByNupGeomComponent } from './detail-by-nup-geom/detail-by-nup-geom.component';
import { CartographieFiscaleTfuComponent } from './cartographie-fiscale-tfu/cartographie-fiscale-tfu.component';
import { SommaireCartographieComponent } from './sommaire-cartographie/sommaire-cartographie.component';
import { NotFoundComponent } from 'src/app/shared/not-found/not-found.component';
import { CartographieRouterComponent } from './cartographie-router/cartographie-router.component';
import { QuartierComponent } from '../reference/quartier/quartier.component';
import { ExerciceComponent } from '../reference/exercice/exercice.component';
import { ZoneRfuComponent } from '../reference/zone-rfu/zone-rfu.component';
import { DepartementComponent } from '../reference/departement/departement.component';
import { CommuneComponent } from '../reference/commune/commune.component';
import { ArrondissementComponent } from '../reference/arrondissement/arrondissement.component';
import { UsageComponent } from '../reference/usage/usage.component';
import { CategorieBatimentComponent } from '../reference/categorie-batiment/categorie-batiment.component';
import { DetailInformationParcelleComponent } from 'src/app/shared/detail-information-parcelle/detail-information-parcelle.component';
import { DetailInformationBatimentComponent } from 'src/app/shared/detail-information-batiment/detail-information-batiment.component';
import { DetailInformationUniteLogementComponent } from 'src/app/shared/detail-information-unite-logement/detail-information-unite-logement.component';
const routes: Routes = [
{
path: '', component: CartographieComponent,
children: [
{
path: 'data', component: CartographieRouterComponent,
children: [
{ path: 'reference/quartier', component: QuartierComponent },
{ path: 'reference/exercice', component: ExerciceComponent },
{ path: 'reference/zone-rfu', component: ZoneRfuComponent },
{ path: 'reference/departement', component: DepartementComponent },
{ path: 'reference/commune', component: CommuneComponent },
{ path: 'reference/arrondissement', component: ArrondissementComponent },
{ path: 'reference/usage', component: UsageComponent },
{ path: 'reference/categorie-batiment', component: CategorieBatimentComponent },
{ path: 'fiche-chargement-geojson', component: ChargerFichierComponent },
{ path: 'sommaire-cartographie', component: SommaireCartographieComponent },
{ path: 'detail-parcelle/:id', component: DetailInformationParcelleComponent },
{ path: 'detail-batiment/:id', component: DetailInformationBatimentComponent },
{ path: 'detail-unite-logement/:id', component: DetailInformationUniteLogementComponent },
{ path: '**', component: NotFoundComponent }
]
},
{ path: 'cartographie-fiscale-tfu', component: CartographieFiscaleTfuComponent },
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class CartographieRoutingModule { }

View File

@@ -0,0 +1,83 @@
.badge-tree-display {
float: right;
margin-right: 35px;
margin-top: 20px;
margin-bottom: -28px;
font-size: 8px;
}
.badge-tree-bloc {
float: right;
margin-right: 35px;
margin-top: 10px;
margin-bottom: 0px;
font-size: 8px;
}
.boutton-enquete-action-green {
cursor: pointer;
background: green;
padding: 7px;
border-radius: 50%;
color: white;
}
.boutton-enquete-action-secondary {
cursor: pointer;
background: sienna;
padding: 7px;
border-radius: 50%;
color: white;
}
.boutton-enquete-action-teal {
cursor: pointer;
background: darkblue;
padding: 7px;
border-radius: 50%;
color: white;
}
.map-legend {
position: absolute;
top: 10px;
right: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 12px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
z-index: 999999;
margin-top: 17%;
}
.map-legend ul {
list-style: none;
padding: 0;
margin: 0;
}
.legend-color {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 6px;
vertical-align: middle;
border: 1px solid #aaa;
}
dl, ol, ul {
font-size: 11px!important;
}
.color-parcelle-non-enquetee {
background-color: rgb(128,128,128);
}
.color-parcelle-enquetee-non-bati {
background-color: cyan;
}
.color-parcelle-enquetee-bati {
background-color: green;
}

View File

@@ -0,0 +1,75 @@
<div class="row" style="margin-top: 75px;">
<div class="col-md-12">
<ul nz-menu nzTheme="dark" nzMode="horizontal" class="navBar">
<li nz-menu-item style="margin-left: 6% !important;" class="text-center" (click)="goHome()">
<img src="assets/sigibe/home.svg" alt="" class="icon-home-header">
</li>
<li nz-menu-item nzSelected routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/cartographie/data/sommaire-cartographie"> Sommaire </a>
</li>
<li nz-menu-item nz-popover [(nzPopoverVisible)]="isVisibleReference"
(nzPopoverVisibleChange)="change($event, 1)" nzPopoverPlacement="bottom"
[nzPopoverContent]="contentTemplateMenuReference">
Références <svg data-icon="chevron-down" height="13" role="img" viewBox="0 0 16 16" width="13">
<path
d="M12 5c-.28 0-.53.11-.71.29L8 8.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42l4 4c.18.18.43.29.71.29s.53-.11.71-.29l4-4A1.003 1.003 0 0012 5z"
fill-rule="evenodd"></path>
</svg>
</li>
<li nz-menu-item nzSelected routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/cartographie/data/fiche-chargement-geojson"> Fiche de chargement des fichiers GeoJSON </a>
</li>
<li nz-menu-item routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/cartographie/cartographie-fiscale-tfu"> Catographie sémantique des immeubles et taxes </a>
</li>
</ul>
</div>
</div>
<ng-template #contentTemplateMenuReference>
<nz-list>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/cartographie/data/reference/deparrtement"> <i class="mdi mdi-arrow-right"> </i> Les départements
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/cartographie/data/reference/commune"> <i class="mdi mdi-arrow-right"> </i> Les communes
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/cartographie/data/reference/arrondissement"> <i class="mdi mdi-arrow-right"> </i> Les arrondissements
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/cartographie/data/reference/quartier"> <i class="mdi mdi-arrow-right"> </i> Les quartiers
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/cartographie/data/reference/exercice"> <i class="mdi mdi-arrow-right"> </i> Les exercices
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/cartographie/data/reference/zone-rfu"> <i class="mdi mdi-arrow-right"> </i> Les zones RFU
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/cartographie/data/reference/categorie-batiment"> <i class="mdi mdi-arrow-right"> </i> Les
catégories de bâtiment
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/cartographie/data/reference/usage"> <i class="mdi mdi-arrow-right"> </i>
Les usages des immeubles
</a>
</nz-list-item>
</nz-list>
</ng-template>
<router-outlet></router-outlet>

View File

@@ -0,0 +1,56 @@
import { Component, OnInit, ViewContainerRef } from '@angular/core';
import { Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
/* fin déclaration pour outils de mesure */
@Component({
selector: 'app-cartographie',
templateUrl: './cartographie.component.html',
styleUrls: [
'./cartographie.component.css'
]
})
export class CartographieComponent {
user: any = null;
isVisibleReference = false;
isVisibleSecteur = false;
isVisibleEquipe = false;
menuNum = 0;
module: any = null;
constructor(
private tokenStorage: TokenStorage,
private router: Router,
) {
}
ngOnInit(): void {
this.module = JSON.parse(this.tokenStorage.getModule());
console.log(JSON.parse(this.tokenStorage.getModule()));
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
console.log(this.user);
}
change(value: any, menuNum: number): void {
if (menuNum == 1)
this.isVisibleReference = value;
if (menuNum == 2)
this.isVisibleSecteur = value;
if (menuNum == 3)
this.isVisibleEquipe = value;
}
goHome(): void {
this.tokenStorage.saveModule(null);
this.router.navigate(['/principale']);
}
}

View File

@@ -0,0 +1,44 @@
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CartographieRoutingModule } from './cartographie-routing.module';
import { CartographieComponent } from './cartographie.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ChargerFichierComponent } from './charger-fichier/charger-fichier.component';
import { DetailByNupGeomComponent } from './detail-by-nup-geom/detail-by-nup-geom.component';
import { DataTablesModule } from 'angular-datatables';
import { SharedModule } from 'src/app/shared/shared.module';
import { SommaireCartographieComponent } from './sommaire-cartographie/sommaire-cartographie.component';
import { CartographieFiscaleTfuComponent } from './cartographie-fiscale-tfu/cartographie-fiscale-tfu.component';
import { CartographieRouterComponent } from './cartographie-router/cartographie-router.component';
import { ReferenceModule } from '../reference/reference.module';
import { NgApexchartsModule } from 'ng-apexcharts';
@NgModule({
declarations: [
CartographieComponent,
ChargerFichierComponent,
DetailByNupGeomComponent,
SommaireCartographieComponent,
CartographieFiscaleTfuComponent,
CartographieRouterComponent
],
imports: [
CommonModule,
CartographieRoutingModule,
DataTablesModule,
FormsModule,
ReactiveFormsModule,
SharedModule,
ReferenceModule,
NgApexchartsModule,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class CartographieModule { }

View File

@@ -0,0 +1,105 @@
<div class="row">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review">
<h6 class="card-title" style="font-size: 16px!important;text-transform: none;"> Fiche chargement de
fichiers GeoJSON </h6>
<nz-divider></nz-divider>
<form [formGroup]="uploadForm" *ngIf="uploadForm">
<div class="row">
<div class="col-lg-6">
<div class="did-floating-label-content mt-3">
<input class="did-floating-input" type="text" id="reference" formControlName="reference"
placeholder="Entrez la référence">
<label class="did-floating-label"> Référence du chargement <span class="text-danger">
*</span> </label>
</div>
</div>
<div class="col-lg-6">
<div class="did-floating-label-content mt-3">
<input class="did-floating-input" style="padding-top: 10px;"
(change)="handleFileInput($event)" type="file" accept=".geojson"
placeholder="Entrez la référence" #fileInput>
<label class="did-floating-label"> Fichier à charger <span class="text-danger"> *</span>
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="did-floating-label-content mt-3">
<input class="did-floating-input" type="text" id="description"
formControlName="description" placeholder="Entrez la description">
<label class="did-floating-label"> Entrez une description <span class="text-danger">
*</span> </label>
</div>
</div>
</div>
<br>
<button class="btn btn-review btn-secondary btn-fw mr-2" (click)="resetForm()"
[disabled]="isActionInProgress">
Fermer
</button>
<button class="btn btn-review btn-primary btn-fw" (click)="saveForm()"
[disabled]="isActionInProgress">
{{ isActionInProgress == false ? 'Enregistrer' : 'Opération en cours ...' }}
</button>
</form>
</div>
</div>
</div>
</div>
<div class="row" [ngClass]="isActionInProgress ? 'hidden-for-loading': 'visible-for-loading'">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4 class="card-title" style="font-size: 16px!important;margin-bottom: 0px;text-transform: unset;">
Liste des fichiers GeoJSON chargés </h4>
<p class="card-description text-gray" style="margin-bottom: 2%;line-height: 30px;font-size: 12px;">
Liste des différents fichiers GeoJSON chargés pour l'affichage cartographique
</p>
<div class="table-responsive">
<table class="table table-striped" datatable [dtOptions]="dtOptions" [dtTrigger]="dtTrigger">
<thead>
<tr>
<th>
Référence
</th>
<th>
Description
</th>
<th class="text-center">
Actions
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let todo of fichierList; let i=index" class="border-bottom-light">
<td>
{{ todo.reference }}
</td>
<td>
{{ todo.description }}
</td>
<td class="text-center">
<button type="button" class="btn btn-icons btn-rounded btn-success mr-1">
<i class="mdi mdi-pencil btn-icon-modify"></i>
</button>
<button type="button" class="btn btn-icons btn-rounded btn-danger">
<i class="mdi mdi-delete btn-icon-modify"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,206 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { CrudService } from 'src/app/crud.service';
import { GlobalService } from 'src/app/global.service';
import { DataTableDirective } from 'angular-datatables';
import { Subject } from 'rxjs';
@Component({
selector: 'app-charger-fichier',
templateUrl: './charger-fichier.component.html',
styleUrls: ['./charger-fichier.component.css']
})
export class ChargerFichierComponent implements OnInit {
fileToUpload: File | null = null;
uploadForm?: FormGroup;
@ViewChild('fileInput', { static: false }) fileInput!: ElementRef;
isActionInProgress: boolean = false;
@ViewChild(DataTableDirective, { static: false })
dtElement?: DataTableDirective;
dtOptions: DataTables.Settings = {};
dtTrigger: Subject<any> = new Subject<any>();
isForm = false;
fichierList: any[] = [];
constructor(
private fb: FormBuilder,
private router: Router,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
private globalService: GlobalService
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
ngOnInit(): void {
this.globalService.setLodingSuccess(false);
this.dtOptions = {
pagingType: 'full_numbers',
pageLength: 10,
processing: true,
language: {
search: "Rechercher&nbsp;:",
emptyTable: "Aucune donnée disponible",
lengthMenu: "Afficher _MENU_ &eacute;l&eacute;ments",
info: "Affichage de l'&eacute;lement _START_ &agrave; _END_ sur _TOTAL_ &eacute;l&eacute;ments",
infoEmpty: "Affichage de l'&eacute;lement 0 &agrave; 0 sur 0 &eacute;l&eacute;ments",
infoFiltered: "(filtr&eacute; de _MAX_ &eacute;l&eacute;ments au total)",
paginate: {
first: "<i class='menu-icon mdi mdi-chevron-double-left'></i>",
previous: "<i class='menu-icon mdi mdi-chevron-left'></i>",
next: "<i class='menu-icon mdi mdi-chevron-right'></i>",
last: "<i class='menu-icon mdi mdi-chevron-double-right'></i>"
},
}
};
this.makeForm();
}
makeForm(): void {
this.uploadForm = this.fb.group({
id: [null],
reference: [null, [Validators.required]],
description: [null],
});
}
list(): void {
this.globalService.setLodingSuccess(true);
this.fichierList = [];
this.crudService.getAll('parcelle-geom/geojsonfile-all').subscribe(
(data: any) => {
this.fichierList = data != null ? data.object : [];
this.fichierList = [...this.fichierList];
this.refreshDataTable();
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
console.log('OK');
this.globalService.setLodingSuccess(false);
}
);
}
refreshDataTable(): void {
this.dtElement?.dtInstance.then((dtInstance: DataTables.Api) => {
// Destroy the table first
dtInstance.destroy();
// Call the dtTrigger to rerender again
this.dtTrigger.next(null);
});
}
ngAfterViewInit(): void {
this.dtTrigger.next(this.dtOptions);
}
ngOnDestroy(): void {
this.dtTrigger.unsubscribe();
}
saveForm(): void {
for (const i in this.uploadForm?.controls) {
this.uploadForm?.controls[i].markAsDirty();
this.uploadForm?.controls[i].updateValueAndValidity();
}
if (this.uploadForm?.valid && this.fileToUpload != null) {
this.isActionInProgress = true;
const formData = this.uploadForm?.value;
this.globalService.setLodingSuccess(true);
this.crudService.saveFile(formData, this.fileToUpload).subscribe(
(data: any) => {
this.globalService.setLodingSuccess(false);
if (data.success == true) {
this.message.create('success', `Chargement effectué avec succès.`);
this.modal.success({
nzTitle: 'Succèss',
nzContent: data.message,
nzOnOk: () => {
this.resetForm();
}
});
}
else {
this.message.create('error', `Chargement du fichier erroné`);
this.modal.error({
nzTitle: 'Erreur',
nzContent: data.message
});
}
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.globalService.setLodingSuccess(false);
}
);
} else {
this.modal.error({
nzTitle: 'Erreur',
nzContent: 'Formulaire invalid. Un ou plusieurs champs sont vides...'
});
}
}
resetForm(): void {
this.fileInput.nativeElement.value = '';
this.uploadForm?.reset();
}
handleFileInput(event: any) {
//1. limiter la taille à 2Mo et contrôler l'extension si c'est du geojson
const fileSizeMB = event.target.files[0]?.size / (1024 * 1024);
if (fileSizeMB > 2) {
this.modal.error({
nzTitle: 'Erreur',
nzContent: 'Fichier trop volumineux. La taille maximale autorisée est de 2 Mo.',
nzOnOk: () => {
this.fileToUpload = null;
this.fileInput.nativeElement.value = '';
}
});
}
// 2. Vérification du type de fichier (extension/MIME)
if (!event.target.files[0]?.type.includes('geojson') && !event.target.files[0]?.type.includes('geo+json')) {
this.modal.error({
nzTitle: 'Erreur',
nzContent: 'Format de fichier erroné. Le fichier doit être de type geojson.',
nzOnOk: () => {
this.fileToUpload = null;
this.fileInput.nativeElement.value = '';
}
});
}
this.fileToUpload = event.target.files[0];
console.log(this.fileToUpload?.name);
}
}

View File

@@ -0,0 +1,19 @@
<div class="row mt-3">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4 class="card-title" style="font-size: 18px!important;font-weight: 900; text-transform: unset;"> DÉTAIL DES INFORMATIONS SUR LA PARCELLE </h4>
<nz-divider></nz-divider>
<app-detail-enquete-information
[acteurConcernesList]="acteurConcernesList"
[enquete]="enquete"
[parcelle]="parcelle">
</app-detail-enquete-information>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,94 @@
import { HttpClient } from '@angular/common/http';
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { firstValueFrom } from 'rxjs';
import { CrudService } from 'src/app/crud.service';
import { GlobalService } from 'src/app/global.service';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
@Component({
selector: 'app-detail-by-nup-geom',
templateUrl: './detail-by-nup-geom.component.html',
styleUrls: ['./detail-by-nup-geom.component.css']
})
export class DetailByNupGeomComponent implements OnInit {
acteurConcernesList: any[] = [];
enquete: any = null;
parcelle: any = null;
isActionInProgress: boolean = false;
user: any = null;
nup: string = '';
constructor(
private fb: FormBuilder,
private router: Router,
private route: ActivatedRoute,
private modal: NzModalService,
private message: NzMessageService,
private globalService: GlobalService,
private http: HttpClient,
private crudService: CrudService,
private tokenStorage: TokenStorage,
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
async ngOnInit(): Promise<void> {
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
console.log(this.user);
this.nup = this.route.snapshot.params["nup"];
console.log('this.nup', this.nup);
if (this.nup != undefined && this.nup != '') {
this.globalService.setLodingSuccess(true);
const result: any = await firstValueFrom(this.crudService.getAll('enquete/fiche/nup-provisoir/' + this.nup));
console.log('enquete ===> ', result);
if (result && result.object != null) {
this.enquete = result.object.enquete;
this.parcelle = result.object.enquete?.parcelle;
this.acteurConcernesList = result.object.acteurConcernes ? result.object.acteurConcernes : [];
if (this.acteurConcernesList && this.acteurConcernesList.length > 0) {
for (let i = 0; i < this.acteurConcernesList.length; i++) {
this.acteurConcernesList[i].active = false;
if (this.acteurConcernesList[i].pieces && this.acteurConcernesList[i].pieces.length > 0) {
for (let j = 0; j < this.acteurConcernesList[i].pieces.length; j++) {
this.acteurConcernesList[i].pieces[j].active = false;
for (let k = 0; k < this.acteurConcernesList[i].pieces[j].uploads.length; k++) {
this.acteurConcernesList[i].pieces[j].uploads[k].active = false;
}
}
}
}
}
}
this.globalService.setLodingSuccess(false);
}
}
}

View File

@@ -0,0 +1,531 @@
/* ══════════════════════════════════════════════════════════════
SOMMAIRE CARTOGRAPHIE — styles consolidés
ViewEncapsulation.None requis
══════════════════════════════════════════════════════════════ */
/* ── Reset nz-card body ── */
.kpi-card .ant-card-body,
.stat-banner-card .ant-card-body,
.chart-card .ant-card-body,
.stats-table-card .ant-card-body,
.alert-card .ant-card-body {
padding: 0 !important;
}
/* ══════════════════════════════════════════════════════════════
DASHBOARD CONTAINER
══════════════════════════════════════════════════════════════ */
.dashboard-container {
padding: 24px;
width: 100%;
min-height: 100vh;
}
/* ══════════════════════════════════════════════════════════════
KPI CARDS
══════════════════════════════════════════════════════════════ */
.kpi-cards-section {
margin-bottom: 32px;
}
.kpi-card {
border-radius: 12px;
border: none;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin-bottom: 16px;
}
.kpi-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(26, 88, 144, 0.18);
}
.kpi-content {
display: flex;
align-items: center;
padding: 20px 24px;
gap: 18px;
position: relative;
overflow: hidden;
}
.kpi-content::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 4px;
}
.kpi-icon {
width: 56px; height: 56px;
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
background: transparent;
}
.kpi-icon [nz-icon],
.kpi-icon span[nz-icon] {
font-size: 30px;
}
.kpi-details { flex: 1; }
.kpi-value {
font-size: 34px;
font-weight: 700;
line-height: 1;
margin-bottom: 6px;
}
.kpi-label {
font-size: 10px;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.6px;
}
/* Variantes couleur — barre top + icône + valeur */
.kpi-card-blue .kpi-content::before { background: linear-gradient(90deg, #1a5890, #0e3660); }
.kpi-card-blue .kpi-icon [nz-icon] { color: #1a5890; }
.kpi-card-blue .kpi-value { color: #1a5890; }
.kpi-card-green .kpi-content::before { background: linear-gradient(90deg, #10b981, #059669); }
.kpi-card-green .kpi-icon [nz-icon] { color: #10b981; }
.kpi-card-green .kpi-value { color: #10b981; }
.kpi-card-purple .kpi-content::before { background: linear-gradient(90deg, #1a5890, #6d28d9); }
.kpi-card-purple .kpi-icon [nz-icon] { color: #1a5890; }
.kpi-card-purple .kpi-value { color: #1a5890; }
.kpi-card-red .kpi-content::before { background: linear-gradient(90deg, #ef4444, #dc2626); }
.kpi-card-red .kpi-icon [nz-icon] { color: #ef4444; }
.kpi-card-red .kpi-value { color: #ef4444; }
/* ══════════════════════════════════════════════════════════════
STAT BANNER
══════════════════════════════════════════════════════════════ */
.stat-banner-card {
border-radius: 12px;
border: none;
box-shadow: 0 2px 12px rgba(26, 88, 144, 0.10);
overflow: hidden;
}
.stat-banner-container {
display: flex;
align-items: stretch;
min-height: 110px;
}
.stat-banner-item {
flex: 1;
display: flex;
align-items: center;
gap: 16px;
padding: 18px 20px;
transition: filter 0.2s ease;
margin-right: 3px;
}
.stat-banner-item:hover { filter: brightness(0.96); }
.stat-banner-green { background: linear-gradient(135deg, #f0fdf4, #dcfce7); }
.stat-banner-blue { background: linear-gradient(135deg, #e8f1fb, #cce0f5); }
.stat-banner-purple { background: linear-gradient(135deg, #eef2fb, #d6e4f5); }
.stat-banner-orange { background: linear-gradient(135deg, #fffbeb, #fef3c7); }
.stat-banner-icon {
font-size: 28px;
flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
width: 48px; height: 48px;
border-radius: 10px;
}
.stat-banner-green .stat-banner-icon { color: #10b981; background: rgba(16, 185, 129, 0.12); }
.stat-banner-blue .stat-banner-icon { color: #1a5890; background: rgba(26, 88, 144, 0.12); }
.stat-banner-purple .stat-banner-icon { color: #1a5890; background: rgba(26, 88, 144, 0.10); }
.stat-banner-orange .stat-banner-icon { color: #f59e0b; background: rgba(245, 158, 11, 0.12); }
.stat-banner-body {
flex: 1;
display: flex; flex-direction: column; gap: 3px;
}
.stat-banner-value {
font-size: 26px;
font-weight: 700;
line-height: 1;
color: #111827;
}
.stat-banner-unit {
font-size: 15px;
font-weight: 600;
color: #6b7280;
margin-left: 2px;
}
.stat-banner-label {
font-size: 11px;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-banner-bar {
width: 100%; height: 5px;
background: rgba(0,0,0,0.07);
border-radius: 999px;
overflow: hidden;
margin-top: 4px;
}
.stat-banner-bar-fill {
height: 100%;
border-radius: 999px;
transition: width 0.9s cubic-bezier(0.4, 0, 0.2, 1);
}
.stat-banner-bar-fill.green { background: #10b981; }
.stat-banner-bar-fill.blue { background: #1a5890; }
.stat-banner-bar-fill.purple { background: #1a5890; }
.stat-banner-bar-fill.orange { background: #f59e0b; }
.stat-banner-percent {
font-size: 11px;
color: #9ca3af;
font-weight: 500;
}
.stat-banner-divider {
width: 1px;
background: rgba(0,0,0,0.07);
margin: 12px 0;
flex-shrink: 0;
}
/* ══════════════════════════════════════════════════════════════
ONGLETS THÉMATIQUES
══════════════════════════════════════════════════════════════ */
.thematique-tabs-section {
margin-top: 28px;
}
.thematique-tabs {
display: flex;
gap: 4px;
flex-wrap: wrap;
margin-bottom: 24px;
border-bottom: 2px solid #e0ecf8;
padding-bottom: 0;
}
.ttab {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 10px 18px;
font-size: 13px;
font-weight: 500;
color: #6b7280;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px 6px 0 0;
line-height: 1;
}
.ttab:hover {
color: #1a5890;
background: #e8f1fb;
}
.ttab-active {
color: #1a5890 !important;
border-bottom-color: #1a5890 !important;
background: #e8f1fb !important;
font-weight: 700;
}
.thematique-content {
animation: fadeInUp 0.3s ease-out;
}
/* ══════════════════════════════════════════════════════════════
CHART CARDS
══════════════════════════════════════════════════════════════ */
.chart-card {
border-radius: 12px;
border: none;
box-shadow: 0 2px 12px rgba(26, 88, 144, 0.08);
height: 100%;
margin-bottom: 16px;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 2px solid #e0ecf8;
}
.chart-title {
font-size: 14px;
font-weight: 700;
color: #1a5890;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.chart-title [nz-icon] {
color: #1a5890;
font-size: 18px;
}
.chart-content {
padding: 16px 20px;
min-height: 360px;
}
.chart-footer {
padding: 16px 20px;
background: #f4f8fd;
border-top: 1px solid #e0ecf8;
}
/* ── Legend custom ── */
.chart-legend-custom {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 10px;
}
.legend-item {
display: flex; align-items: center; gap: 8px; font-size: 13px;
}
.legend-color {
width: 11px; height: 11px;
border-radius: 50%; flex-shrink: 0;
}
.legend-text { flex: 1; color: #6b7280; font-weight: 500; }
.legend-value { color: #1a5890; font-weight: 700; }
/* ── Summary ── */
.chart-summary {
display: flex;
justify-content: space-around;
gap: 16px;
}
.summary-item {
display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.summary-label {
font-size: 11px; color: #6b7280; font-weight: 500;
text-transform: uppercase; letter-spacing: 0.5px;
}
.summary-value {
font-size: 22px; font-weight: 700; color: #1a5890;
}
.summary-value.active { color: #10b981; }
.summary-value.inactive { color: #ef4444; }
/* ══════════════════════════════════════════════════════════════
LÉGENDE STATUTS
══════════════════════════════════════════════════════════════ */
.statut-legend-list {
padding: 14px 18px;
display: flex; flex-direction: column; gap: 13px;
}
.statut-legend-item {
display: flex; align-items: flex-start; gap: 10px;
}
.statut-legend-dot {
width: 13px; height: 13px;
border-radius: 50%; flex-shrink: 0; margin-top: 3px;
}
.statut-legend-body {
flex: 1; display: flex; flex-direction: column; gap: 4px;
}
.statut-legend-label {
font-size: 12px; font-weight: 600; color: #374151;
}
.statut-legend-bar-wrap {
display: flex; align-items: center; gap: 8px;
}
.statut-legend-bar {
flex: 1; height: 5px;
background: #e0ecf8; border-radius: 999px; overflow: hidden;
}
.statut-legend-bar-fill {
height: 100%; border-radius: 999px;
transition: width 0.9s cubic-bezier(0.4, 0, 0.2, 1);
}
.statut-legend-val {
font-size: 12px; font-weight: 700; color: #1a5890; white-space: nowrap;
}
.statut-legend-val small {
font-size: 11px; color: #9ca3af; font-weight: 400;
}
/* ══════════════════════════════════════════════════════════════
STATS TABLE CARD
══════════════════════════════════════════════════════════════ */
.stats-table-card {
border-radius: 12px;
border: none;
box-shadow: 0 2px 12px rgba(26, 88, 144, 0.08);
}
.table-header {
display: flex; justify-content: space-between; align-items: center;
padding: 16px 20px;
border-bottom: 2px solid #e0ecf8;
}
.table-title {
font-size: 14px; font-weight: 700; color: #1a5890;
margin: 0; display: flex; align-items: center; gap: 8px;
}
.table-title [nz-icon] { color: #1a5890; font-size: 18px; }
.fonction-name { font-weight: 600; color: #1a5890; }
.ant-table { font-size: 13px; }
.ant-table thead > tr > th {
background: #e8f1fb !important;
font-weight: 700 !important;
color: #1a5890 !important;
border-bottom: 2px solid #c5d9ef !important;
}
.ant-table tbody > tr:hover > td { background: #f4f8fd !important; }
.ant-table tbody > tr > td { padding: 14px 16px !important; }
/* ══════════════════════════════════════════════════════════════
ALERT CARDS
══════════════════════════════════════════════════════════════ */
.alert-card {
border-radius: 12px; border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
margin-bottom: 16px;
}
.alert-content {
display: flex; align-items: flex-start;
gap: 16px; padding: 20px; border-radius: 10px;
}
.alert-icon { font-size: 30px; flex-shrink: 0; margin-top: 2px; }
.alert-body { flex: 1; }
.alert-title {
font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px;
}
.alert-value {
font-size: 26px; font-weight: 700; line-height: 1.1; margin-bottom: 4px;
}
.alert-desc { font-size: 12px; opacity: 0.72; }
/* Warning */
.alert-warning { background: linear-gradient(135deg, #fffbeb, #fef3c7); }
.alert-warning .alert-icon { color: #f59e0b; }
.alert-warning .alert-title { color: #92400e; }
.alert-warning .alert-value { color: #d97706; }
.alert-warning .alert-desc { color: #78350f; }
/* Danger */
.alert-danger { background: linear-gradient(135deg, #fff1f2, #ffe4e6); }
.alert-danger .alert-icon { color: #ef4444; }
.alert-danger .alert-title { color: #991b1b; }
.alert-danger .alert-value { color: #dc2626; }
.alert-danger .alert-desc { color: #7f1d1d; }
/* Success */
.alert-success { background: linear-gradient(135deg, #f0fdf4, #dcfce7); }
.alert-success .alert-icon { color: #10b981; }
.alert-success .alert-title { color: #14532d; }
.alert-success .alert-value { color: #16a34a; }
.alert-success .alert-desc { color: #15803d; }
/* Info — teinte bleue */
.alert-info { background: linear-gradient(135deg, #e8f1fb, #cce0f5); }
.alert-info .alert-icon { color: #1a5890; }
.alert-info .alert-title { color: #0e3660; }
.alert-info .alert-value { color: #1a5890; }
.alert-info .alert-desc { color: #2563eb; }
/* ══════════════════════════════════════════════════════════════
DIVERS
══════════════════════════════════════════════════════════════ */
.statut-dot-table {
display: inline-block;
width: 14px; height: 14px; border-radius: 50%;
}
/* ══════════════════════════════════════════════════════════════
ANIMATIONS
══════════════════════════════════════════════════════════════ */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.kpi-card { animation: fadeInUp 0.45s ease-out both; }
.chart-card { animation: fadeInUp 0.45s ease-out both; }
.stats-table-card{ animation: fadeInUp 0.45s ease-out both; }
.kpi-card:nth-child(1) { animation-delay: 0.05s; }
.kpi-card:nth-child(2) { animation-delay: 0.12s; }
.kpi-card:nth-child(3) { animation-delay: 0.19s; }
.kpi-card:nth-child(4) { animation-delay: 0.26s; }
/* ══════════════════════════════════════════════════════════════
RESPONSIVE
══════════════════════════════════════════════════════════════ */
@media (max-width: 992px) {
.stat-banner-container { flex-wrap: wrap; }
.stat-banner-item { flex: 0 0 50%; min-width: 0; }
.stat-banner-divider { display: none; }
}
@media (max-width: 768px) {
.dashboard-container { padding: 12px; }
.stat-banner-container { flex-direction: column; }
.stat-banner-item { flex: 1 1 100%; }
.thematique-tabs { gap: 2px; }
.ttab { padding: 8px 10px; font-size: 12px; }
.kpi-content { padding: 14px 16px; }
.kpi-value { font-size: 26px; }
}

View File

@@ -0,0 +1,489 @@
<div class="row">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review">
<div class="dashboard-container">
<!-- ── KPIs principaux ── -->
<div class="kpi-cards-section">
<div class="row">
<div class="col-lg-3 col-md-6">
<nz-card class="kpi-card kpi-card-blue" [nzLoading]="loading">
<div class="kpi-content">
<div class="kpi-icon"><span nz-icon nzType="global" nzTheme="outline"></span></div>
<div class="kpi-details">
<div class="kpi-value">{{ statistiquesGlobales.totalParcelles | number:'1.0-0': 'fr' }}</div>
<div class="kpi-label">Total Parcelles</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-3 col-md-6">
<nz-card class="kpi-card kpi-card-green" [nzLoading]="loading">
<div class="kpi-content">
<div class="kpi-icon"><span nz-icon nzType="check-circle" nzTheme="outline"></span></div>
<div class="kpi-details">
<div class="kpi-value">{{ statistiquesGlobales.parcellesEnquetees | number:'1.0-0': 'fr' }}</div>
<div class="kpi-label">Parcelles Enquêtées</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-3 col-md-6">
<nz-card class="kpi-card kpi-card-purple" [nzLoading]="loading">
<div class="kpi-content">
<div class="kpi-icon"><span nz-icon nzType="environment" nzTheme="outline"></span></div>
<div class="kpi-details">
<div class="kpi-value">{{ statistiquesGlobales.parcellesGeoreferencees | number:'1.0-0': 'fr' }}</div>
<div class="kpi-label">Géoréférencées</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-3 col-md-6">
<nz-card class="kpi-card kpi-card-red" [nzLoading]="loading">
<div class="kpi-content">
<div class="kpi-icon"><span nz-icon nzType="warning" nzTheme="outline"></span></div>
<div class="kpi-details">
<div class="kpi-value">{{ statistiquesGlobales.parcellesAvecDonneesNonGeo | number:'1.0-0': 'fr' }}</div>
<div class="kpi-label">Données sans géom.</div>
</div>
</div>
</nz-card>
</div>
</div>
<!-- ── Bandeaux secondaires ── -->
<div class="row mt-2">
<div class="col-lg-12">
<nz-card class="stat-banner-card" [nzLoading]="loading">
<div class="stat-banner-container">
<div class="stat-banner-item stat-banner-green">
<div class="stat-banner-icon"><span nz-icon nzType="check-circle" nzTheme="fill"></span></div>
<div class="stat-banner-body">
<div class="stat-banner-value">{{ getTauxEnquete() }}<span class="stat-banner-unit">%</span></div>
<div class="stat-banner-label">Taux d'enquête</div>
<div class="stat-banner-bar"><div class="stat-banner-bar-fill green" [style.width]="getTauxEnquete() + '%'"></div></div>
<div class="stat-banner-percent">{{ statistiquesGlobales.parcellesEnquetees }} / {{ statistiquesGlobales.totalParcelles }}</div>
</div>
</div>
<div class="stat-banner-divider"></div>
<div class="stat-banner-item stat-banner-blue">
<div class="stat-banner-icon"><span nz-icon nzType="home" nzTheme="fill"></span></div>
<div class="stat-banner-body">
<div class="stat-banner-value">{{ getTauxBati() }}<span class="stat-banner-unit">%</span></div>
<div class="stat-banner-label">Taux de bâti</div>
<div class="stat-banner-bar"><div class="stat-banner-bar-fill blue" [style.width]="getTauxBati() + '%'"></div></div>
<div class="stat-banner-percent">{{ statistiquesGlobales.parcellesBaties }} bâties / {{ statistiquesGlobales.parcellesNonBaties }} non bâties</div>
</div>
</div>
<div class="stat-banner-divider"></div>
<div class="stat-banner-item stat-banner-purple">
<div class="stat-banner-icon"><span nz-icon nzType="environment" nzTheme="fill"></span></div>
<div class="stat-banner-body">
<div class="stat-banner-value">{{ getTauxGeo() }}<span class="stat-banner-unit">%</span></div>
<div class="stat-banner-label">Taux de géoréférencement</div>
<div class="stat-banner-bar"><div class="stat-banner-bar-fill purple" [style.width]="getTauxGeo() + '%'"></div></div>
<div class="stat-banner-percent">{{ statistiquesGlobales.parcellesGeoreferencees }} géoréf. / {{ statistiquesGlobales.parcellesNonGeoreferencees }} sans géom.</div>
</div>
</div>
<div class="stat-banner-divider"></div>
<div class="stat-banner-item stat-banner-orange">
<div class="stat-banner-icon"><span nz-icon nzType="rise" nzTheme="outline"></span></div>
<div class="stat-banner-body">
<div class="stat-banner-value">{{ getTauxAJour() }}<span class="stat-banner-unit">%</span></div>
<div class="stat-banner-label">Taux à jour fiscal</div>
<div class="stat-banner-bar"><div class="stat-banner-bar-fill orange" [style.width]="getTauxAJour() + '%'"></div></div>
<div class="stat-banner-percent">{{ statistiquesGlobales.parcellesEndettees }} endettées</div>
</div>
</div>
</div>
</nz-card>
</div>
</div>
</div>
<!-- ── Onglets thématiques ── -->
<div class="thematique-tabs-section">
<div class="thematique-tabs">
<button class="ttab" [class.ttab-active]="activeThematique === 'statuts'" (click)="activeThematique = 'statuts'">
<span nz-icon nzType="pie-chart" nzTheme="outline"></span> Statuts des parcelles
</button>
<button class="ttab" [class.ttab-active]="activeThematique === 'enquete'" (click)="activeThematique = 'enquete'">
<span nz-icon nzType="audit" nzTheme="outline"></span> Enquête par territoire
</button>
<button class="ttab" [class.ttab-active]="activeThematique === 'georef'" (click)="activeThematique = 'georef'">
<span nz-icon nzType="environment" nzTheme="outline"></span> Géoréférencement
</button>
<button class="ttab" [class.ttab-active]="activeThematique === 'fiscal'" (click)="activeThematique = 'fiscal'">
<span nz-icon nzType="dollar-circle" nzTheme="outline"></span> Situation fiscale
</button>
</div>
<!-- ── THÉMATIQUE : Statuts ── -->
<div *ngIf="activeThematique === 'statuts'" class="thematique-content">
<div class="row">
<!-- Légende statuts -->
<div class="col-lg-4">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="unordered-list"></span> Légende des statuts</h3>
</div>
<div class="statut-legend-list">
<div class="statut-legend-item" *ngFor="let s of statsParStatut">
<div class="statut-legend-dot" [style.background]="s.couleur"></div>
<div class="statut-legend-body">
<span class="statut-legend-label">{{ s.libelle }}</span>
<div class="statut-legend-bar-wrap">
<div class="statut-legend-bar">
<div class="statut-legend-bar-fill" [style.width]="s.pourcentage + '%'" [style.background]="s.couleur"></div>
</div>
<span class="statut-legend-val">{{ s.nombre | number:'1.0-0': 'fr' }} <small>({{ s.pourcentage }}%)</small></span>
</div>
</div>
</div>
</div>
</nz-card>
</div>
<!-- Donut statuts -->
<div class="col-lg-4">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="pie-chart"></span> Répartition par statut</h3>
</div>
<div class="chart-content">
<apx-chart
[series]="pieStatutsOptions.series"
[chart]="pieStatutsOptions.chart"
[labels]="pieStatutsOptions.labels"
[colors]="pieStatutsOptions.colors"
[legend]="pieStatutsOptions.legend"
[plotOptions]="pieStatutsOptions.plotOptions"
[dataLabels]="pieStatutsOptions.dataLabels"
[responsive]="pieStatutsOptions.responsive">
</apx-chart>
</div>
</nz-card>
</div>
<!-- Pie bâties -->
<div class="col-lg-4">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="home"></span> Bâties vs Non bâties</h3>
</div>
<div class="chart-content">
<apx-chart
[series]="pieBatieOptions.series"
[chart]="pieBatieOptions.chart"
[labels]="pieBatieOptions.labels"
[colors]="pieBatieOptions.colors"
[legend]="pieBatieOptions.legend"
[plotOptions]="pieBatieOptions.plotOptions"
[dataLabels]="pieBatieOptions.dataLabels"
[responsive]="pieBatieOptions.responsive">
</apx-chart>
</div>
<div class="chart-footer">
<div class="chart-summary">
<div class="summary-item">
<span class="summary-label">Bâties :</span>
<span class="summary-value active">{{ statistiquesGlobales.parcellesBaties | number:'1.0-0': 'fr' }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Non bâties :</span>
<span class="summary-value inactive">{{ statistiquesGlobales.parcellesNonBaties | number:'1.0-0': 'fr' }}</span>
</div>
</div>
</div>
</nz-card>
</div>
</div>
</div>
<!-- ── THÉMATIQUE : Enquête par territoire ── -->
<div *ngIf="activeThematique === 'enquete'" class="thematique-content">
<div class="row">
<!-- Bar par commune -->
<div class="col-lg-6">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="bar-chart"></span> Enquête par commune</h3>
</div>
<div class="chart-content">
<apx-chart
[series]="barCommuneOptions.series"
[chart]="barCommuneOptions.chart"
[xaxis]="barCommuneOptions.xaxis"
[yaxis]="barCommuneOptions.yaxis"
[colors]="barCommuneOptions.colors"
[legend]="barCommuneOptions.legend"
[stroke]="barCommuneOptions.stroke"
[markers]="barCommuneOptions.markers"
[grid]="barCommuneOptions.grid"
[dataLabels]="barCommuneOptions.dataLabels"
[plotOptions]="barCommuneOptions.plotOptions"
[tooltip]="barCommuneOptions.tooltip"
[fill]="barCommuneOptions.fill">
</apx-chart>
</div>
</nz-card>
</div>
<!-- Bar par structure -->
<div class="col-lg-6">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="bank"></span> Enquête par structure</h3>
</div>
<div class="chart-content">
<apx-chart
[series]="barStructureOptions.series"
[chart]="barStructureOptions.chart"
[xaxis]="barStructureOptions.xaxis"
[yaxis]="barStructureOptions.yaxis"
[colors]="barStructureOptions.colors"
[legend]="barStructureOptions.legend"
[stroke]="barStructureOptions.stroke"
[markers]="barStructureOptions.markers"
[grid]="barStructureOptions.grid"
[dataLabels]="barStructureOptions.dataLabels"
[plotOptions]="barStructureOptions.plotOptions"
[tooltip]="barStructureOptions.tooltip"
[fill]="barStructureOptions.fill">
</apx-chart>
</div>
</nz-card>
</div>
</div>
<!-- Tableau par commune -->
<div class="row mt-3">
<div class="col-lg-12">
<nz-card class="stats-table-card">
<div class="table-header">
<h3 class="table-title"><span nz-icon nzType="table"></span> Détail par commune</h3>
</div>
<nz-table #communeTable [nzData]="statsParCommune" [nzPageSize]="5">
<thead>
<tr>
<th>Commune</th>
<th nzAlign="center">Total</th>
<th nzAlign="center">Enquêtées</th>
<th nzAlign="center">Non enquêtées</th>
<th nzAlign="center">Taux d'enquête</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of communeTable.data">
<td><span class="fonction-name">{{ item.commune }}</span></td>
<td nzAlign="center"><nz-tag [nzColor]="'blue'">{{ item.total | number:'1.0-0': 'fr' }}</nz-tag></td>
<td nzAlign="center"><nz-tag [nzColor]="'green'">{{ item.enquetees | number:'1.0-0': 'fr' }}</nz-tag></td>
<td nzAlign="center"><nz-tag [nzColor]="'default'">{{ item.nonEnquetees | number:'1.0-0': 'fr' }}</nz-tag></td>
<td nzAlign="center">
<nz-progress [nzPercent]="item.tauxEnquete"
[nzStrokeColor]="getProgressColor(item.tauxEnquete)"
[nzShowInfo]="true" nzSize="small">
</nz-progress>
</td>
</tr>
</tbody>
</nz-table>
</nz-card>
</div>
</div>
</div>
<!-- ── THÉMATIQUE : Géoréférencement ── -->
<div *ngIf="activeThematique === 'georef'" class="thematique-content">
<div class="row">
<div class="col-lg-5">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="pie-chart"></span> Géoréférencement global</h3>
</div>
<div class="chart-content">
<apx-chart
[series]="pieGeoOptions.series"
[chart]="pieGeoOptions.chart"
[labels]="pieGeoOptions.labels"
[colors]="pieGeoOptions.colors"
[legend]="pieGeoOptions.legend"
[plotOptions]="pieGeoOptions.plotOptions"
[dataLabels]="pieGeoOptions.dataLabels"
[responsive]="pieGeoOptions.responsive">
</apx-chart>
</div>
<div class="chart-footer">
<div class="chart-summary">
<div class="summary-item">
<span class="summary-label">Géoréférencées :</span>
<span class="summary-value active">{{ statistiquesGlobales.parcellesGeoreferencees | number:'1.0-0': 'fr' }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Sans géométrie :</span>
<span class="summary-value inactive">{{ statistiquesGlobales.parcellesNonGeoreferencees | number:'1.0-0': 'fr' }}</span>
</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-7">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="bar-chart"></span> Géoréférencement par commune</h3>
</div>
<div class="chart-content">
<apx-chart
[series]="barGeoParCommuneOptions.series"
[chart]="barGeoParCommuneOptions.chart"
[xaxis]="barGeoParCommuneOptions.xaxis"
[yaxis]="barGeoParCommuneOptions.yaxis"
[colors]="barGeoParCommuneOptions.colors"
[legend]="barGeoParCommuneOptions.legend"
[stroke]="barGeoParCommuneOptions.stroke"
[markers]="barGeoParCommuneOptions.markers"
[grid]="barGeoParCommuneOptions.grid"
[dataLabels]="barGeoParCommuneOptions.dataLabels"
[plotOptions]="barGeoParCommuneOptions.plotOptions"
[tooltip]="barGeoParCommuneOptions.tooltip"
[fill]="barGeoParCommuneOptions.fill">
</apx-chart>
</div>
<nz-card class="alert-card m-3">
<div class="alert-content alert-warning">
<span nz-icon nzType="warning" nzTheme="fill" class="alert-icon"></span>
<div class="alert-body">
<div class="alert-title">Données attributaires sans géométrie</div>
<div class="alert-value">{{ statistiquesGlobales.parcellesAvecDonneesNonGeo | number:'1.0-0': 'fr' }} parcelles</div>
<div class="alert-desc">Ces parcelles ont des données fiscales mais ne sont pas encore géoréférencées.</div>
</div>
</div>
</nz-card>
</nz-card>
<!-- Alerte données sans géom -->
</div>
</div>
</div>
<!-- ── THÉMATIQUE : Situation fiscale ── -->
<div *ngIf="activeThematique === 'fiscal'" class="thematique-content">
<div class="row">
<div class="col-lg-4">
<nz-card class="alert-card">
<div class="alert-content alert-danger">
<span nz-icon nzType="close-circle" nzTheme="fill" class="alert-icon"></span>
<div class="alert-body">
<div class="alert-title">Parcelles endettées</div>
<div class="alert-value">{{ statistiquesGlobales.parcellesEndettees | number:'1.0-0': 'fr' }}</div>
<div class="alert-desc">Parcelles avec arriérés fiscaux non réglés.</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-4">
<nz-card class="alert-card">
<div class="alert-content alert-success">
<span nz-icon nzType="check-circle" nzTheme="fill" class="alert-icon"></span>
<div class="alert-body">
<div class="alert-title">Parcelles à jour</div>
<div class="alert-value">{{ statistiquesGlobales.parcellesAJour | number:'1.0-0': 'fr' }}</div>
<div class="alert-desc">Parcelles dont la situation fiscale est régularisée.</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-4">
<nz-card class="alert-card">
<div class="alert-content alert-info">
<span nz-icon nzType="rise" nzTheme="outline" class="alert-icon"></span>
<div class="alert-body">
<div class="alert-title">Taux de régularisation</div>
<div class="alert-value">{{ getTauxAJour() }}%</div>
<div class="alert-desc">Part des parcelles à jour sur le total enquêté.</div>
</div>
</div>
</nz-card>
</div>
</div>
<!-- Tableau statuts fiscaux -->
<div class="row mt-3">
<div class="col-lg-12">
<nz-card class="stats-table-card">
<div class="table-header">
<h3 class="table-title"><span nz-icon nzType="table"></span> Détail par statut fiscal</h3>
</div>
<nz-table #statutTable [nzData]="statsParStatut" [nzPageSize]="10" [nzShowPagination]="false">
<thead>
<tr>
<th>Couleur</th>
<th>Statut</th>
<th nzAlign="center">Nombre</th>
<th nzAlign="center">Pourcentage</th>
<th>Progression</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of statutTable.data">
<td>
<span class="statut-dot-table" [style.background]="item.couleur"></span>
</td>
<td><span class="fonction-name">{{ item.libelle }}</span></td>
<td nzAlign="center"><nz-tag [nzColor]="'default'">{{ item.nombre | number:'1.0-0': 'fr' }}</nz-tag></td>
<td nzAlign="center"><strong>{{ item.pourcentage }}%</strong></td>
<td>
<nz-progress [nzPercent]="item.pourcentage"
[nzStrokeColor]="item.couleur"
[nzShowInfo]="false" nzSize="small">
</nz-progress>
</td>
</tr>
</tbody>
</nz-table>
</nz-card>
</div>
</div>
</div>
</div>
<!-- fin thematique-tabs-section -->
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,388 @@
import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { NzMessageService } from 'ng-zorro-antd/message';
import {
ChartComponent,
ApexChart,
ApexAxisChartSeries,
ApexXAxis,
ApexYAxis,
ApexLegend,
ApexStroke,
ApexMarkers,
ApexGrid,
ApexDataLabels,
ApexTooltip,
ApexPlotOptions,
ApexNonAxisChartSeries,
ApexResponsive,
ApexFill
} from 'ng-apexcharts';
export interface StatutParcelle {
couleur: string;
code: string;
libelle: string;
}
export interface StatistiquesGlobales {
totalParcelles: number;
parcellesBaties: number;
parcellesNonBaties: number;
parcellesEnquetees: number;
parcellesNonEnquetees: number;
parcellesGeoreferencees: number;
parcellesNonGeoreferencees: number;
parcellesAJour: number;
parcellesEndettees: number;
parcellesAvecDonneesNonGeo: number;
}
export interface StatParStatut {
code: string;
libelle: string;
couleur: string;
nombre: number;
pourcentage: number;
}
export interface StatParCommune {
commune: string;
enquetees: number;
nonEnquetees: number;
total: number;
tauxEnquete: number;
georeferencees: number;
nonGeoreferencees: number;
}
export interface StatParStructure {
structure: string;
enquetees: number;
nonEnquetees: number;
total: number;
tauxEnquete: number;
}
export type PieChartOptions = {
series: ApexNonAxisChartSeries;
chart: ApexChart;
labels: string[];
colors: string[];
legend: ApexLegend;
plotOptions: ApexPlotOptions;
dataLabels: ApexDataLabels;
responsive: ApexResponsive[];
};
export type BarChartOptions = {
series: ApexAxisChartSeries;
chart: ApexChart;
xaxis: ApexXAxis;
yaxis: ApexYAxis;
colors: string[];
legend: ApexLegend;
stroke: ApexStroke;
markers: ApexMarkers;
grid: ApexGrid;
dataLabels: ApexDataLabels;
tooltip: ApexTooltip;
plotOptions: ApexPlotOptions;
fill: ApexFill;
};
@Component({
selector: 'app-sommaire-cartographie',
templateUrl: './sommaire-cartographie.component.html',
styleUrls: ['./sommaire-cartographie.component.css'],
encapsulation: ViewEncapsulation.None // ← ajouter ceci
})
export class SommaireCartographieComponent implements OnInit {
@ViewChild('chart') chart!: ChartComponent;
loading = false;
activeThematique = 'statuts';
readonly statutsParcelle: StatutParcelle[] = [
{ couleur: '#94a3b8', code: 'NON_ENQUETER', libelle: 'Parcelles non enquêtées' },
{ couleur: '#06b6d4', code: 'ENQUETER_NON_BATIE_AJOUR', libelle: 'Non bâties — À jour' },
{ couleur: '#22c55e', code: 'ENQUETER_BATIE_AJOUR', libelle: 'Bâties — À jour' },
{ couleur: '#f59e0b', code: 'ENQUETER_NON_BATIE_NON_AJOUR', libelle: 'Non bâties — Non à jour' },
{ couleur: '#f97316', code: 'ENQUETER_BATIE_NON_AJOUR', libelle: 'Bâties — Non à jour' },
{ couleur: '#ef4444', code: 'PARCELLE_ENDETTE', libelle: 'Parcelles endettées' },
{ couleur: '#8b5cf6', code: 'PARCELLE_A_JOUR_DU_FISC', libelle: 'Parcelles à jour du fisc' },
];
statistiquesGlobales: StatistiquesGlobales = {
totalParcelles: 0,
parcellesBaties: 0,
parcellesNonBaties: 0,
parcellesEnquetees: 0,
parcellesNonEnquetees: 0,
parcellesGeoreferencees: 0,
parcellesNonGeoreferencees: 0,
parcellesAJour: 0,
parcellesEndettees: 0,
parcellesAvecDonneesNonGeo: 0
};
statsParStatut: StatParStatut[] = [];
statsParCommune: StatParCommune[] = [];
statsParStructure: StatParStructure[] = [];
// ── Charts ────────────────────────────────────────────────────────────
pieStatutsOptions: Partial<PieChartOptions> = {};
pieBatieOptions: Partial<PieChartOptions> = {};
pieGeoOptions: Partial<PieChartOptions> = {};
barCommuneOptions: Partial<BarChartOptions> = {};
barStructureOptions: Partial<BarChartOptions> = {};
barGeoParCommuneOptions: Partial<BarChartOptions> = {};
constructor(private message: NzMessageService) {}
ngOnInit(): void {
this.loadData();
}
loadData(): void {
this.loading = true;
setTimeout(() => {
this.statistiquesGlobales = {
totalParcelles: 4820,
parcellesBaties: 2974,
parcellesNonBaties: 1846,
parcellesEnquetees: 3640,
parcellesNonEnquetees: 1180,
parcellesGeoreferencees: 3210,
parcellesNonGeoreferencees: 1610,
parcellesAJour: 2140,
parcellesEndettees: 480,
parcellesAvecDonneesNonGeo: 890
};
this.statsParStatut = [
{ code: 'NON_ENQUETER', libelle: 'Non enquêtées', couleur: '#94a3b8', nombre: 1180, pourcentage: 24.5 },
{ code: 'ENQUETER_NON_BATIE_AJOUR', libelle: 'Non bâties — À jour', couleur: '#06b6d4', nombre: 620, pourcentage: 12.9 },
{ code: 'ENQUETER_BATIE_AJOUR', libelle: 'Bâties — À jour', couleur: '#22c55e', nombre: 1520, pourcentage: 31.5 },
{ code: 'ENQUETER_NON_BATIE_NON_AJOUR', libelle: 'Non bâties — N.à j.', couleur: '#f59e0b', nombre: 410, pourcentage: 8.5 },
{ code: 'ENQUETER_BATIE_NON_AJOUR', libelle: 'Bâties — N.à j.', couleur: '#f97316', nombre: 610, pourcentage: 12.7 },
{ code: 'PARCELLE_ENDETTE', libelle: 'Endettées', couleur: '#ef4444', nombre: 480, pourcentage: 10.0 },
{ code: 'PARCELLE_A_JOUR_DU_FISC', libelle: 'À jour du fisc', couleur: '#8b5cf6', nombre: 0, pourcentage: 0 },
];
this.statsParCommune = [
{ commune: 'Cotonou', enquetees: 1240, nonEnquetees: 310, total: 1550, tauxEnquete: 80, georeferencees: 1100, nonGeoreferencees: 450 },
{ commune: 'Porto-Novo', enquetees: 860, nonEnquetees: 290, total: 1150, tauxEnquete: 75, georeferencees: 740, nonGeoreferencees: 410 },
{ commune: 'Parakou', enquetees: 620, nonEnquetees: 280, total: 900, tauxEnquete: 69, georeferencees: 510, nonGeoreferencees: 390 },
{ commune: 'Abomey-Calavi',enquetees: 510, nonEnquetees: 190, total: 700, tauxEnquete: 73, georeferencees: 430, nonGeoreferencees: 270 },
{ commune: 'Natitingou', enquetees: 410, nonEnquetees: 110, total: 520, tauxEnquete: 79, georeferencees: 430, nonGeoreferencees: 90 },
];
this.statsParStructure = [
{ structure: 'DGI Cotonou', enquetees: 920, nonEnquetees: 230, total: 1150, tauxEnquete: 80 },
{ structure: 'DGI Porto-Novo', enquetees: 710, nonEnquetees: 240, total: 950, tauxEnquete: 75 },
{ structure: 'Centre Impôts Sud',enquetees: 580, nonEnquetees: 220, total: 800, tauxEnquete: 73 },
{ structure: 'Centre Impôts Nord',enquetees: 430, nonEnquetees: 170, total: 600, tauxEnquete: 72 },
{ structure: 'Service Calavi', enquetees: 320, nonEnquetees: 120, total: 440, tauxEnquete: 73 },
];
this.loading = false;
this.buildAllCharts();
}, 800);
}
buildAllCharts(): void {
this.buildPieStatuts();
this.buildPieBatie();
this.buildPieGeo();
this.buildBarCommune();
this.buildBarStructure();
this.buildBarGeoParCommune();
}
// ── Pie : répartition par statut ─────────────────────────────────────
buildPieStatuts(): void {
const nonZero = this.statsParStatut.filter(s => s.nombre > 0);
this.pieStatutsOptions = {
series: nonZero.map(s => s.nombre),
chart: { type: 'donut', height: 360, fontFamily: 'Inter, sans-serif',
animations: { enabled: true, easing: 'easeinout', speed: 700 } },
labels: nonZero.map(s => s.libelle),
colors: nonZero.map(s => s.couleur),
legend: { position: 'bottom', fontSize: '12px', fontWeight: 500, labels: { colors: '#1f2937' } },
plotOptions: {
pie: {
donut: {
size: '68%',
labels: {
show: true,
name: { show: true, fontSize: '14px', fontWeight: 600 },
value: { show: true, fontSize: '20px', fontWeight: 700,
formatter: (val: string) => val + ' parc.' },
total: { show: true, label: 'Total', fontSize: '14px',
formatter: (w: any) => w.globals.seriesTotals.reduce((a: number, b: number) => a + b, 0) + ' parc.' }
}
}
}
},
dataLabels: { enabled: false },
responsive: [{ breakpoint: 480, options: { chart: { height: 280 }, legend: { position: 'bottom' } } }]
};
}
// ── Pie : bâties vs non bâties ────────────────────────────────────────
buildPieBatie(): void {
this.pieBatieOptions = {
series: [this.statistiquesGlobales.parcellesBaties, this.statistiquesGlobales.parcellesNonBaties],
chart: { type: 'pie', height: 300, fontFamily: 'Inter, sans-serif',
animations: { enabled: true, easing: 'easeinout', speed: 700 } },
labels: ['Parcelles Bâties', 'Parcelles Non Bâties'],
colors: ['#10b981', '#f59e0b'],
legend: { position: 'bottom', fontSize: '12px' },
plotOptions: { pie: { expandOnClick: true } },
dataLabels: { enabled: true, formatter: (val: number) => val.toFixed(1) + '%',
style: { fontSize: '12px', fontWeight: 600, colors: ['#fff'] } },
responsive: [{ breakpoint: 480, options: { chart: { height: 250 } } }]
};
}
// ── Pie : géoréférencées vs non géoréférencées ────────────────────────
buildPieGeo(): void {
this.pieGeoOptions = {
series: [
this.statistiquesGlobales.parcellesGeoreferencees,
this.statistiquesGlobales.parcellesNonGeoreferencees
],
chart: { type: 'pie', height: 300, fontFamily: 'Inter, sans-serif',
animations: { enabled: true, easing: 'easeinout', speed: 700 } },
labels: ['Géoréférencées', 'Non géoréférencées'],
colors: ['#3b82f6', '#e11d48'],
legend: { position: 'bottom', fontSize: '12px' },
plotOptions: { pie: { expandOnClick: true } },
dataLabels: { enabled: true, formatter: (val: number) => val.toFixed(1) + '%',
style: { fontSize: '12px', fontWeight: 600, colors: ['#fff'] } },
responsive: [{ breakpoint: 480, options: { chart: { height: 250 } } }]
};
}
// ── Bar : enquêtées vs non enquêtées par commune ──────────────────────
buildBarCommune(): void {
this.barCommuneOptions = {
series: [
{ name: 'Enquêtées', data: this.statsParCommune.map(c => c.enquetees) },
{ name: 'Non enquêtées', data: this.statsParCommune.map(c => c.nonEnquetees) }
],
chart: { type: 'bar', height: 340, stacked: false, fontFamily: 'Inter, sans-serif',
toolbar: { show: true }, animations: { enabled: true, speed: 700 } },
plotOptions: { bar: { horizontal: false, columnWidth: '55%', borderRadius: 4 } },
xaxis: {
categories: this.statsParCommune.map(c => c.commune),
labels: { style: { colors: '#6b7280', fontSize: '12px' } }
},
yaxis: {
title: { text: 'Nombre de parcelles', style: { color: '#6b7280', fontSize: '13px' } },
labels: { formatter: (val: number) => val.toFixed(0) }
},
colors: ['#22c55e', '#94a3b8'],
legend: { position: 'bottom', fontSize: '13px' },
stroke: { show: true, width: 2, colors: ['transparent'] },
markers: { size: 0 },
grid: { borderColor: '#f3f4f6', strokeDashArray: 4 },
dataLabels: { enabled: false },
tooltip: { shared: true, intersect: false, y: { formatter: (val: number) => val + ' parcelles' } },
fill: { opacity: 1 }
};
}
// ── Bar : enquêtées vs non enquêtées par structure ────────────────────
buildBarStructure(): void {
this.barStructureOptions = {
series: [
{ name: 'Enquêtées', data: this.statsParStructure.map(s => s.enquetees) },
{ name: 'Non enquêtées', data: this.statsParStructure.map(s => s.nonEnquetees) }
],
chart: { type: 'bar', height: 340, stacked: true, fontFamily: 'Inter, sans-serif',
toolbar: { show: true }, animations: { enabled: true, speed: 700 } },
plotOptions: { bar: { horizontal: true, borderRadius: 4 } },
xaxis: {
categories: this.statsParStructure.map(s => s.structure),
labels: { style: { colors: '#6b7280', fontSize: '12px' } }
},
yaxis: { labels: { style: { colors: '#6b7280', fontSize: '12px' } } },
colors: ['#22c55e', '#94a3b8'],
legend: { position: 'bottom', fontSize: '13px' },
stroke: { show: false, width: 0, colors: ['transparent'] },
markers: { size: 0 },
grid: { borderColor: '#f3f4f6', strokeDashArray: 4 },
dataLabels: { enabled: true, style: { fontSize: '11px', fontWeight: 600, colors: ['#fff'] } },
tooltip: { shared: true, intersect: false, y: { formatter: (val: number) => val + ' parcelles' } },
fill: { opacity: 1 }
};
}
// ── Bar : géoréférencées par commune ──────────────────────────────────
buildBarGeoParCommune(): void {
this.barGeoParCommuneOptions = {
series: [
{ name: 'Géoréférencées', data: this.statsParCommune.map(c => c.georeferencees) },
{ name: 'Non géoréférencées', data: this.statsParCommune.map(c => c.nonGeoreferencees) }
],
chart: { type: 'bar', height: 340, stacked: true, fontFamily: 'Inter, sans-serif',
toolbar: { show: true }, animations: { enabled: true, speed: 700 } },
plotOptions: { bar: { horizontal: false, columnWidth: '55%', borderRadius: 4 } },
xaxis: {
categories: this.statsParCommune.map(c => c.commune),
labels: { style: { colors: '#6b7280', fontSize: '12px' } }
},
yaxis: {
title: { text: 'Nombre de parcelles', style: { color: '#6b7280', fontSize: '13px' } },
labels: { formatter: (val: number) => val.toFixed(0) }
},
colors: ['#3b82f6', '#e11d48'],
legend: { position: 'bottom', fontSize: '13px' },
stroke: { show: false, width: 0, colors: ['transparent'] },
markers: { size: 0 },
grid: { borderColor: '#f3f4f6', strokeDashArray: 4 },
dataLabels: { enabled: false },
tooltip: { shared: true, intersect: false, y: { formatter: (val: number) => val + ' parcelles' } },
fill: { opacity: 1 }
};
}
// ── Utilitaires ───────────────────────────────────────────────────────
getTauxEnquete(): number {
const total = this.statistiquesGlobales.totalParcelles;
return total > 0 ? Math.round((this.statistiquesGlobales.parcellesEnquetees / total) * 100) : 0;
}
getTauxBati(): number {
const total = this.statistiquesGlobales.totalParcelles;
return total > 0 ? Math.round((this.statistiquesGlobales.parcellesBaties / total) * 100) : 0;
}
getTauxGeo(): number {
const total = this.statistiquesGlobales.totalParcelles;
return total > 0 ? Math.round((this.statistiquesGlobales.parcellesGeoreferencees / total) * 100) : 0;
}
getTauxAJour(): number {
const total = this.statistiquesGlobales.totalParcelles;
return total > 0 ? Math.round((this.statistiquesGlobales.parcellesAJour / total) * 100) : 0;
}
getProgressColor(percent: number): string {
if (percent >= 80) return '#22c55e';
if (percent >= 65) return '#f59e0b';
return '#ef4444';
}
getProgressLabel(percent: number): string {
if (percent >= 80) return 'Excellent';
if (percent >= 65) return 'Moyen';
return 'Faible';
}
refreshData(): void {
this.loadData();
}
}

View File

@@ -0,0 +1,41 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ConsultationComponent } from './consultation.component';
import { NotFoundComponent } from 'src/app/shared/not-found/not-found.component';
import { SommaireConsultationComponent } from './sommaire-consultation/sommaire-consultation.component';
import { ListParcelleConsultationComponent } from './list-parcelle-consultation/list-parcelle-consultation.component';
import { ListBatimentConsultationComponent } from './list-batiment-consultation/list-batiment-consultation.component';
import { ListUniteLogementConsultationComponent } from './list-unite-logement-consultation/list-unite-logement-consultation.component';
import { ListDonneeImpositionConsultationComponent } from './list-donnee-imposition-consultation/list-donnee-imposition-consultation.component';
import { DetailInformationParcelleComponent } from 'src/app/shared/detail-information-parcelle/detail-information-parcelle.component';
import { DetailInformationBatimentComponent } from 'src/app/shared/detail-information-batiment/detail-information-batiment.component';
import { DetailInformationUniteLogementComponent } from 'src/app/shared/detail-information-unite-logement/detail-information-unite-logement.component';
const routes: Routes = [
{
path: '', component: ConsultationComponent,
children: [
{ path: 'liste-parcelle', component: ListParcelleConsultationComponent },
{ path: 'liste-batiment', component: ListBatimentConsultationComponent },
{ path: 'liste-unite-logement', component: ListUniteLogementConsultationComponent },
{ path: 'liste-donnee-imposition', component: ListDonneeImpositionConsultationComponent },
{ path: 'sommaire-consultation', component: SommaireConsultationComponent },
{ path: 'detail-parcelle/:id', component: DetailInformationParcelleComponent },
{ path: 'detail-batiment/:id', component: DetailInformationBatimentComponent },
{ path: 'detail-unite-logement/:id', component: DetailInformationUniteLogementComponent },
{ path: '**', component: NotFoundComponent }
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ConsultationRoutingModule { }

View File

@@ -0,0 +1,49 @@
<div class="row" style="margin-top: 75px;">
<div class="col-md-12">
<ul nz-menu nzTheme="dark" nzMode="horizontal" class="navBar">
<li nz-menu-item style="margin-left: 6% !important;" class="text-center" (click)="goHome()">
<img src="assets/sigibe/home.svg" alt="" class="icon-home-header">
</li>
<li nz-menu-item nzSelected routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/consultation/sommaire-consultation"> Sommaire </a>
</li>
<li nz-menu-item routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/consultation/liste-parcelle">Liste des parcelles </a>
</li>
<li nz-menu-item routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/consultation/liste-batiment"> Liste des bâtiments </a>
</li>
<li nz-menu-item routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/consultation/liste-unite-logement"> Liste des unités de logement </a>
</li>
<li nz-menu-item routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/consultation/liste-donnee-imposition"> Liste des données d'imposition</a>
</li>
</ul>
</div>
</div>
<div class="row" [ngStyle]="{ backgroundColor: module ? module.color : '' }" id="formulaire">
<div class="col-md-5" style="padding: 35px;margin-bottom: 2%;">
<div style="margin-left: 16%;">
<h3 class="text-secondary"
style="font-size: 11px;text-transform: uppercase;color: rgba(255, 255, 255, 0.61) !important;">
Module Consultation </h3>
<h1 class="text-white" style="font-size: 16px;line-height: 1.5;">
Dossier en cours sur le module consultation</h1>
</div>
</div>
<div class="col-md-2">
</div>
<div class="col-md-5" style="padding-left: 3.1%;">
<img src="assets/sigibe/logo-mef-white.png" alt=""
style="width: 200px;margin-top: 4%;float: right;margin-right: 20%;">
</div>
</div>
<div style="margin-left: 6%;margin-right: 6%;background-color: rgb(255 255 255 / 75%);">
<router-outlet></router-outlet>
</div>

View File

@@ -0,0 +1,53 @@
import { Component, OnInit } from '@angular/core';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
import { JwtHelperService } from '@auth0/angular-jwt';
import { Router } from '@angular/router';
@Component({
selector: 'app-consultation',
templateUrl: './consultation.component.html',
styleUrls: ['./consultation.component.css']
})
export class ConsultationComponent {
user: any = null;
isVisibleReference = false;
isVisibleSecteur = false;
isVisibleEquipe = false;
menuNum = 0;
module: any = null;
constructor(
private tokenStorage: TokenStorage,
private router: Router,
) {
}
ngOnInit(): void {
this.module = JSON.parse(this.tokenStorage.getModule());
console.log(JSON.parse(this.tokenStorage.getModule()));
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
console.log(this.user);
}
change(value: any, menuNum: number): void {
if (menuNum == 1)
this.isVisibleReference = value;
if (menuNum == 2)
this.isVisibleSecteur = value;
if (menuNum == 3)
this.isVisibleEquipe = value;
}
goHome(): void {
this.tokenStorage.saveModule(null);
this.router.navigate(['/principale']);
}
}

View File

@@ -0,0 +1,40 @@
import { CUSTOM_ELEMENTS_SCHEMA, NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ConsultationRoutingModule } from './consultation-routing.module';
import { ConsultationComponent } from './consultation.component';
import { SommaireConsultationComponent } from './sommaire-consultation/sommaire-consultation.component';
import { ListParcelleConsultationComponent } from './list-parcelle-consultation/list-parcelle-consultation.component';
import { ListBatimentConsultationComponent } from './list-batiment-consultation/list-batiment-consultation.component';
import { ListUniteLogementConsultationComponent } from './list-unite-logement-consultation/list-unite-logement-consultation.component';
import { ListDonneeImpositionConsultationComponent } from './list-donnee-imposition-consultation/list-donnee-imposition-consultation.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { SharedModule } from 'src/app/shared/shared.module';
import { DataTablesModule } from 'angular-datatables';
import { NgApexchartsModule } from 'ng-apexcharts';
@NgModule({
declarations: [
ConsultationComponent,
SommaireConsultationComponent,
ListParcelleConsultationComponent,
ListBatimentConsultationComponent,
ListUniteLogementConsultationComponent,
ListDonneeImpositionConsultationComponent
],
imports: [
CommonModule,
ConsultationRoutingModule,
FormsModule,
ReactiveFormsModule,
SharedModule,
DataTablesModule,
NgApexchartsModule,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
})
export class ConsultationModule { }

View File

@@ -0,0 +1,424 @@
<div class="row">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review">
<h6 class="card-title" style="font-size: 16px!important;text-transform: none;">
Liste des bâtiments <strong class="text-primary"> - (quartier sélectionné :
{{ quartierSelected ? quartierSelected.quartierNom : '-' }}) </strong>
</h6>
<nz-divider></nz-divider>
<div class="row">
<!-- ══ ARBRE ══════════════════════════════════════ -->
<div class="col-md-3" style="padding-right:0;">
<div class="arbre-container">
<div class="arbre-title">
<span nz-icon nzType="apartment" nzTheme="outline"></span>
Divisions administratives
</div>
<nz-tree [nzData]="nodes" nzShowLine nzShowIcon [nzTreeTemplate]="nzTreeTemplate"
(nzClick)="onNodeClick($event)" (nzExpandChange)="onNodeClick($event)">
</nz-tree>
<ng-template #nzTreeTemplate let-node>
<div class="tree-node-row">
<span class="tree-node-icon">
<span nz-icon
[nzType]="node.isLeaf ? 'environment' : (node.isExpanded ? 'folder-open' : 'folder')"
[style.color]="getBadgeColor(getNiveau(node.key))" nzTheme="outline">
</span>
</span>
<span class="tree-node-title" [class.tree-node-leaf]="node.isLeaf"
[class.tree-node-selected]="node.isLeaf && quartierSelected?.quartierId === node.origin?.quartier?.quartierId">
{{ node.title }}
</span>
<!--<span class="tree-node-badge"
[style.background]="getBadgeColor(getNiveau(node.key))">
{{ node.origin?.nbParcelles | number:'1.0-0':'fr' }} p.
</span>-->
</div>
</ng-template>
<div *ngIf="nodes.length === 0" class="no-data">
<span nz-icon nzType="inbox" nzTheme="outline" style="font-size:28px;"></span>
<span>Aucun département trouvé</span>
</div>
</div>
</div>
<!-- fin arbre -->
<div class="col-md-9">
<div class="formulaire p-4"
style="width: 100%; border-radius: 5px; background: #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.08);">
<div>
<h2 style="font-size: 18px;margin-bottom: -10px;">Liste des bâtiments
</h2>
<span style="background-color: #313131;font-size: 3px;margin-top:5px;">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp;
&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</span>
<p style="font-size: 12px; color: #636363; margin-bottom: 30px;margin-top:5px;">
Cette interface affiche toutes les informations sur les bâtiments
enregistrés (identification, références foncières, catégorie, usage).
</p>
</div>
<div class="pl-container">
<!-- ── En-tête ── -->
<div class="pl-header">
<div class="pl-header-left">
<span nz-icon nzType="bank" nzTheme="outline"
style="font-size:18px;color:#1f8653;"></span>
<div>
<div class="pl-title">Bâtiments du quartier</div>
<div class="pl-count">{{ totalElements }} bâtiment(s) au total</div>
</div>
</div>
<div class="pl-header-right">
<button class="pl-btn-filter mr-1" (click)="exportPageCourante()">
<span nz-icon nzType="file-excel" nzTheme="outline"
style="margin-top:-5px;"></span>
Exporter en Excel
</button>
<button class="pl-btn-filter"
[class.pl-btn-filter-active]="filterVisible || filterApplied"
(click)="toggleFiltre()">
<span nz-icon nzType="filter" nzTheme="outline"
style="margin-top:-5px;"></span>
Filtres
<span class="pl-filter-badge" *ngIf="filterApplied"></span>
</button>
</div>
</div>
<!-- ── Panneau filtres ── -->
<div class="pl-filter-panel" [class.pl-filter-panel-open]="filterVisible">
<form [formGroup]="filterForm">
<div class="pl-filter-grid">
<!-- NUB / Code -->
<div class="pl-filter-field">
<label class="pl-filter-label">NUB</label>
<input class="pl-filter-input" formControlName="nub"
placeholder="ex: B01">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Code bâtiment</label>
<input class="pl-filter-input" formControlName="code"
placeholder="ex: CODE-001">
</div>
<!-- Q / I / P parcelle -->
<div class="pl-filter-field"
style="display:grid;grid-template-columns:1fr 1fr;gap:6px;">
<div style="width: 90px;">
<label class="pl-filter-label">I (Îlot)</label>
<input style="width: 90px;" class="pl-filter-input" formControlName="parcelleI"
placeholder="ex: 101">
</div>
<div style="width: 90px;">
<label class="pl-filter-label">P (Parcelle)</label>
<input style="width: 90px;" class="pl-filter-input" formControlName="parcelleP"
placeholder="ex: A">
</div>
</div>
<!-- NUP -->
<div class="pl-filter-field">
<label class="pl-filter-label">NUP Parcelle</label>
<input class="pl-filter-input" formControlName="parcelleNup"
placeholder="ex: NUP-001">
</div>
<!-- Propriétaire -->
<div class="pl-filter-field">
<label class="pl-filter-label">Nom propriétaire</label>
<input class="pl-filter-input" formControlName="personneNom"
placeholder="ex: CODO">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Prénom propriétaire</label>
<input class="pl-filter-input" formControlName="personnePrenom"
placeholder="ex: MICHEL">
</div>
<!-- Catégorie & Usage -->
<div class="pl-filter-field">
<label class="pl-filter-label">Catégorie bâtiment</label>
<select class="pl-filter-input" formControlName="categorieBatimentId">
<option value="">-- Toutes --</option>
<option *ngFor="let cat of categorieBatimentList" [value]="cat.id">
{{ cat.code }} — {{ cat.standing }}
</option>
</select>
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Usage</label>
<select class="pl-filter-input" formControlName="usageId">
<option value="">-- Tous --</option>
<option *ngFor="let usage of usageList" [value]="usage.id">
{{ usage.nom }}
</option>
</select>
</div>
</div>
<div class="pl-filter-actions">
<button class="pl-btn-reset" type="button" (click)="reinitialiserFiltre()">
<span nz-icon nzType="redo" nzTheme="outline"></span>
Réinitialiser
</button>
<button class="pl-btn-apply" type="button" (click)="appliquerFiltre()">
<span nz-icon nzType="search" nzTheme="outline"></span>
Appliquer
<span *ngIf="filterApplied" style="margin-left:4px;">
({{ filteredList.length }} résultat(s))
</span>
</button>
</div>
</form>
</div>
<!-- ── Loading ── -->
<div class="pl-loading" *ngIf="loading">
<nz-spin nzSimple></nz-spin>
<span>Chargement des bâtiments…</span>
</div>
<!-- ── Liste ── -->
<div *ngIf="!loading">
<div class="pl-empty" *ngIf="filteredList.length === 0">
<span nz-icon nzType="inbox" nzTheme="outline" style="font-size:32px;"></span>
<p>Aucun bâtiment trouvé</p>
</div>
<!-- Cards -->
<div class="pl-card" *ngFor="let item of pageCourante">
<div class="pl-card-main">
<!-- Identification -->
<div class="pl-col pl-col-identity">
<div class="pl-badges">
<span class="pl-badge pl-badge-qip">
Q{{ item.parcelleQ }} . {{ item.parcelleI }} .
{{ item.parcelleP }}
</span>
<span class="pl-badge" *ngIf="item.categorieBatimentCode"
style="background:#e0f2fe;color:#0369a1;">
{{ item.categorieBatimentCode }}
{{ item.categorieBatimentStanding ? '— ' + item.categorieBatimentStanding : '' }}
</span>
<span class="pl-badge pl-badge-enquete"
*ngIf="item.enqueteBatiementCourantId">
<span nz-icon nzType="file-search" nzTheme="outline"></span>
Enquêté
</span>
<span class="pl-badge pl-badge-no-enquete"
*ngIf="!item.enqueteBatiementCourantId">
Non enquêté
</span>
</div>
<div class="pl-nup">
NUB : {{ item.nub || '—' }} — Code : {{ item.code || '—' }}
</div>
<div class="pl-info-line" *ngIf="item.parcelleNup">
<span nz-icon nzType="home" nzTheme="outline"></span>
NUP Parcelle : {{ item.parcelleNup }}
</div>
<div class="pl-info-line" *ngIf="item.dateConstruction">
<span nz-icon nzType="calendar" nzTheme="outline"></span>
Construit le : {{ item.dateConstruction | date:'dd/MM/yyyy' }}
</div>
</div>
<!-- Usage & Surfaces -->
<div class="pl-col pl-col-domaine">
<div class="pl-domaine-type">{{ item.usageNom || '—' }}</div>
<div class="pl-domaine-nature">{{ item.nbreUniteLogement || 0 }}
unité(s) logement</div>
<div class="pl-superficie" *ngIf="item.superficieAuSol">
<span nz-icon nzType="expand" nzTheme="outline"></span>
{{ item.superficieAuSol | number:'1.0-2':'fr' }} m² au sol
</div>
<div class="pl-superficie" *ngIf="item.nombrePiscine">
🏊 {{ item.nombrePiscine }} piscine(s)
</div>
</div>
<!-- Propriétaire -->
<div class="pl-col pl-col-prop">
<div class="pl-prop-name">
{{ item.personneRaisonSociale
|| ((item.personneNom || '') + ' ' + (item.personnePrenom || ''))
|| '—' }}
</div>
<div class="pl-prop-sub" *ngIf="item.valeurBatimentEstime">
Val. estimée : {{ formatMontant(item.valeurBatimentEstime) }}
</div>
<div class="pl-prop-sub" *ngIf="item.montantMensuelLocation">
Loyer mensuel : {{ formatMontant(item.montantMensuelLocation) }}
</div>
</div>
<!-- Action -->
<div class="pl-col pl-col-action">
<button class="pl-btn-detail" (click)="toggleRow(item.id)">
<span nz-icon [nzType]="isExpanded(item.id) ? 'up' : 'down'"></span>
{{ isExpanded(item.id) ? 'Réduire' : 'Détails' }}
</button>
</div>
</div>
<!-- Détails expandés -->
<div class="pl-card-detail" *ngIf="isExpanded(item.id)">
<div class="pl-detail-grid">
<div class="pl-detail-item">
<span class="pl-detail-label">NUB</span>
<span class="pl-detail-val">{{ item.nub || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Code</span>
<span class="pl-detail-val">{{ item.code || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Date construction</span>
<span class="pl-detail-val">
{{ (item.dateConstruction | date:'dd/MM/yyyy') || '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Q . I . P Parcelle</span>
<span class="pl-detail-val">
{{ item.parcelleQ }} . {{ item.parcelleI }} .
{{ item.parcelleP }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">NUP Parcelle</span>
<span class="pl-detail-val">{{ item.parcelleNup || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Catégorie / Standing</span>
<span class="pl-detail-val">
{{ item.categorieBatimentCode || '—' }}
{{ item.categorieBatimentStanding ? '— ' + item.categorieBatimentStanding : '' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Usage</span>
<span class="pl-detail-val">{{ item.usageNom || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Superficie au sol (m²)</span>
<span class="pl-detail-val">{{ item.superficieAuSol || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Superficie louée (m²)</span>
<span class="pl-detail-val">{{ item.superficieLouee || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Nbre unités logement</span>
<span
class="pl-detail-val">{{ item.nbreUniteLogement ?? '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Nombre piscines</span>
<span class="pl-detail-val">{{ item.nombrePiscine ?? '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Propriétaire</span>
<span class="pl-detail-val">
{{ item.personneRaisonSociale
|| ((item.personneNom || '') + ' ' + (item.personnePrenom || ''))
|| '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Loyer mensuel</span>
<span
class="pl-detail-val">{{ formatMontant(item.montantMensuelLocation) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Loyer annuel déclaré</span>
<span
class="pl-detail-val">{{ formatMontant(item.montantLocatifAnnuelDeclare) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Loyer annuel calculé</span>
<span
class="pl-detail-val">{{ formatMontant(item.montantLocatifAnnuelCalcule) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Loyer annuel estimé</span>
<span
class="pl-detail-val">{{ formatMontant(item.montantLocatifAnnuelEstime) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. estimée bâtiment</span>
<span
class="pl-detail-val">{{ formatMontant(item.valeurBatimentEstime) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. réelle bâtiment</span>
<span
class="pl-detail-val">{{ formatMontant(item.valeurBatimentReel) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. calculée bâtiment</span>
<span
class="pl-detail-val">{{ formatMontant(item.valeurBatimentCalcule) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-val">
<button class="btn-action btn-action-person"
(click)="afficherDetail(item.id)">
<i class="mdi mdi-arrow-right"></i> Plus de détails
</button>
</span>
</div>
</div>
</div>
</div>
<!-- fin cards -->
<!-- Pagination -->
<div class="pl-pagination" *ngIf="totalElements > pageSize">
<span class="pl-pagination-info">
Page {{ pageNo + 1 }} sur {{ totalPages }} — {{ totalElements }} bâtiment(s)
</span>
<nz-pagination [nzPageIndex]="pageNo + 1" [nzTotal]="totalElements"
[nzPageSize]="pageSize" [nzShowSizeChanger]="true"
[nzPageSizeOptions]="[10, 20, 50, 100]"
(nzPageIndexChange)="onPageChange($event)"
(nzPageSizeChange)="onPageSizeChange($event)">
</nz-pagination>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,420 @@
import { Component, SimpleChanges, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzTreeNodeOptions } from 'ng-zorro-antd/tree';
import { firstValueFrom } from 'rxjs';
import { CrudService } from 'src/app/crud.service';
import { ExcelExportService } from 'src/app/excel-export.service';
import { GlobalService } from 'src/app/global.service';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
export interface BatimentPagedItem {
id?: number;
nub?: string;
code?: string;
dateConstruction?: string;
parcelleId?: number;
parcelleNup?: string;
parcelleQ?: string;
parcelleI?: string;
parcelleP?: string;
personneId?: number;
personneNom?: string;
personnePrenom?: string;
personneRaisonSociale?: string;
superficieAuSol?: number;
superficieLouee?: number;
enqueteBatiementCourantId?: number;
categorieBatimentId?: number;
categorieBatimentCode?: string;
categorieBatimentStanding?: string;
nombrePiscine?: number;
montantLocatifAnnuelDeclare?: number;
montantLocatifAnnuelCalcule?: number;
montantLocatifAnnuelEstime?: number;
valeurBatimentEstime?: number;
valeurBatimentReel?: number;
valeurBatimentCalcule?: number;
montantMensuelLocation?: number;
usageId?: number;
usageNom?: string;
nbreUniteLogement?: number;
}
@Component({
selector: 'app-list-batiment-consultation',
templateUrl: './list-batiment-consultation.component.html',
styleUrls: ['./list-batiment-consultation.component.css']
})
export class ListBatimentConsultationComponent {
// ── Données ───────────────────────────────────────────
donnees: BatimentPagedItem[] = [];
loading = false;
// ── Pagination client-side ────────────────────────────
pageNo = 0;
pageSize = 10;
// ── Filtre ────────────────────────────────────────────
filterForm!: FormGroup;
filterVisible = false;
filterApplied = false;
// ── Ligne expandée ────────────────────────────────────
expandedIds = new Set<number>();
// ── Référentiels ──────────────────────────────────────
usageList: any[] = [];
categorieBatimentList: any[] = [];
// ── EXPORT LABELS ─────────────────────────────────────
private readonly EXPORT_LABELS: { [key: string]: string } = {
id: 'ID Bâtiment',
nub: 'N° Bâtiment (NUB)',
code: 'Code Bâtiment',
dateConstruction: 'Date Construction',
parcelleId: 'ID Parcelle',
parcelleNup: 'NUP Parcelle',
parcelleQ: 'Q (Quartier)',
parcelleI: 'I (Îlot)',
parcelleP: 'P (Parcelle)',
personneId: 'ID Propriétaire',
personneNom: 'Nom Propriétaire',
personnePrenom: 'Prénom Propriétaire',
personneRaisonSociale: 'Raison Sociale Propriétaire',
superficieAuSol: 'Superficie au Sol (m²)',
superficieLouee: 'Superficie Louée (m²)',
enqueteBatiementCourantId: 'ID Enquête Courante',
categorieBatimentId: 'ID Catégorie Bâtiment',
categorieBatimentCode: 'Code Catégorie Bâtiment',
categorieBatimentStanding: 'Standing Bâtiment',
nombrePiscine: 'Nombre Piscines',
montantLocatifAnnuelDeclare: 'Loyer Annuel Déclaré (FCFA)',
montantLocatifAnnuelCalcule: 'Loyer Annuel Calculé (FCFA)',
montantLocatifAnnuelEstime: 'Loyer Annuel Estimé (FCFA)',
valeurBatimentEstime: 'Valeur Estimée Bâtiment (FCFA)',
valeurBatimentReel: 'Valeur Réelle Bâtiment (FCFA)',
valeurBatimentCalcule: 'Valeur Calculée Bâtiment (FCFA)',
montantMensuelLocation: 'Loyer Mensuel (FCFA)',
usageId: 'ID Usage',
usageNom: 'Usage',
nbreUniteLogement: 'Nbre Unités Logement',
};
private readonly CHAMPS_EXCLUS = new Set([
'id', 'parcelleId', 'personneId',
'enqueteBatiementCourantId', 'categorieBatimentId', 'usageId',
]);
user: any = null;
numMenu = 2;
nodes: NzTreeNodeOptions[] = []; // Les noeuds de l'arbre
arbreUtilisateurCourant: any[] = [];
quartierSelected: any = null;
isActionInProgress = false;
constructor(
private fb: FormBuilder,
private tokenStorage: TokenStorage,
private router: Router,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
private globalService: GlobalService,
private modalService: NzModalService,
private viewContainerRef: ViewContainerRef,
private excelExportService: ExcelExportService,
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
ngOnInit(): void {
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
if (this.user) {
this.crudService.getAll('secteur-decoupage/arbre/user-id/' + this.user?.id).subscribe(
(data: any) => {
this.arbreUtilisateurCourant = data.object ? data.object : [];
if (this.arbreUtilisateurCourant && this.arbreUtilisateurCourant.length > 0) {
this.constructionArbreUtilisateurs();
}
this.message.success(`Chargement des découpages de l'utilisateur ${this.user?.nom} réussi`);
},
(error: any) => {
this.message.error(`Chargement des découpages de l'utilisateur ${this.user?.nom} échoué`);
});
}
this.initFilterForm();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['quartierId'] && this.quartierSelected?.quartierId) {
this.pageNo = 0;
this.reinitialiserFiltre();
this.charger();
}
}
// ── Utilitaire ────────────────────────────────────────
constructionArbreUtilisateurs() {
const data: any[] = this.arbreUtilisateurCourant;
// Grouper par département
const deptMap = new Map<number, any>();
data.forEach(item => {
if (!deptMap.has(item.departementId)) {
deptMap.set(item.departementId, {
...item,
communes: new Map<number, any>()
});
}
const dept = deptMap.get(item.departementId);
if (!dept.communes.has(item.communeId)) {
dept.communes.set(item.communeId, {
...item,
arrondissements: new Map<number, any>()
});
}
const commune = dept.communes.get(item.communeId);
if (!commune.arrondissements.has(item.arrondissementId)) {
commune.arrondissements.set(item.arrondissementId, {
...item,
quartiers: []
});
}
const arr = commune.arrondissements.get(item.arrondissementId);
arr.quartiers.push(item);
});
// Construire les noeuds
this.nodes = Array.from(deptMap.values()).map(dept => ({
title: `${dept.departementCode}${dept.departementNom}`,
key: `dept-${dept.departementId}`,
icon: 'bank',
isLeaf: false,
expanded: false,
nbParcelles: dept.nbParcellesDepartement,
children: Array.from(dept.communes.values()).map((comm: any) => ({
title: `${comm.communeCode}${comm.communeNom}`,
key: `comm-${comm.communeId}`,
icon: 'home',
isLeaf: false,
expanded: false,
nbParcelles: comm.nbParcellesCommune,
children: Array.from(comm.arrondissements.values()).map((arr: any) => ({
title: arr.arrondissementNom,
key: `arr-${arr.arrondissementId}`,
icon: 'apartment',
isLeaf: false,
expanded: false,
nbParcelles: arr.nbParcellesArrondissement,
children: arr.quartiers.map((quart: any) => ({
title: quart.quartierNom,
key: `quart-${quart.quartierId}`,
icon: 'environment',
isLeaf: true,
nbParcelles: quart.nbParcellesQuartier,
quartier: quart
}))
}))
}))
}));
}
onNodeClick(event: any) {
const node = event.node;
if (node.isLeaf && node.key.startsWith('quart-')) {
this.quartierSelected = node.origin.quartier;
console.log(' this.quartierSelected ==>', this.quartierSelected);
this.message.create('success', `Quartier ${this.quartierSelected.quartierNom} sélectionné.`);
// votre logique de sélection...
this.charger();
}
}
getBadgeColor(niveau: string): string {
const colors: any = {
'dept': '#204e10',
'comm': '#10b981',
'arr': '#f59e0b',
'quart': '#ef6972'
};
return colors[niveau] ?? '#6b7280';
}
getNiveau(key: string): string {
if (key.startsWith('dept')) return 'dept';
if (key.startsWith('comm')) return 'comm';
if (key.startsWith('arr')) return 'arr';
if (key.startsWith('quart')) return 'quart';
return '';
}
// ── Init formulaire de filtre ─────────────────────────
initFilterForm(): void {
this.crudService.getAll('usage/all').subscribe((data: any) => {
this.usageList = data.object ?? [];
});
this.crudService.getAll('categorie-batiment/all').subscribe((data: any) => {
this.categorieBatimentList = data.object ?? [];
});
this.filterForm = this.fb.group({
nub: [null],
code: [null],
parcelleNup: [null],
parcelleQ: [null],
parcelleI: [null],
parcelleP: [null],
personneNom: [null],
personnePrenom: [null],
personneRaisonSociale: [null],
categorieBatimentId: [null],
usageId: [null],
});
}
// ── Chargement ────────────────────────────────────────
charger(): void {
if (!this.quartierSelected) return;
this.loading = true;
const url = `batiment/all/by-quartier-id/${this.quartierSelected?.quartierId}`;
this.crudService.getAll(url).subscribe({
next: (data: any) => {
this.donnees = data?.object ?? [];
this.pageNo = 0;
this.loading = false;
},
error: () => {
this.message.error('Erreur lors du chargement des bâtiments.');
this.loading = false;
}
});
}
// ── Filtre client-side ────────────────────────────────
get filteredList(): BatimentPagedItem[] {
if (!this.filterApplied) return this.donnees;
const f = this.filterForm.value;
const match = (
filterVal: string | null | undefined,
itemVal: string | null | undefined
): boolean => {
if (!filterVal?.trim()) return true;
if (!itemVal?.trim()) return false;
return itemVal.toLowerCase().includes(filterVal.trim().toLowerCase());
};
return this.donnees.filter(b =>
match(f.nub, b.nub) &&
match(f.code, b.code) &&
match(f.parcelleNup, b.parcelleNup) &&
match(f.parcelleQ, b.parcelleQ) &&
match(f.parcelleI, b.parcelleI) &&
match(f.parcelleP, b.parcelleP) &&
(
match(f.personneNom, b.personneNom) ||
match(f.personneNom, b.personneRaisonSociale)
) &&
match(f.personnePrenom, b.personnePrenom) &&
(!f.categorieBatimentId || b.categorieBatimentId?.toString() === f.categorieBatimentId) &&
(!f.usageId || b.usageId?.toString() === f.usageId)
);
}
get pageCourante(): BatimentPagedItem[] {
const debut = this.pageNo * this.pageSize;
return this.filteredList.slice(debut, debut + this.pageSize);
}
get totalElements(): number { return this.filteredList.length; }
get totalPages(): number {
return this.pageSize > 0 ? Math.ceil(this.totalElements / this.pageSize) : 0;
}
onPageChange(page: number): void { this.pageNo = page - 1; }
onPageSizeChange(size: number): void {
this.pageSize = size;
this.pageNo = 0;
}
appliquerFiltre(): void {
this.filterApplied = true;
this.pageNo = 0;
}
reinitialiserFiltre(): void {
this.filterForm?.reset();
this.filterApplied = false;
this.pageNo = 0;
}
toggleFiltre(): void { this.filterVisible = !this.filterVisible; }
toggleRow(id: number | undefined): void {
if (id == null) return;
this.expandedIds.has(id) ? this.expandedIds.delete(id) : this.expandedIds.add(id);
}
isExpanded(id: number | undefined): boolean {
return id != null && this.expandedIds.has(id);
}
formatMontant(val: number | null | undefined): string {
if (val == null) return '—';
return new Intl.NumberFormat('fr-FR').format(val) + ' FCFA';
}
private nettoyerLigneExport(item: any): { [label: string]: any } {
const ligne: { [label: string]: any } = {};
for (const key of Object.keys(this.EXPORT_LABELS)) {
if (this.CHAMPS_EXCLUS.has(key)) continue;
const val = item[key];
ligne[this.EXPORT_LABELS[key]] =
typeof val === 'boolean' ? (val ? 'OUI' : 'NON') :
val == null ? '—' : val;
}
return ligne;
}
exportPageCourante(): void {
if (!this.filteredList.length) {
this.message.warning('Aucune donnée à exporter.');
return;
}
const data = this.filteredList.map(item => this.nettoyerLigneExport(item));
this.excelExportService.exportAsExcelFile(data, 'batiments', 'Bâtiments');
}
afficherDetail(id: any): void {
this.router.navigate(['/core/consultation/detail-batiment/' + id]);
}
}

View File

@@ -0,0 +1,689 @@
<div class="row">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review">
<h6 class="card-title" style="font-size: 16px!important;text-transform: none;">
Liste des données d'imposition
</h6>
<nz-divider></nz-divider>
<div class="row">
<!-- ══ ARBRE ══════════════════════════════════════ -->
<div class="col-md-3" style="padding-right:0;">
<div class="arbre-container" style="min-height: auto;">
<div class="arbre-title">
<span nz-icon nzType="calendar" nzTheme="outline"></span>
Centre des impôts
</div>
<div class="mt-4">
<div class="row">
<div class="col-md-12">
<div class="did-floating-label-content mt-1">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Sélectionner le centre"
[(ngModel)]="structure" [compareWith]="compareFn"
(ngModelChange)="changeStructure($event)">
<nz-option *ngFor="let s of structureList" [nzLabel]="s.nom"
[nzValue]="s">
</nz-option>
</nz-select>
<label class="did-floating-label" style="top:-15px;">
Centre des impôts <span class="text-danger">*</span>
</label>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-md-12">
<div class="did-floating-label-content mt-1">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Sélectionner le centre"
[(ngModel)]="exercice" [compareWith]="compareFn"
(ngModelChange)="changeExercice($event)">
<nz-option *ngFor="let s of exerciceList" [nzLabel]="s.annee"
[nzValue]="s">
</nz-option>
</nz-select>
<label class="did-floating-label" style="top:-15px;">
Exercice fiscale <span class="text-danger">*</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="arbre-container mt-3" style="min-height: 40%;">
<div class="arbre-title">
<span nz-icon nzType="apartment" nzTheme="outline"></span>
Divisions administratives
</div>
<nz-tree [nzData]="nodes" nzShowLine nzShowIcon [nzTreeTemplate]="nzTreeTemplate"
(nzClick)="onNodeClick($event)" (nzExpandChange)="onNodeClick($event)">
</nz-tree>
<ng-template #nzTreeTemplate let-node>
<div class="tree-node-row">
<span class="tree-node-icon">
<span nz-icon
[nzType]="node.isLeaf ? 'environment' : (node.isExpanded ? 'folder-open' : 'folder')"
[style.color]="getBadgeColor(getNiveau(node.key))" nzTheme="outline">
</span>
</span>
<span class="tree-node-title" [class.tree-node-leaf]="node.isLeaf"
[class.tree-node-selected]="node.isLeaf && quartierSelected?.quartierId === node.origin?.quartier?.quartierId">
{{ node.title }}
</span>
<!--<span class="tree-node-badge"
[style.background]="getBadgeColor(getNiveau(node.key))">
{{ node.origin?.nbParcelles | number:'1.0-0':'fr' }} p.
</span>-->
</div>
</ng-template>
<div *ngIf="nodes.length === 0" class="no-data">
<span nz-icon nzType="inbox" nzTheme="outline" style="font-size:28px;"></span>
<span>Aucun département trouvé</span>
</div>
</div>
</div>
<!-- fin arbre -->
<div class="col-md-9">
<div class="formulaire p-4"
style="width: 100%; border-radius: 5px; background: #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.08);">
<div>
<span class="quartier-pill quartier-pill" *ngIf="structure">
<span nz-icon nzType="home" nzTheme="outline"
style="margin-top: -5px;"></span>
<span class="context-chip-label"></span>
{{ structure?.nom || '-' }}
</span>
<span class="quartier-pill quartier-pill ml-2" *ngIf="exercice">
<span nz-icon nzType="calendar" nzTheme="outline"
style="margin-top: -5px;"></span>
<span class="context-chip-label"> </span>
{{ exercice?.annee || '-' }}
</span>
<!-- Parcelle sélectionnée -->
<span class="quartier-pill quartier-pill-empty ml-2" *ngIf="quartierSelected">
<span nz-icon nzType="environment" nzTheme="outline"
style="margin-top: -5px;"></span>
<span class="context-chip-label"> </span>
{{ quartierSelected?.quartierNom || '-' }}
</span>
<h2 style="font-size: 18px;margin-bottom: -10px;" class="mt-3">Liste des données d'imposition
</h2>
<span style="background-color: #313131;font-size: 3px;margin-top:5px;">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp;
&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</span>
<p style="font-size: 12px; color: #636363; margin-bottom: 30px;margin-top:5px;">
Cette interface affiche toutes les données permettant d'éditer les avis d'imposition
(nature d'impôt, montant de l'impôt, contribuable, etc).
</p>
</div>
<div class="pl-container">
<!-- ── En-tête ── -->
<div class="pl-header">
<div class="pl-header-left">
<span nz-icon nzType="dollar-circle" nzTheme="outline"
style="font-size:18px;color:#1f8653;"></span>
<div>
<div class="pl-title">Données d'impositions</div>
<div class="pl-count">{{ totalElements }} imposition(s) au total</div>
</div>
</div>
<div class="pl-header-right">
<button class="pl-btn-filter mr-1" (click)="exportPageCourante()">
<span nz-icon nzType="file-excel" nzTheme="outline"
style="margin-top:-5px;"></span>
Exporter en Excel
</button>
<button class="pl-btn-filter"
[class.pl-btn-filter-active]="filterVisible || filterApplied"
(click)="toggleFiltre()">
<span nz-icon nzType="filter" nzTheme="outline"
style="margin-top:-5px;"></span>
Filtres
<span class="pl-filter-badge" *ngIf="filterApplied"></span>
</button>
</div>
</div>
<!-- ── Panneau filtres ── -->
<div class="pl-filter-panel" [class.pl-filter-panel-open]="filterVisible">
<form [formGroup]="filterForm">
<div class="pl-filter-grid">
<!-- Identification parcelle -->
<div class="pl-filter-field">
<label class="pl-filter-label">NUP</label>
<input class="pl-filter-input" formControlName="nup"
placeholder="ex: NUP-001">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Titre Foncier</label>
<input class="pl-filter-input" formControlName="titreFoncier"
placeholder="ex: TF-2023">
</div>
<div class="pl-filter-field"
style="display:grid;grid-template-columns:1fr 1fr;gap:6px;">
<div style="width: 100px;">
<label class="pl-filter-label">Îlot</label>
<input style="width: 100px;" class="pl-filter-input"
formControlName="ilot" placeholder="ex: 101">
</div>
<div style="width: 100px;">
<label class="pl-filter-label">Parcelle</label>
<input style="width: 100px;" class="pl-filter-input"
formControlName="parcelle" placeholder="ex: A">
</div>
</div>
<!-- Propriétaire -->
<div class="pl-filter-field">
<label class="pl-filter-label">Nom propriétaire</label>
<input class="pl-filter-input" formControlName="nomProp"
placeholder="ex: CODO">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Prénom propriétaire</label>
<input class="pl-filter-input" formControlName="prenomProp"
placeholder="ex: MICHEL">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">IFU</label>
<input class="pl-filter-input" formControlName="ifu"
placeholder="ex: 011056">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">NPI</label>
<input class="pl-filter-input" formControlName="npi"
placeholder="ex: NPI-001">
</div>
<!-- Localisation -->
<div class="pl-filter-field">
<label class="pl-filter-label">Quartier / Village</label>
<input class="pl-filter-input" formControlName="nomQuartierVillage"
placeholder="ex: Bocossi">
</div>
<!-- Exercice & Nature -->
<div class="pl-filter-field">
<label class="pl-filter-label">Année</label>
<input class="pl-filter-input" type="number" formControlName="annee"
placeholder="ex: 2025">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Nature impôt</label>
<select class="pl-filter-input" formControlName="natureImpot">
<option value="">-- Tous --</option>
<option value="TFU">TFU</option>
<option value="TP">TP</option>
</select>
</div>
<!-- Bâtie / Exonérée -->
<div class="pl-filter-field">
<label class="pl-filter-label">Bâtie</label>
<select class="pl-filter-input" formControlName="batie">
<option [ngValue]="null">-- Toutes --</option>
<option [ngValue]="true">Oui</option>
<option [ngValue]="false">Non</option>
</select>
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Exonérée</label>
<select class="pl-filter-input" formControlName="exonere">
<option [ngValue]="null">-- Toutes --</option>
<option [ngValue]="true">Oui</option>
<option [ngValue]="false">Non</option>
</select>
</div>
</div>
<div class="pl-filter-actions">
<button class="pl-btn-reset" type="button" (click)="reinitialiserFiltre()">
<span nz-icon nzType="redo" nzTheme="outline"></span>
Réinitialiser
</button>
<button class="pl-btn-apply" type="button" (click)="appliquerFiltre()">
<span nz-icon nzType="search" nzTheme="outline"></span>
Appliquer
<span *ngIf="filterApplied" style="margin-left:4px;">
({{ filteredList.length }} résultat(s))
</span>
</button>
</div>
</form>
</div>
<!-- ── Loading ── -->
<div class="pl-loading" *ngIf="loading">
<nz-spin nzSimple></nz-spin>
<span>Chargement des impositions…</span>
</div>
<!-- ── Liste ── -->
<div *ngIf="!loading">
<div class="pl-empty" *ngIf="filteredList.length === 0">
<span nz-icon nzType="inbox" nzTheme="outline" style="font-size:32px;"></span>
<p>Aucune imposition trouvée</p>
</div>
<!-- Cards -->
<div class="pl-card" *ngFor="let item of pageCourante">
<div class="pl-card-main">
<!-- Identification -->
<div class="pl-col pl-col-identity">
<div class="pl-badges">
<span class="pl-badge pl-badge-qip">
Q{{ item.q }} . {{ item.ilot }} . {{ item.parcelle }}
</span>
<span class="pl-badge"
[style.background]="item.batie ? '#dcfce7' : '#fef3c7'"
[style.color]="item.batie ? '#166534' : '#92400e'">
{{ item.batie ? 'Bâtie' : 'Non bâtie' }}
</span>
<span class="pl-badge" *ngIf="item.exonere"
style="background:#f3e8ff;color:#7e22ce;">
Exonérée
</span>
<span class="pl-badge" style="background:#e0f2fe;color:#0369a1;"
*ngIf="item.natureImpot">
{{ item.natureImpot }}
</span>
</div>
<div class="pl-nup">
{{ item.nup || item.nupProvisoire || '— NUP non défini' }}
</div>
<div class="pl-info-line" *ngIf="item.titreFoncier">
<span nz-icon nzType="file-protect" nzTheme="outline"></span>
TF : {{ item.titreFoncier }}
</div>
<div class="pl-info-line">
<span nz-icon nzType="environment" nzTheme="outline"></span>
{{ item.nomQuartierVillage || '—' }}
{{ item.annee ? '— ' + item.annee : '' }}
</div>
</div>
<!-- Fiscalité -->
<div class="pl-col pl-col-domaine">
<div class="pl-domaine-type">
Taxe : {{ formatMontant(item.montantTaxe) }}
</div>
<div class="pl-domaine-nature">
Loyer annuel : {{ formatMontant(item.montantLoyerAnnuel) }}
</div>
<div class="pl-superficie" *ngIf="item.superficieParc">
<span nz-icon nzType="expand" nzTheme="outline"></span>
{{ item.superficieParc | number:'1.0-2':'fr' }} m²
</div>
<div class="pl-domaine-nature" *ngIf="item.zoneRfu?.nom">
Zone RFU : {{ item.zoneRfu?.nom }}
</div>
</div>
<!-- Propriétaire -->
<div class="pl-col pl-col-prop">
<div class="pl-prop-name">
{{ item.raisonSociale
|| ((item.nomProp || '') + ' ' + (item.prenomProp || ''))
|| '—' }}
</div>
<div class="pl-prop-sub" *ngIf="item.ifu">
IFU : {{ item.ifu.trim() }}
</div>
<div class="pl-prop-sub" *ngIf="item.npi">
NPI : {{ item.npi }}
</div>
<div class="pl-prop-sub" *ngIf="item.telProp">
<span nz-icon nzType="phone" nzTheme="outline"></span>
{{ item.telProp }}
</div>
</div>
<!-- Action -->
<div class="pl-col pl-col-action">
<button class="pl-btn-detail" (click)="toggleRow(item.id)">
<span nz-icon [nzType]="isExpanded(item.id) ? 'up' : 'down'"></span>
{{ isExpanded(item.id) ? 'Réduire' : 'Détails' }}
</button>
</div>
</div>
<!-- Détails expandés -->
<div class="pl-card-detail" *ngIf="isExpanded(item.id)">
<div class="pl-detail-grid">
<!-- Localisation -->
<div class="pl-detail-item">
<span class="pl-detail-label">Département</span>
<span class="pl-detail-val">
{{ item.codeDepartement }} — {{ item.nomDepartement || '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Commune</span>
<span class="pl-detail-val">
{{ item.codeCommune }} — {{ item.nomCommune || '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Arrondissement</span>
<span class="pl-detail-val">
{{ item.codeArrondissement }} —
{{ item.nomArrondissement || '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Quartier / Village</span>
<span class="pl-detail-val">
{{ item.codeQuartierVillage }} —
{{ item.nomQuartierVillage || '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Q . Îlot . Parcelle</span>
<span class="pl-detail-val">
{{ item.q }} . {{ item.ilot }} . {{ item.parcelle }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">NUP / NUP Provisoire</span>
<span class="pl-detail-val">
{{ item.nup || '—' }} / {{ item.nupProvisoire || '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Titre Foncier</span>
<span class="pl-detail-val">{{ item.titreFoncier || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Zone RFU</span>
<span class="pl-detail-val">{{ item.zoneRfu?.nom || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Longitude / Latitude</span>
<span class="pl-detail-val">
{{ item.longitude ? (item.longitude + ' / ' + item.latitude) : '—' }}
</span>
</div>
<!-- Propriétaire -->
<div class="pl-detail-item">
<span class="pl-detail-label">Propriétaire</span>
<span class="pl-detail-val">
{{ item.raisonSociale
|| ((item.nomProp || '') + ' ' + (item.prenomProp || ''))
|| '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">IFU / NPI</span>
<span class="pl-detail-val">
{{ item.ifu?.trim() || '—' }} / {{ item.npi || '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Tél. / Email prop.</span>
<span class="pl-detail-val">
{{ item.telProp || '—' }} / {{ item.emailProp || '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Adresse prop.</span>
<span class="pl-detail-val">{{ item.adresseProp || '—' }}</span>
</div>
<!-- Signataire -->
<div class="pl-detail-item" *ngIf="item.nomSc">
<span class="pl-detail-label">Signataire</span>
<span class="pl-detail-val">
{{ item.nomSc }} {{ item.prenomSc }}
</span>
</div>
<div class="pl-detail-item" *ngIf="item.telSc">
<span class="pl-detail-label">Tél. signataire</span>
<span class="pl-detail-val">{{ item.telSc }}</span>
</div>
<!-- Caractéristiques -->
<div class="pl-detail-item">
<span class="pl-detail-label">Bâtie</span>
<span class="pl-detail-val">
<span class="pl-badge"
[ngClass]="item.batie ? 'badge-success' : 'badge-warning'">
{{ item.batie ? 'OUI' : 'NON' }}
</span>
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Exonérée</span>
<span class="pl-detail-val">
<span class="pl-badge"
[ngClass]="item.exonere ? 'badge-danger' : 'badge-success'">
{{ item.exonere ? 'OUI' : 'NON' }}
</span>
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Bâtiment exonéré</span>
<span class="pl-detail-val">
<span class="pl-badge"
[ngClass]="item.batimentExonere ? 'badge-danger' : 'badge-success'">
{{ item.batimentExonere ? 'OUI' : 'NON' }}
</span>
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">U.L. exonérée</span>
<span class="pl-detail-val">
<span class="pl-badge"
[ngClass]="item.uniteLogementExonere ? 'badge-danger' : 'badge-success'">
{{ item.uniteLogementExonere ? 'OUI' : 'NON' }}
</span>
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Catégorie bâtiment</span>
<span class="pl-detail-val">{{ item.categorieBat || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Standing</span>
<span class="pl-detail-val">{{ item.standingBat || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Usage</span>
<span class="pl-detail-val">{{ item.categorieUsage || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">N° Bâtiment / N° U.L.</span>
<span class="pl-detail-val">
{{ item.numBatiment || '—' }} /
{{ item.numUniteLogement || '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Nbre bâtiments / U.L. /
Piscines</span>
<span class="pl-detail-val">
{{ item.nombreBat ?? '—' }} / {{ item.nombreUlog ?? '—' }} /
{{ item.nombrePiscine ?? '—' }}
</span>
</div>
<!-- Surfaces -->
<div class="pl-detail-item">
<span class="pl-detail-label">Sup. parcelle (m²)</span>
<span class="pl-detail-val">{{ item.superficieParc || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Sup. sol bâtiment (m²)</span>
<span
class="pl-detail-val">{{ item.superficieAuSolBat || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Sup. sol U.L. (m²)</span>
<span
class="pl-detail-val">{{ item.superficieAuSolUlog || '—' }}</span>
</div>
<!-- Valeurs fiscales -->
<div class="pl-detail-item">
<span class="pl-detail-label">Montant Taxe</span>
<span class="pl-detail-val accent">
{{ formatMontant(item.montantTaxe) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Taux TFU (%)</span>
<span class="pl-detail-val">{{ item.tauxTfu ?? '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">TFU Minimum</span>
<span
class="pl-detail-val">{{ formatMontant(item.tfuMinimum) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">TFU / m²</span>
<span
class="pl-detail-val">{{ formatMontant(item.tfuMetreCarre) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Loyer annuel</span>
<span
class="pl-detail-val">{{ formatMontant(item.montantLoyerAnnuel) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. Loc. Adm.</span>
<span
class="pl-detail-val">{{ formatMontant(item.valeurLocativeAdm) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. Loc. Adm. / m²</span>
<span class="pl-detail-val">
{{ formatMontant(item.valeurLocativeAdmMetreCarre) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Valeur bâtiment</span>
<span
class="pl-detail-val">{{ formatMontant(item.valeurBatiment) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Valeur parcelle</span>
<span
class="pl-detail-val">{{ formatMontant(item.valeurParcelle) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. Adm. Parc. NB</span>
<span
class="pl-detail-val">{{ formatMontant(item.valeurAdminParcelleNb) }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. Adm. Parc. NB / m²</span>
<span class="pl-detail-val">
{{ formatMontant(item.valeurAdminParcelleNbMetreCarre) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Date enquête</span>
<span class="pl-detail-val">
{{ (item.dateEnquete | date:'dd/MM/yyyy') || '—' }}
</span>
</div>
<!-- ── Champs réintégrés ── -->
<div class="pl-detail-item">
<span class="pl-detail-label">Zone RFU</span>
<span class="pl-detail-val">{{ item.zoneRfu?.nom || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. Loc. Adm. Taux Prop. Parc.</span>
<span class="pl-detail-val">
{{ formatMontant(item.valeurLocativeAdmTauxPropParc) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. Loc. Adm. Sup. Réelle</span>
<span class="pl-detail-val">
{{ formatMontant(item.valeurLocativeAdmSupReel) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Superficie Sol Taux Prop. Parc.
(m²)</span>
<span
class="pl-detail-val">{{ item.superficieAuSolTauxPropParc || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">TFU Calculé Taux Prop. Parc.</span>
<span class="pl-detail-val">
{{ formatMontant(item.tfuCalculeTauxPropParc) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">TFU Superficie Sol Réelle</span>
<span class="pl-detail-val">
{{ formatMontant(item.tfuSuperficieAuSolReel) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">TFU Piscine</span>
<span
class="pl-detail-val">{{ formatMontant(item.tfuPiscine) }}</span>
</div>
</div>
</div>
</div>
<!-- fin cards -->
<!-- Pagination -->
<div class="pl-pagination" *ngIf="totalElements > pageSize">
<span class="pl-pagination-info">
Page {{ pageNo + 1 }} sur {{ totalPages }}
— {{ totalElements }} imposition(s)
</span>
<nz-pagination [nzPageIndex]="pageNo + 1" [nzTotal]="totalElements"
[nzPageSize]="pageSize" [nzShowSizeChanger]="true"
[nzPageSizeOptions]="[10, 20, 50, 100]"
(nzPageIndexChange)="onPageChange($event)"
(nzPageSizeChange)="onPageSizeChange($event)">
</nz-pagination>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,538 @@
import { Component, SimpleChanges, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzTreeNodeOptions } from 'ng-zorro-antd/tree';
import { firstValueFrom } from 'rxjs';
import { CrudService } from 'src/app/crud.service';
import { ExcelExportService } from 'src/app/excel-export.service';
import { GlobalService } from 'src/app/global.service';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
export interface ImpositionPagedItem {
externalKey?: number;
enqueteExternalKey?: number;
id?: number;
annee?: number;
codeDepartement?: string;
nomDepartement?: string;
codeCommune?: string;
nomCommune?: string;
codeArrondissement?: string;
nomArrondissement?: string;
codeQuartierVillage?: string;
nomQuartierVillage?: string;
q?: string;
ilot?: string;
parcelle?: string;
nup?: string;
nupProvisoire?: string;
titreFoncier?: string;
numBatiment?: string;
numUniteLogement?: string;
ifu?: string;
npi?: string;
telProp?: string;
emailProp?: string;
nomProp?: string;
prenomProp?: string;
raisonSociale?: string;
adresseProp?: string;
telSc?: string;
emailSc?: string;
nomSc?: string;
prenomSc?: string;
adresseSc?: string;
longitude?: string;
latitude?: string;
superficieParc?: number;
superficieAuSolBat?: number;
superficieAuSolLoue?: number;
superficieAuSolUlog?: number;
batie?: boolean;
exonere?: boolean;
batimentExonere?: boolean;
uniteLogementExonere?: boolean;
valeurLocativeAdm?: number;
valeurLocativeAdmTauxPropParc?: number;
valeurLocativeAdmSupReel?: number;
superficieAuSolTauxPropParc?: number;
valeurLocativeAdmMetreCarre?: number;
montantLoyerAnnuel?: number;
tfuMetreCarre?: number;
tfuMinimum?: number;
standingBat?: string;
categorieUsage?: string;
categorieBat?: string;
nombrePiscine?: number;
nombreUlog?: number;
nombreBat?: number;
dateEnquete?: string;
enqueteId?: number;
secteurId?: number;
zoneRfu?: {
externalKey?: number;
enqueteExternalKey?: number;
id?: number;
code?: string;
nom?: string;
};
valeurAdminParcelleNb?: number;
tauxTfu?: number;
tfuPiscine?: number;
montantTaxe?: number;
tfuCalculeTauxPropParc?: number;
tfuSuperficieAuSolReel?: number;
valeurAdminParcelleNbMetreCarre?: number;
natureImpot?: string;
valeurBatiment?: number;
valeurParcelle?: number;
}
@Component({
selector: 'app-list-donnee-imposition-consultation',
templateUrl: './list-donnee-imposition-consultation.component.html',
styleUrls: ['./list-donnee-imposition-consultation.component.css']
})
export class ListDonneeImpositionConsultationComponent {
// ── Données ───────────────────────────────────────────
donnees: ImpositionPagedItem[] = [];
loading = false;
// ── Pagination client-side ────────────────────────────
pageNo = 0;
pageSize = 10;
// ── Filtre ────────────────────────────────────────────
filterForm!: FormGroup;
filterVisible = false;
filterApplied = false;
// ── Ligne expandée ────────────────────────────────────
expandedIds = new Set<number>();
// ── EXPORT LABELS ─────────────────────────────────────
private readonly EXPORT_LABELS: { [key: string]: string } = {
annee: 'Année',
natureImpot: 'Nature Impôt',
codeDepartement: 'Code Département',
nomDepartement: 'Département',
codeCommune: 'Code Commune',
nomCommune: 'Commune',
codeArrondissement: 'Code Arrondissement',
nomArrondissement: 'Arrondissement',
codeQuartierVillage: 'Code Quartier/Village',
nomQuartierVillage: 'Quartier/Village',
q: 'Q',
ilot: 'Îlot',
parcelle: 'Parcelle',
nup: 'NUP',
nupProvisoire: 'NUP Provisoire',
titreFoncier: 'Titre Foncier',
numBatiment: 'N° Bâtiment',
numUniteLogement: 'N° Unité Logement',
ifu: 'IFU',
npi: 'NPI',
telProp: 'Tél. Propriétaire',
emailProp: 'Email Propriétaire',
nomProp: 'Nom Propriétaire',
prenomProp: 'Prénom Propriétaire',
raisonSociale: 'Raison Sociale',
adresseProp: 'Adresse Propriétaire',
telSc: 'Tél. Signataire',
emailSc: 'Email Signataire',
nomSc: 'Nom Signataire',
prenomSc: 'Prénom Signataire',
adresseSc: 'Adresse Signataire',
longitude: 'Longitude',
latitude: 'Latitude',
superficieParc: 'Superficie Parcelle (m²)',
superficieAuSolBat: 'Superficie Sol Bâtiment (m²)',
superficieAuSolLoue: 'Superficie Sol Louée (m²)',
superficieAuSolUlog: 'Superficie Sol Unité Log. (m²)',
batie: 'Bâtie',
exonere: 'Exonérée',
batimentExonere: 'Bâtiment Exonéré',
uniteLogementExonere: 'Unité Logement Exonérée',
valeurLocativeAdm: 'Valeur Locative Adm. (FCFA)',
valeurLocativeAdmMetreCarre: 'Val. Loc. Adm. / m² (FCFA)',
montantLoyerAnnuel: 'Montant Loyer Annuel (FCFA)',
tfuMetreCarre: 'TFU / m² (FCFA)',
tfuMinimum: 'TFU Minimum (FCFA)',
montantTaxe: 'Montant Taxe (FCFA)',
standingBat: 'Standing Bâtiment',
categorieBat: 'Catégorie Bâtiment',
categorieUsage: 'Usage',
nombrePiscine: 'Nombre Piscines',
nombreUlog: 'Nombre Unités Logement',
nombreBat: 'Nombre Bâtiments',
dateEnquete: 'Date Enquête',
tauxTfu: 'Taux TFU (%)',
valeurAdminParcelleNb: 'Val. Adm. Parcelle Non Bâtie (FCFA)',
valeurAdminParcelleNbMetreCarre: 'Val. Adm. Parc. NB / m² (FCFA)',
valeurBatiment: 'Valeur Bâtiment (FCFA)',
valeurParcelle: 'Valeur Parcelle (FCFA)',
// ── Champs réintégrés ─────────────────────────────
zoneRfu: 'Zone RFU',
valeurLocativeAdmTauxPropParc: 'Val. Loc. Adm. Taux Prop. Parc. (FCFA)',
valeurLocativeAdmSupReel: 'Val. Loc. Adm. Sup. Réelle (FCFA)',
superficieAuSolTauxPropParc: 'Superficie Sol Taux Prop. Parc. (m²)',
tfuCalculeTauxPropParc: 'TFU Calculé Taux Prop. Parc. (FCFA)',
tfuSuperficieAuSolReel: 'TFU Superficie Sol Réelle (FCFA)',
tfuPiscine: 'TFU Piscine (FCFA)',
};
private readonly CHAMPS_EXCLUS = new Set([
'id',
'externalKey',
'enqueteExternalKey',
'enqueteId',
'secteurId',
// ── tous les autres champs réintégrés ── supprimés
]);
user: any = null;
numMenu = 2;
structureList: any[] = []; // Les noeuds de l'arbre
exerciceList: any[] = [];
structure: any = null;
exercice: any = null;
quartierSelected: any = null;
isActionInProgress = false;
nodes: NzTreeNodeOptions[] = []; // Les noeuds de l'arbre
arbreUtilisateurCourant: any[] = [];
constructor(
private fb: FormBuilder,
private tokenStorage: TokenStorage,
private router: Router,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
private globalService: GlobalService,
private modalService: NzModalService,
private viewContainerRef: ViewContainerRef,
private excelExportService: ExcelExportService,
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
ngOnInit(): void {
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
if (this.user) {
this.crudService.getAll('structure/all').subscribe(
(data: any) => {
this.structureList = data.object ? data.object : [];
});
this.crudService.getAll('secteur-decoupage/arbre/user-id/' + this.user?.id).subscribe(
(data: any) => {
this.arbreUtilisateurCourant = data.object ? data.object : [];
if (this.arbreUtilisateurCourant && this.arbreUtilisateurCourant.length > 0) {
this.constructionArbreUtilisateurs();
}
this.message.success(`Chargement des découpages de l'utilisateur ${this.user?.nom} réussi`);
},
(error: any) => {
this.message.error(`Chargement des découpages de l'utilisateur ${this.user?.nom} échoué`);
});
}
this.crudService.getAll('exercice/all').subscribe(
(data: any) => {
this.exerciceList = data.object ? data.object : [];
});
this.initFilterForm();
}
// ── Utilitaire ────────────────────────────────────────
constructionArbreUtilisateurs() {
const data: any[] = this.arbreUtilisateurCourant;
// Grouper par département
const deptMap = new Map<number, any>();
data.forEach(item => {
if (!deptMap.has(item.departementId)) {
deptMap.set(item.departementId, {
...item,
communes: new Map<number, any>()
});
}
const dept = deptMap.get(item.departementId);
if (!dept.communes.has(item.communeId)) {
dept.communes.set(item.communeId, {
...item,
arrondissements: new Map<number, any>()
});
}
const commune = dept.communes.get(item.communeId);
if (!commune.arrondissements.has(item.arrondissementId)) {
commune.arrondissements.set(item.arrondissementId, {
...item,
quartiers: []
});
}
const arr = commune.arrondissements.get(item.arrondissementId);
arr.quartiers.push(item);
});
// Construire les noeuds
this.nodes = Array.from(deptMap.values()).map(dept => ({
title: `${dept.departementCode}${dept.departementNom}`,
key: `dept-${dept.departementId}`,
icon: 'bank',
isLeaf: false,
expanded: false,
nbParcelles: dept.nbParcellesDepartement,
children: Array.from(dept.communes.values()).map((comm: any) => ({
title: `${comm.communeCode}${comm.communeNom}`,
key: `comm-${comm.communeId}`,
icon: 'home',
isLeaf: false,
expanded: false,
nbParcelles: comm.nbParcellesCommune,
children: Array.from(comm.arrondissements.values()).map((arr: any) => ({
title: arr.arrondissementNom,
key: `arr-${arr.arrondissementId}`,
icon: 'apartment',
isLeaf: false,
expanded: false,
nbParcelles: arr.nbParcellesArrondissement,
children: arr.quartiers.map((quart: any) => ({
title: quart.quartierNom,
key: `quart-${quart.quartierId}`,
icon: 'environment',
isLeaf: true,
nbParcelles: quart.nbParcellesQuartier,
quartier: quart
}))
}))
}))
}));
}
onNodeClick(event: any) {
const node = event.node;
if (node.isLeaf && node.key.startsWith('quart-')) {
this.quartierSelected = node.origin.quartier;
console.log(' this.quartierSelected ==>', this.quartierSelected);
this.message.create('success', `Quartier ${this.quartierSelected.quartierNom} sélectionné.`);
// votre logique de sélection...
this.charger();
}
}
getBadgeColor(niveau: string): string {
const colors: any = {
'dept': '#204e10',
'comm': '#10b981',
'arr': '#f59e0b',
'quart': '#ef6972'
};
return colors[niveau] ?? '#6b7280';
}
getNiveau(key: string): string {
if (key.startsWith('dept')) return 'dept';
if (key.startsWith('comm')) return 'comm';
if (key.startsWith('arr')) return 'arr';
if (key.startsWith('quart')) return 'quart';
return '';
}
// ── Init formulaire de filtre ─────────────────────────
initFilterForm(): void {
this.filterForm = this.fb.group({
nup: [null],
titreFoncier: [null],
ilot: [null],
parcelle: [null],
nomProp: [null],
prenomProp: [null],
raisonSociale: [null],
ifu: [null],
npi: [null],
nomQuartierVillage: [null],
annee: [null],
natureImpot: [null],
batie: [null],
exonere: [null],
});
}
compareFn = (o1: any, o2: any) => (o1 && o2 ? o1.id === o2.id : o1 === o2);
changeExercice(value: any): void {
this.exercice = value;
this.charger();
}
changeStructure(value: any): void {
console.log(' this.structure ', this.structure );
this.structure = value;
this.charger();
}
// ── Chargement ────────────────────────────────────────
charger(): void {
if (!this.quartierSelected || !this.exercice || !this.structure) return;
this.loading = true;
const url = `donnees-impositions-tfu/all/by-exercice-id/by-structure-id/by-quartier-id/${this.exercice?.id}/${this.structure?.id}/${this.quartierSelected?.quartierId}`;
this.crudService.getAll(url).subscribe({
next: (data: any) => {
this.donnees = data?.object ?? [];
this.pageNo = 0;
this.loading = false;
},
error: () => {
this.message.error('Erreur lors du chargement des impositions.');
this.loading = false;
}
});
}
// ── Filtre client-side ────────────────────────────────
get filteredList(): ImpositionPagedItem[] {
if (!this.filterApplied) return this.donnees;
const f = this.filterForm.value;
const match = (
filterVal: string | null | undefined,
itemVal: string | null | undefined
): boolean => {
if (!filterVal?.trim()) return true;
if (!itemVal?.trim()) return false;
return itemVal.toLowerCase().includes(filterVal.trim().toLowerCase());
};
const matchBool = (
filterVal: boolean | null | undefined,
itemVal: boolean | null | undefined
): boolean => {
if (filterVal === null || filterVal === undefined) return true;
return itemVal === filterVal;
};
return this.donnees.filter(item =>
match(f.nup, item.nup) &&
match(f.titreFoncier, item.titreFoncier) &&
match(f.ilot, item.ilot) &&
match(f.parcelle, item.parcelle) &&
(
match(f.nomProp, item.nomProp) ||
match(f.nomProp, item.raisonSociale)
) &&
match(f.prenomProp, item.prenomProp) &&
match(f.raisonSociale, item.raisonSociale) &&
match(f.ifu, item.ifu) &&
match(f.npi, item.npi) &&
match(f.nomQuartierVillage, item.nomQuartierVillage) &&
match(f.natureImpot, item.natureImpot) &&
(!f.annee || item.annee?.toString() === f.annee?.toString()) &&
matchBool(f.batie, item.batie) &&
matchBool(f.exonere, item.exonere)
);
}
get pageCourante(): ImpositionPagedItem[] {
const debut = this.pageNo * this.pageSize;
return this.filteredList?.slice(debut, debut + this.pageSize) || [];
}
get totalElements(): number { return this.filteredList.length; }
get totalPages(): number {
return this.pageSize > 0 ? Math.ceil(this.totalElements / this.pageSize) : 0;
}
onPageChange(page: number): void { this.pageNo = page - 1; }
onPageSizeChange(size: number): void {
this.pageSize = size;
this.pageNo = 0;
}
appliquerFiltre(): void {
this.filterApplied = true;
this.pageNo = 0;
}
reinitialiserFiltre(): void {
this.filterForm?.reset();
this.filterApplied = false;
this.pageNo = 0;
}
toggleFiltre(): void { this.filterVisible = !this.filterVisible; }
toggleRow(id: number | undefined): void {
if (id == null) return;
this.expandedIds.has(id) ? this.expandedIds.delete(id) : this.expandedIds.add(id);
}
isExpanded(id: number | undefined): boolean {
return id != null && this.expandedIds.has(id);
}
formatMontant(val: number | null | undefined): string {
if (val == null) return '—';
return new Intl.NumberFormat('fr-FR').format(val) + ' FCFA';
}
private nettoyerLigneExport(item: ImpositionPagedItem): { [label: string]: any } {
const ligne: { [label: string]: any } = {};
for (const key of Object.keys(this.EXPORT_LABELS)) {
if (this.CHAMPS_EXCLUS.has(key)) continue;
const label = this.EXPORT_LABELS[key];
// ── Cas spécial : zoneRfu → on n'exporte que le nom ──
if (key === 'zoneRfu') {
ligne[label] = (item.zoneRfu?.nom) || '—';
continue;
}
const val = (item as any)[key];
ligne[label] =
typeof val === 'boolean' ? (val ? 'OUI' : 'NON') :
val == null ? '—' : val;
}
return ligne;
}
exportPageCourante(): void {
if (!this.filteredList.length) {
this.message.warning('Aucune donnée à exporter.');
return;
}
const data = this.filteredList.map(item => this.nettoyerLigneExport(item));
this.excelExportService.exportAsExcelFile(data, 'impositions', 'Impositions');
}
}

View File

@@ -0,0 +1,413 @@
<div class="row">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review">
<h6 class="card-title" style="font-size: 16px!important;text-transform: none;">
Liste des parcelles <strong class="text-primary"> - (quartier sélectionné :
{{ quartierSelected ? quartierSelected.quartierNom : '-' }}) </strong>
</h6>
<nz-divider></nz-divider>
<div class="row">
<!-- ══ ARBRE ══════════════════════════════════════ -->
<div class="col-md-3" style="padding-right:0;">
<div class="arbre-container">
<div class="arbre-title">
<span nz-icon nzType="apartment" nzTheme="outline"></span>
Divisions administratives
</div>
<nz-tree [nzData]="nodes" nzShowLine nzShowIcon [nzTreeTemplate]="nzTreeTemplate"
(nzClick)="onNodeClick($event)" (nzExpandChange)="onNodeClick($event)">
</nz-tree>
<ng-template #nzTreeTemplate let-node>
<div class="tree-node-row">
<span class="tree-node-icon">
<span nz-icon
[nzType]="node.isLeaf ? 'environment' : (node.isExpanded ? 'folder-open' : 'folder')"
[style.color]="getBadgeColor(getNiveau(node.key))" nzTheme="outline">
</span>
</span>
<span class="tree-node-title" [class.tree-node-leaf]="node.isLeaf"
[class.tree-node-selected]="node.isLeaf && quartierSelected?.quartierId === node.origin?.quartier?.quartierId">
{{ node.title }}
</span>
<span class="tree-node-badge"
[style.background]="getBadgeColor(getNiveau(node.key))">
{{ node.origin?.nbParcelles | number:'1.0-0':'fr' }} p.
</span>
</div>
</ng-template>
<div *ngIf="nodes.length === 0" class="no-data">
<span nz-icon nzType="inbox" nzTheme="outline" style="font-size:28px;"></span>
<span>Aucun département trouvé</span>
</div>
</div>
</div>
<!-- fin arbre -->
<div class="col-md-9">
<div class="formulaire p-4"
style="width: 100%; border-radius: 5px; background: #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.08);">
<div>
<h2 style="font-size: 18px;margin-bottom: -10px;">Liste des parcelles
</h2>
<span style="background-color: #313131;font-size: 3px;margin-top:5px;">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp;
&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</span>
<p style="font-size: 12px; color: #636363; margin-bottom: 30px;margin-top:5px;">
Cette interface affiche toutes les informations sur les parcelles
enregistrées
(identification, références foncières).
</p>
</div>
<div class="pl-container">
<!-- ── En-tête ── -->
<div class="pl-header">
<div class="pl-header-left">
<span nz-icon nzType="home" nzTheme="outline"
style="font-size:18px;color:#1f8653;"></span>
<div>
<div class="pl-title">Parcelles du quartier</div>
<div class="pl-count">{{ totalElements }} parcelle(s) au total</div>
</div>
</div>
<div class="pl-header-right">
<button class="pl-btn-filter mr-1" (click)="exportPageCourante()">
<span style="margin-top: -5px;" nz-icon nzType="file"
nzTheme="outline"></span>
Exporter en excel
<span class="pl-filter-badge" *ngIf="filterApplied"></span>
</button>
<button class="pl-btn-filter"
[class.pl-btn-filter-active]="filterVisible || filterApplied"
(click)="toggleFiltre()">
<span style="margin-top: -5px;" nz-icon nzType="filter"
nzTheme="outline"></span>
Filtres
<span class="pl-filter-badge" *ngIf="filterApplied"></span>
</button>
</div>
</div>
<!-- ── Panneau de filtres ── -->
<div class="pl-filter-panel" [class.pl-filter-panel-open]="filterVisible">
<form [formGroup]="filterForm">
<div class="pl-filter-grid">
<!-- Q / I / P -->
<!--<div class="pl-filter-field">
<label class="pl-filter-label">Q (Quartier)</label>
<input class="pl-filter-input" formControlName="q"
placeholder="ex: 1120">
</div>-->
<div class="pl-filter-field"
style="display: grid;grid-template-columns: 1fr 1fr;">
<div style="width: 90px;">
<label class="pl-filter-label">I (Îlot)</label>
<input class="pl-filter-input" style="width: 90px;"
formControlName="i" placeholder="ex: 101">
</div>
<div style="width: 90px;">
<label class="pl-filter-label">P (Parcelle)</label>
<input class="pl-filter-input" style="width: 90px;"
formControlName="p" placeholder="ex: A">
</div>
</div>
<!-- NUP / TF -->
<div class="pl-filter-field">
<label class="pl-filter-label">NUP</label>
<input class="pl-filter-input" formControlName="nup"
placeholder="ex: NUP-001">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">N° Titre Foncier</label>
<input class="pl-filter-input" formControlName="numTitreFoncier"
placeholder="ex: TF-2023">
</div>
<!-- Propriétaire -->
<div class="pl-filter-field">
<label class="pl-filter-label">Nom propriétaire</label>
<input class="pl-filter-input" formControlName="proprietaireNom"
placeholder="ex: CODO">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Prénom propriétaire</label>
<input class="pl-filter-input" formControlName="proprietairePrenom"
placeholder="ex: MICHEL">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">IFU / NC propriétaire</label>
<input class="pl-filter-input" formControlName="proprietaireIfu"
placeholder="ex: 011056">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Type domaine</label>
<select class="pl-filter-input" formControlName="typeDomaineId"
(change)="getNatureDomaineList($event)">
<option *ngFor="let item of typeDomaineList" value="{{ item.id }}">
{{ item.libelle }} </option>
</select>
</div>
<!-- Domaine -->
<div class="pl-filter-field">
<label class="pl-filter-label">Nature domaine</label>
<select class="pl-filter-input" formControlName="natureDomaineId">
<option *ngFor="let item of natureDomaineFilteredList"
value="{{ item.id }}"> {{ item.libelle }} </option>
</select>
</div>
</div>
<div class="pl-filter-actions">
<button class="pl-btn-reset" type="button" (click)="reinitialiserFiltre()">
<span nz-icon nzType="redo" nzTheme="outline"></span>
Réinitialiser
</button>
<button class="pl-btn-apply" type="button" (click)="appliquerFiltre()">
<span nz-icon nzType="search" nzTheme="outline"></span>
Appliquer
<span *ngIf="filterApplied" style="margin-left:4px;">
({{ filteredList.length }} résultat(s))
</span>
</button>
</div>
</form>
</div>
<!-- ── Loading ── -->
<div class="pl-loading" *ngIf="loading">
<nz-spin nzSimple></nz-spin>
<span>Chargement des parcelles…</span>
</div>
<!-- ── Liste ── -->
<div *ngIf="!loading">
<!-- Aucune donnée -->
<div class="pl-empty" *ngIf="filteredList.length === 0">
<span nz-icon nzType="inbox" nzTheme="outline" style="font-size:32px;"></span>
<p>Aucune parcelle trouvée</p>
</div>
<!-- Cards -->
<div class="pl-card" *ngFor="let item of pageCourante">
<!-- Ligne principale -->
<div class="pl-card-main">
<!-- Colonne : Identification -->
<div class="pl-col pl-col-identity">
<div class="pl-badges">
<span class="pl-badge pl-badge-qip">
Q{{ item.q }} . {{ item.i }} . {{ item.p }}
</span>
<span class="pl-badge pl-badge-enquete"
*ngIf="item.enqueteCouranteId">
<span nz-icon nzType="file-search" nzTheme="outline"></span>
Enquêtée
</span>
<span class="pl-badge pl-badge-no-enquete"
*ngIf="!item.enqueteCouranteId">
Non enquêtée
</span>
</div>
<div class="pl-nup">
{{ item.nup || item.nupProvisoire || '— NUP non défini' }}
</div>
<div class="pl-info-line" *ngIf="item.numTitreFoncier">
<span nz-icon nzType="file-protect" nzTheme="outline"></span>
TF : {{ item.numTitreFoncier }}
</div>
<div class="pl-info-line">
<span nz-icon nzType="environment" nzTheme="outline"></span>
{{ item.quartierNom || '—' }}
</div>
</div>
<!-- Colonne : Domaine -->
<div class="pl-col pl-col-domaine">
<div class="pl-domaine-type">{{ item.typeDomaineLibelle || '—' }}</div>
<div class="pl-domaine-nature">{{ item.natureDomaineLibelle || '—' }}
</div>
<div class="pl-superficie" *ngIf="item.superficie">
<span nz-icon nzType="expand" nzTheme="outline"></span>
{{ item.superficie | number:'1.0-2':'fr' }} m²
</div>
</div>
<!-- Colonne : Propriétaire -->
<div class="pl-col pl-col-prop">
<div class="pl-prop-name">
{{ item.proprietaireRaisonSociale
|| ((item.proprietaireNom || '') + ' ' + (item.proprietairePrenom || ''))
|| '—' }}
</div>
<div class="pl-prop-sub" *ngIf="item.proprietaireIfu">
IFU : {{ item.proprietaireIfu.trim() }}
</div>
<div class="pl-prop-sub" *ngIf="item.proprietaireNpi">
NPI : {{ item.proprietaireNpi }}
</div>
<div class="pl-prop-sub" *ngIf="item.numEntreeParcelle">
<span nz-icon nzType="number" nzTheme="outline"></span>
Entrée : {{ item.numEntreeParcelle.trim() }}
</div>
</div>
<!-- Colonne : Action -->
<div class="pl-col pl-col-action">
<button class="pl-btn-detail" (click)="toggleRow(item.id)">
<span nz-icon [nzType]="isExpanded(item.id) ? 'up' : 'down'"></span>
{{ isExpanded(item.id) ? 'Réduire' : 'Détails' }}
</button>
</div>
</div>
<!-- fin pl-card-main -->
<!-- Détails expandés -->
<div class="pl-card-detail" *ngIf="isExpanded(item.id)">
<div class="pl-detail-grid">
<!--<div class="pl-detail-item">
<span class="pl-detail-label">ID Parcelle</span>
<span class="pl-detail-val">{{ item.id }}</span>
</div>-->
<div class="pl-detail-item">
<span class="pl-detail-label">Q . I . P</span>
<span class="pl-detail-val">{{ item.q }} . {{ item.i }} .
{{ item.p }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">NUP</span>
<span class="pl-detail-val">{{ item.nup || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">NUP Provisoire</span>
<span class="pl-detail-val">{{ item.nupProvisoire || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">N° Titre Foncier</span>
<span class="pl-detail-val">{{ item.numTitreFoncier || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Superficie (m²)</span>
<span class="pl-detail-val">{{ item.superficie || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Quartier</span>
<span class="pl-detail-val">{{ item.quartierCode }} —
{{ item.quartierNom }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Rue</span>
<span class="pl-detail-val">
{{ item.rueNom ? (item.rueNumero ? 'N°' + item.rueNumero + ' ' : '') + item.rueNom : '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">N° Entrée</span>
<span
class="pl-detail-val">{{ item.numEntreeParcelle?.trim() || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Type Domaine</span>
<span
class="pl-detail-val">{{ item.typeDomaineLibelle || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Nature Domaine</span>
<span
class="pl-detail-val">{{ item.natureDomaineLibelle || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Longitude / Latitude</span>
<span class="pl-detail-val">
{{ item.longitude ? (item.longitude + ' / ' + item.latitude) : '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Propriétaire</span>
<span class="pl-detail-val">
{{ item.proprietaireRaisonSociale
|| ((item.proprietaireNom || '') + ' ' + (item.proprietairePrenom || ''))
|| '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">IFU</span>
<span
class="pl-detail-val">{{ item.proprietaireIfu?.trim() || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">NPI</span>
<span class="pl-detail-val">{{ item.proprietaireNpi || '—' }}</span>
</div>
<div class="pl-detail-item" *ngIf="item.observation">
<span class="pl-detail-label">Observation</span>
<span class="pl-detail-val">{{ item.observation }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-val">
<button class="btn-action btn-action-person"
(click)="afficherDetail(item.id)">
<i class="mdi mdi-arrow-right"></i> Plus de détails
</button>
</span>
</div>
</div>
</div>
</div>
<!-- fin cards -->
<!-- ── Pagination ── -->
<div class="pl-pagination" *ngIf="totalElements > pageSize">
<span class="pl-pagination-info">
Page {{ pageNo + 1 }} sur {{ totalPages }} — {{ totalElements }} parcelle(s)
</span>
<nz-pagination [nzPageIndex]="pageNo + 1" [nzTotal]="totalElements"
[nzPageSize]="pageSize" [nzShowSizeChanger]="true"
[nzPageSizeOptions]="[10, 20, 50, 100]"
(nzPageIndexChange)="onPageChange($event)"
(nzPageSizeChange)="onPageSizeChange($event)">
</nz-pagination>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,517 @@
import { Component, SimpleChanges, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzTreeNodeOptions } from 'ng-zorro-antd/tree';
import { firstValueFrom } from 'rxjs';
import { CrudService } from 'src/app/crud.service';
import { ExcelExportService } from 'src/app/excel-export.service';
import { GlobalService } from 'src/app/global.service';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
// parcelle-paged.model.ts
export interface ParcellePagedItem {
id?: number;
q?: string;
i?: string;
p?: string;
nup?: string;
nupProvisoire?: string;
numTitreFoncier?: string;
longitude?: string;
latitude?: string;
altitude?: string;
superficie?: number;
observation?: string;
situationGeographique?: string;
numEntreeParcelle?: string;
quartierId?: number;
quartierCode?: string;
quartierNom?: string;
natureDomaineId?: string;
natureDomaineLibelle?: string;
typeDomaineId?: string;
typeDomaineLibelle?: string;
rueId?: number;
rueNumero?: string;
rueNom?: string;
proprietaireId?: number;
proprietaireIfu?: string;
proprietaireNpi?: string;
proprietaireNom?: string;
proprietairePrenom?: string;
proprietaireRaisonSociale?: string;
enqueteCouranteId?: number;
}
@Component({
selector: 'app-list-parcelle-consultation',
templateUrl: './list-parcelle-consultation.component.html',
styleUrls: ['./list-parcelle-consultation.component.css']
})
export class ListParcelleConsultationComponent {
user: any = null;
numMenu = 2;
nodes: NzTreeNodeOptions[] = []; // Les noeuds de l'arbre
arbreUtilisateurCourant: any[] = [];
quartierSelected: any = null;
isActionInProgress = false;
// ── Données ───────────────────────────────────────────
parcelleList: ParcellePagedItem[] = [];
loading = false;
// ── Pagination ────────────────────────────────────────
// ── Pagination client-side ────────────────────────────
pageNo = 0;
pageSize = 10;
// ── Données brutes complètes (chargées une seule fois) ──
donnees: ParcellePagedItem[] = [];
// ── Filtre ────────────────────────────────────────────
filterForm!: FormGroup;
filterVisible = false;
filterApplied = false;
// ── Ligne expandée ────────────────────────────────────
expandedIds = new Set<number>();
natureDomaineFilteredList: any[] = [];
natureDomaineList: any[] = [];
typeDomaineList: any[] = [];
// ── Mapping des labels français pour l'export ─────────────
private readonly EXPORT_LABELS: { [key: string]: string } = {
// ── Identifiant ───────────────────────────────────────
id: 'ID Parcelle',
// ── Identification parcellaire ────────────────────────
q: 'Q (Quartier)',
i: 'I (Îlot)',
p: 'P (Parcelle)',
nup: 'NUP',
nupProvisoire: 'NUP Provisoire',
numTitreFoncier: 'N° Titre Foncier',
// ── GPS ───────────────────────────────────────────────
longitude: 'Longitude',
latitude: 'Latitude',
altitude: 'Altitude (m)',
// ── Caractéristiques ──────────────────────────────────
superficie: 'Superficie (m²)',
observation: 'Observation',
situationGeographique: 'Situation Géographique',
numEntreeParcelle: 'N° Entrée Parcelle',
// ── Quartier ──────────────────────────────────────────
quartierId: 'ID Quartier',
quartierCode: 'Code Quartier',
quartierNom: 'Quartier',
// ── Nature domaine ────────────────────────────────────
natureDomaineId: 'ID Nature Domaine',
natureDomaineLibelle: 'Nature Domaine',
// ── Type domaine ──────────────────────────────────────
typeDomaineId: 'ID Type Domaine',
typeDomaineLibelle: 'Type Domaine',
// ── Rue ───────────────────────────────────────────────
rueId: 'ID Rue',
rueNumero: 'N° Rue',
rueNom: 'Nom Rue',
// ── Propriétaire ──────────────────────────────────────
proprietaireId: 'ID Propriétaire',
proprietaireIfu: 'IFU Propriétaire',
proprietaireNpi: 'NPI Propriétaire',
proprietaireNom: 'Nom Propriétaire',
proprietairePrenom: 'Prénom Propriétaire',
proprietaireRaisonSociale: 'Raison Sociale Propriétaire',
// ── Enquête ───────────────────────────────────────────
enqueteCouranteId: 'ID Enquête Courante',
};
// ── Champs à exclure de l'export ──────────────────────────
private readonly CHAMPS_EXCLUS = new Set([
'id',
'quartierId',
'natureDomaineId',
'typeDomaineId',
'rueId',
'proprietaireId',
'enqueteCouranteId',
]);
constructor(
private fb: FormBuilder,
private tokenStorage: TokenStorage,
private router: Router,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
private globalService: GlobalService,
private modalService: NzModalService,
private viewContainerRef: ViewContainerRef,
private excelExportService: ExcelExportService,
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
ngOnInit(): void {
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
if (this.user) {
this.crudService.getAll('secteur-decoupage/arbre/user-id/' + this.user?.id).subscribe(
(data: any) => {
this.arbreUtilisateurCourant = data.object ? data.object : [];
if (this.arbreUtilisateurCourant && this.arbreUtilisateurCourant.length > 0) {
this.constructionArbreUtilisateurs();
}
this.message.success(`Chargement des découpages de l'utilisateur ${this.user?.nom} réussi`);
},
(error: any) => {
this.message.error(`Chargement des découpages de l'utilisateur ${this.user?.nom} échoué`);
});
}
this.initFilterForm();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['quartierId'] && this.quartierSelected?.quartierId) {
this.pageNo = 0;
this.reinitialiserFiltre();
this.charger();
}
}
getNatureDomaineList(value: any): void {
//console.log('value type domaine', value);
this.natureDomaineFilteredList = [];
if (value) {
this.natureDomaineFilteredList = this.natureDomaineList.filter((el) => el.typeDomaine?.id == value.target.value);
} else {
this.natureDomaineFilteredList = [];
}
}
// ── Init formulaire de filtre ─────────────────────────
initFilterForm(): void {
this.crudService.getAll('nature-domaine/all').subscribe(
(data: any) => {
this.natureDomaineList = data.object ? data.object : [];
});
this.crudService.getAll('type-domaine/all').subscribe(
(data: any) => {
this.typeDomaineList = data.object ? data.object : [];
});
this.filterForm = this.fb.group({
q: [null],
i: [null],
p: [null],
nup: [null],
numTitreFoncier: [null],
proprietaireNom: [null],
proprietairePrenom: [null],
proprietaireIfu: [null],
natureDomaineId: [null],
typeDomaineId: [null],
});
}
// ── Chargement paginé ─────────────────────────────────
charger(): void {
if (!this.quartierSelected) return;
this.loading = true;
const url = `parcelle/all/by-quartier-id/${this.quartierSelected?.quartierId}`;
this.crudService.getAll(url).subscribe({
next: (data: any) => {
this.donnees = data?.object ?? [];
this.pageNo = 0;
this.loading = false;
},
error: () => {
this.message.error('Erreur lors du chargement des parcelles.');
this.loading = false;
}
});
}
get filteredList(): ParcellePagedItem[] {
if (!this.filterApplied) {
return this.donnees;
}
const f = this.filterForm.value;
// Fonction de match plus stricte et claire
const match = (filterValue: string | null | undefined, itemValue: string | null | undefined): boolean => {
// Si le filtre est vide → on accepte (on ne filtre pas sur ce critère)
if (!filterValue?.trim()) {
return true;
}
// Si le champ de la donnée est vide/null → on refuse (le filtre demande quelque chose)
if (!itemValue?.trim()) {
return false;
}
// Recherche insensible à la casse + trim
return itemValue.toLowerCase().includes(filterValue.trim().toLowerCase());
};
return this.donnees.filter(p => {
return (
match(f.q, p.q) &&
match(f.i, p.i) &&
match(f.p, p.p) &&
match(f.nup, p.nup) &&
match(f.numTitreFoncier, p.numTitreFoncier) &&
// Propriétaire : nom OU raison sociale
(match(f.proprietaireNom, p.proprietaireNom) ||
match(f.proprietaireNom, p.proprietaireRaisonSociale)) &&
match(f.proprietairePrenom, p.proprietairePrenom) &&
match(f.proprietaireIfu, p.proprietaireIfu) &&
match(f.natureDomaineId, p.natureDomaineId?.toString()) && // ← souvent number → toString()
match(f.typeDomaineId, p.typeDomaineId?.toString())
);
});
}
// ── Éléments de la page courante ─────────────────────────
get pageCourante(): ParcellePagedItem[] {
const debut = this.pageNo * this.pageSize;
const fin = debut + this.pageSize;
return this.filteredList.slice(debut, fin);
}
// ── Total des éléments filtrés ───────────────────────────
get totalElements(): number {
return this.filteredList.length;
}
// ── Total des éléments filtrés ───────────────────────────
get totalPages(): number {
const count = this.filteredList?.length ?? 0;
const size = this.pageSize ?? 10; // valeur par défaut si pageSize n'est pas défini
if (size <= 0 || count === 0) {
return 0;
}
return Math.ceil(count / size);
}
// ── Pagination ───────────────────────────────────────────
onPageChange(page: number): void {
this.pageNo = page - 1;
// ← pas d'appel API, on retranche simplement la page
}
onPageSizeChange(size: number): void {
this.pageSize = size;
this.pageNo = 0;
// ← pas d'appel API
}
// ── Filtre : reset la page à 0 à chaque application ──────
appliquerFiltre(): void {
this.filterApplied = true;
this.pageNo = 0; // ← retour page 1 après filtre
}
reinitialiserFiltre(): void {
this.filterForm?.reset();
this.filterApplied = false;
this.pageNo = 0;
}
toggleFiltre(): void {
this.filterVisible = !this.filterVisible;
}
// ── Expand ligne ──────────────────────────────────────
toggleRow(id: number | undefined): void {
if (id == null) return;
if (this.expandedIds.has(id)) {
this.expandedIds.delete(id);
} else {
this.expandedIds.add(id);
}
}
isExpanded(id: number | undefined): boolean {
return id != null && this.expandedIds.has(id);
}
// ── Utilitaire ────────────────────────────────────────
formatMontant(val: number | null | undefined): string {
if (val == null) return '—';
return new Intl.NumberFormat('fr-FR').format(val) + ' FCFA';
}
constructionArbreUtilisateurs() {
const data: any[] = this.arbreUtilisateurCourant;
// Grouper par département
const deptMap = new Map<number, any>();
data.forEach(item => {
if (!deptMap.has(item.departementId)) {
deptMap.set(item.departementId, {
...item,
communes: new Map<number, any>()
});
}
const dept = deptMap.get(item.departementId);
if (!dept.communes.has(item.communeId)) {
dept.communes.set(item.communeId, {
...item,
arrondissements: new Map<number, any>()
});
}
const commune = dept.communes.get(item.communeId);
if (!commune.arrondissements.has(item.arrondissementId)) {
commune.arrondissements.set(item.arrondissementId, {
...item,
quartiers: []
});
}
const arr = commune.arrondissements.get(item.arrondissementId);
arr.quartiers.push(item);
});
// Construire les noeuds
this.nodes = Array.from(deptMap.values()).map(dept => ({
title: `${dept.departementCode}${dept.departementNom}`,
key: `dept-${dept.departementId}`,
icon: 'bank',
isLeaf: false,
expanded: false,
nbParcelles: dept.nbParcellesDepartement,
children: Array.from(dept.communes.values()).map((comm: any) => ({
title: `${comm.communeCode}${comm.communeNom}`,
key: `comm-${comm.communeId}`,
icon: 'home',
isLeaf: false,
expanded: false,
nbParcelles: comm.nbParcellesCommune,
children: Array.from(comm.arrondissements.values()).map((arr: any) => ({
title: arr.arrondissementNom,
key: `arr-${arr.arrondissementId}`,
icon: 'apartment',
isLeaf: false,
expanded: false,
nbParcelles: arr.nbParcellesArrondissement,
children: arr.quartiers.map((quart: any) => ({
title: quart.quartierNom,
key: `quart-${quart.quartierId}`,
icon: 'environment',
isLeaf: true,
nbParcelles: quart.nbParcellesQuartier,
quartier: quart
}))
}))
}))
}));
}
onNodeClick(event: any) {
const node = event.node;
if (node.isLeaf && node.key.startsWith('quart-')) {
this.quartierSelected = node.origin.quartier;
console.log(' this.quartierSelected ==>', this.quartierSelected);
this.message.create('success', `Quartier ${this.quartierSelected.quartierNom} sélectionné.`);
// votre logique de sélection...
this.charger();
}
}
getBadgeColor(niveau: string): string {
const colors: any = {
'dept': '#204e10',
'comm': '#10b981',
'arr': '#f59e0b',
'quart': '#ef6972'
};
return colors[niveau] ?? '#6b7280';
}
getNiveau(key: string): string {
if (key.startsWith('dept')) return 'dept';
if (key.startsWith('comm')) return 'comm';
if (key.startsWith('arr')) return 'arr';
if (key.startsWith('quart')) return 'quart';
return '';
}
// ── Nettoyage et formatage d'une ligne ───────────────────
private nettoyerLigneExport(item: any): { [label: string]: any } {
const ligne: { [label: string]: any } = {};
for (const key of Object.keys(this.EXPORT_LABELS)) {
if (this.CHAMPS_EXCLUS.has(key)) continue;
const label = this.EXPORT_LABELS[key];
const val = item[key];
// Booléens → OUI / NON
if (typeof val === 'boolean') {
ligne[label] = val ? 'OUI' : 'NON';
continue;
}
// Null / undefined → tiret
if (val === null || val === undefined) {
ligne[label] = '—';
continue;
}
ligne[label] = val;
}
return ligne;
}
// ── Export de la page courante ────────────────────────────
exportPageCourante(): void {
if (!this.filteredList || this.filteredList.length === 0) {
this.message.warning('Aucune donnée à exporter.');
return;
}
const data = this.filteredList.map(item => this.nettoyerLigneExport(item));
this.excelExportService.exportAsExcelFile(
data,
`parcelles`,
'Parcelles'
);
}
afficherDetail(id: any): void {
this.router.navigate(['/core/consultation/detail-parcelle/'+ id]);
}
}

View File

@@ -0,0 +1,420 @@
<div class="row">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review">
<h6 class="card-title" style="font-size: 16px!important;text-transform: none;">
Liste des unités de logements <strong class="text-primary"> - (quartier sélectionné :
{{ quartierSelected ? quartierSelected.quartierNom : '-' }}) </strong>
</h6>
<nz-divider></nz-divider>
<div class="row">
<!-- ══ ARBRE ══════════════════════════════════════ -->
<div class="col-md-3" style="padding-right:0;">
<div class="arbre-container">
<div class="arbre-title">
<span nz-icon nzType="apartment" nzTheme="outline"></span>
Divisions administratives
</div>
<nz-tree [nzData]="nodes" nzShowLine nzShowIcon [nzTreeTemplate]="nzTreeTemplate"
(nzClick)="onNodeClick($event)" (nzExpandChange)="onNodeClick($event)">
</nz-tree>
<ng-template #nzTreeTemplate let-node>
<div class="tree-node-row">
<span class="tree-node-icon">
<span nz-icon
[nzType]="node.isLeaf ? 'environment' : (node.isExpanded ? 'folder-open' : 'folder')"
[style.color]="getBadgeColor(getNiveau(node.key))" nzTheme="outline">
</span>
</span>
<span class="tree-node-title" [class.tree-node-leaf]="node.isLeaf"
[class.tree-node-selected]="node.isLeaf && quartierSelected?.quartierId === node.origin?.quartier?.quartierId">
{{ node.title }}
</span>
<!--<span class="tree-node-badge"
[style.background]="getBadgeColor(getNiveau(node.key))">
{{ node.origin?.nbParcelles | number:'1.0-0':'fr' }} p.
</span>-->
</div>
</ng-template>
<div *ngIf="nodes.length === 0" class="no-data">
<span nz-icon nzType="inbox" nzTheme="outline" style="font-size:28px;"></span>
<span>Aucun département trouvé</span>
</div>
</div>
</div>
<!-- fin arbre -->
<div class="col-md-9">
<div class="formulaire p-4"
style="width: 100%; border-radius: 5px; background: #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.08);">
<div>
<h2 style="font-size: 18px;margin-bottom: -10px;">Liste des unités de logement
</h2>
<span style="background-color: #313131;font-size: 3px;margin-top:5px;">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp;
&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</span>
<p style="font-size: 12px; color: #636363; margin-bottom: 30px;margin-top:5px;">
Cette interface affiche toutes les informations sur les unités de logement
enregistrées (identification, références foncières, catégorie, usage).
</p>
</div>
<div class="pl-container">
<!-- ── En-tête ── -->
<div class="pl-header">
<div class="pl-header-left">
<span nz-icon nzType="apartment" nzTheme="outline"
style="font-size:18px;color:#1f8653;"></span>
<div>
<div class="pl-title">Unités de logement du quartier</div>
<div class="pl-count">{{ totalElements }} unité(s) au total</div>
</div>
</div>
<div class="pl-header-right">
<button class="pl-btn-filter mr-1" (click)="exportPageCourante()">
<span nz-icon nzType="file-excel" nzTheme="outline"
style="margin-top:-5px;"></span>
Exporter en Excel
</button>
<button class="pl-btn-filter"
[class.pl-btn-filter-active]="filterVisible || filterApplied"
(click)="toggleFiltre()">
<span nz-icon nzType="filter" nzTheme="outline"
style="margin-top:-5px;"></span>
Filtres
<span class="pl-filter-badge" *ngIf="filterApplied"></span>
</button>
</div>
</div>
<!-- ── Panneau filtres ── -->
<div class="pl-filter-panel" [class.pl-filter-panel-open]="filterVisible">
<form [formGroup]="filterForm">
<div class="pl-filter-grid">
<!-- NUL / Code / Étage -->
<div class="pl-filter-field">
<label class="pl-filter-label">NUL</label>
<input class="pl-filter-input" formControlName="nul"
placeholder="ex: UL-001">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Code unité</label>
<input class="pl-filter-input" formControlName="code"
placeholder="ex: CODE-001">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Numéro d'étage</label>
<input class="pl-filter-input" formControlName="numeroEtage"
placeholder="ex: 2">
</div>
<!-- NUB Bâtiment -->
<div class="pl-filter-field">
<label class="pl-filter-label">NUB Bâtiment</label>
<input class="pl-filter-input" formControlName="batimentNub"
placeholder="ex: B01">
</div>
<!-- Propriétaire -->
<div class="pl-filter-field">
<label class="pl-filter-label">Nom propriétaire</label>
<input class="pl-filter-input" formControlName="personneNom"
placeholder="ex: CODO">
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Prénom propriétaire</label>
<input class="pl-filter-input" formControlName="personnePrenom"
placeholder="ex: MICHEL">
</div>
<!-- Catégorie & Usage -->
<div class="pl-filter-field">
<label class="pl-filter-label">Catégorie bâtiment</label>
<select class="pl-filter-input" formControlName="categorieBatimentId">
<option value="">-- Toutes --</option>
<option *ngFor="let cat of categorieBatimentList" [value]="cat.id">
{{ cat.code }} — {{ cat.standing }}
</option>
</select>
</div>
<div class="pl-filter-field">
<label class="pl-filter-label">Usage</label>
<select class="pl-filter-input" formControlName="usageId">
<option value="">-- Tous --</option>
<option *ngFor="let usage of usageList" [value]="usage.id">
{{ usage.nom }}
</option>
</select>
</div>
</div>
<div class="pl-filter-actions">
<button class="pl-btn-reset" type="button" (click)="reinitialiserFiltre()">
<span nz-icon nzType="redo" nzTheme="outline"></span>
Réinitialiser
</button>
<button class="pl-btn-apply" type="button" (click)="appliquerFiltre()">
<span nz-icon nzType="search" nzTheme="outline"></span>
Appliquer
<span *ngIf="filterApplied" style="margin-left:4px;">
({{ filteredList.length }} résultat(s))
</span>
</button>
</div>
</form>
</div>
<!-- ── Loading ── -->
<div class="pl-loading" *ngIf="loading">
<nz-spin nzSimple></nz-spin>
<span>Chargement des unités de logement…</span>
</div>
<!-- ── Liste ── -->
<div *ngIf="!loading">
<div class="pl-empty" *ngIf="filteredList.length === 0">
<span nz-icon nzType="inbox" nzTheme="outline" style="font-size:32px;"></span>
<p>Aucune unité de logement trouvée</p>
</div>
<!-- Cards -->
<div class="pl-card" *ngFor="let item of pageCourante">
<div class="pl-card-main">
<!-- Identification -->
<div class="pl-col pl-col-identity">
<div class="pl-badges">
<span class="pl-badge pl-badge-qip">
NUL : {{ item.nul || '—' }}
</span>
<span class="pl-badge" *ngIf="item.categorieBatimentCode"
style="background:#e0f2fe;color:#0369a1;">
{{ item.categorieBatimentCode }}
{{ item.categorieBatimentStanding ? '— ' + item.categorieBatimentStanding : '' }}
</span>
<span class="pl-badge pl-badge-enquete"
*ngIf="item.enqueteUniteLogementCourantId">
<span nz-icon nzType="file-search" nzTheme="outline"></span>
Enquêtée
</span>
<span class="pl-badge pl-badge-no-enquete"
*ngIf="!item.enqueteUniteLogementCourantId">
Non enquêtée
</span>
</div>
<div class="pl-nup">
Code : {{ item.code || '—' }}
— Étage : {{ item.numeroEtage || '0' }}
</div>
<div class="pl-info-line" *ngIf="item.batimentNub">
<span nz-icon nzType="bank" nzTheme="outline"></span>
Bâtiment NUB : {{ item.batimentNub }}
</div>
<div class="pl-info-line" *ngIf="item.dateConstruction">
<span nz-icon nzType="calendar" nzTheme="outline"></span>
Construit le : {{ item.dateConstruction | date:'dd/MM/yyyy' }}
</div>
</div>
<!-- Usage & Surfaces -->
<div class="pl-col pl-col-domaine">
<div class="pl-domaine-type">{{ item.usageNom || '—' }}</div>
<div class="pl-superficie" *ngIf="item.superficieAuSol">
<span nz-icon nzType="expand" nzTheme="outline"></span>
{{ item.superficieAuSol | number:'1.0-2':'fr' }} m² au sol
</div>
<div class="pl-superficie" *ngIf="item.superficieLouee">
<span nz-icon nzType="shrink" nzTheme="outline"></span>
{{ item.superficieLouee | number:'1.0-2':'fr' }} m² loués
</div>
<div class="pl-superficie" *ngIf="item.nombrePiscine">
🏊 {{ item.nombrePiscine }} piscine(s)
</div>
</div>
<!-- Propriétaire -->
<div class="pl-col pl-col-prop">
<div class="pl-prop-name">
{{ item.personneRaisonSociale
|| ((item.personneNom || '') + ' ' + (item.personnePrenom || ''))
|| '—' }}
</div>
<div class="pl-prop-sub" *ngIf="item.valeurUniteLogementEstime">
Val. estimée : {{ formatMontant(item.valeurUniteLogementEstime) }}
</div>
<div class="pl-prop-sub" *ngIf="item.montantMensuelLocation">
Loyer mensuel : {{ formatMontant(item.montantMensuelLocation) }}
</div>
</div>
<!-- Action -->
<div class="pl-col pl-col-action">
<button class="pl-btn-detail" (click)="toggleRow(item.id)">
<span nz-icon [nzType]="isExpanded(item.id) ? 'up' : 'down'"></span>
{{ isExpanded(item.id) ? 'Réduire' : 'Détails' }}
</button>
</div>
</div>
<!-- Détails expandés -->
<div class="pl-card-detail" *ngIf="isExpanded(item.id)">
<div class="pl-detail-grid">
<div class="pl-detail-item">
<span class="pl-detail-label">NUL</span>
<span class="pl-detail-val">{{ item.nul || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Code</span>
<span class="pl-detail-val">{{ item.code || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Numéro d'étage</span>
<span class="pl-detail-val">{{ item.numeroEtage || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">NUB Bâtiment</span>
<span class="pl-detail-val">{{ item.batimentNub || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Date construction</span>
<span class="pl-detail-val">
{{ (item.dateConstruction | date:'dd/MM/yyyy') || '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Catégorie / Standing</span>
<span class="pl-detail-val">
{{ item.categorieBatimentCode || '—' }}
{{ item.categorieBatimentStanding ? '— ' + item.categorieBatimentStanding : '' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Usage</span>
<span class="pl-detail-val">{{ item.usageNom || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Superficie au sol (m²)</span>
<span class="pl-detail-val">{{ item.superficieAuSol || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Superficie louée (m²)</span>
<span class="pl-detail-val">{{ item.superficieLouee || '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Nombre piscines</span>
<span class="pl-detail-val">{{ item.nombrePiscine ?? '—' }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Propriétaire</span>
<span class="pl-detail-val">
{{ item.personneRaisonSociale
|| ((item.personneNom || '') + ' ' + (item.personnePrenom || ''))
|| '—' }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Loyer mensuel</span>
<span class="pl-detail-val">
{{ formatMontant(item.montantMensuelLocation) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Loyer annuel déclaré</span>
<span class="pl-detail-val">
{{ formatMontant(item.montantLocatifAnnuelDeclare) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Loyer annuel calculé</span>
<span class="pl-detail-val">
{{ formatMontant(item.montantLocatifAnnuelCalcule) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Loyer annuel estimé</span>
<span class="pl-detail-val">
{{ formatMontant(item.montantLocatifAnnuelEstime) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. estimée U.L.</span>
<span class="pl-detail-val">
{{ formatMontant(item.valeurUniteLogementEstime) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. réelle U.L.</span>
<span class="pl-detail-val">
{{ formatMontant(item.valeurUniteLogementReel) }}
</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-label">Val. calculée U.L.</span>
<span class="pl-detail-val">
{{ formatMontant(item.valeurUniteLogementCalcule) }}
</span>
</div>
<div class="pl-detail-item" *ngIf="item.observation">
<span class="pl-detail-label">Observation</span>
<span class="pl-detail-val">{{ item.observation }}</span>
</div>
<div class="pl-detail-item">
<span class="pl-detail-val">
<button class="btn-action btn-action-person"
(click)="afficherDetail(item.id)">
<i class="mdi mdi-arrow-right"></i> Plus de détails
</button>
</span>
</div>
</div>
</div>
</div>
<!-- fin cards -->
<!-- Pagination -->
<div class="pl-pagination" *ngIf="totalElements > pageSize">
<span class="pl-pagination-info">
Page {{ pageNo + 1 }} sur {{ totalPages }}
— {{ totalElements }} unité(s) de logement
</span>
<nz-pagination [nzPageIndex]="pageNo + 1" [nzTotal]="totalElements"
[nzPageSize]="pageSize" [nzShowSizeChanger]="true"
[nzPageSizeOptions]="[10, 20, 50, 100]"
(nzPageIndexChange)="onPageChange($event)"
(nzPageSizeChange)="onPageSizeChange($event)">
</nz-pagination>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,412 @@
import { Component, SimpleChanges, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzTreeNodeOptions } from 'ng-zorro-antd/tree';
import { firstValueFrom } from 'rxjs';
import { CrudService } from 'src/app/crud.service';
import { ExcelExportService } from 'src/app/excel-export.service';
import { GlobalService } from 'src/app/global.service';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
export interface UniteLogementPagedItem {
id?: number;
nul?: string;
numeroEtage?: string;
code?: string;
batimentId?: number;
batimentNub?: string;
superficieAuSol?: number;
superficieLouee?: number;
observation?: string;
dateConstruction?: string;
personneId?: number;
personneNom?: string;
personnePrenom?: string;
personneRaisonSociale?: string;
enqueteUniteLogementCourantId?: number;
categorieBatimentId?: number;
categorieBatimentCode?: string;
categorieBatimentStanding?: string;
montantMensuelLocation?: number;
montantLocatifAnnuelDeclare?: number;
montantLocatifAnnuelCalcule?: number;
montantLocatifAnnuelEstime?: number;
valeurUniteLogementEstime?: number;
valeurUniteLogementReel?: number;
valeurUniteLogementCalcule?: number;
nombrePiscine?: number;
usageId?: number;
usageNom?: string;
}
@Component({
selector: 'app-list-unite-logement-consultation',
templateUrl: './list-unite-logement-consultation.component.html',
styleUrls: ['./list-unite-logement-consultation.component.css']
})
export class ListUniteLogementConsultationComponent {
user: any = null;
numMenu = 2;
nodes: NzTreeNodeOptions[] = []; // Les noeuds de l'arbre
arbreUtilisateurCourant: any[] = [];
quartierSelected: any = null;
isActionInProgress = false;
// ── Données ───────────────────────────────────────────
donnees: UniteLogementPagedItem[] = [];
loading = false;
// ── Pagination client-side ────────────────────────────
pageNo = 0;
pageSize = 10;
// ── Filtre ────────────────────────────────────────────
filterForm!: FormGroup;
filterVisible = false;
filterApplied = false;
// ── Ligne expandée ────────────────────────────────────
expandedIds = new Set<number>();
// ── Référentiels ──────────────────────────────────────
usageList: any[] = [];
categorieBatimentList: any[] = [];
// ── EXPORT LABELS ─────────────────────────────────────
private readonly EXPORT_LABELS: { [key: string]: string } = {
id: 'ID Unité Logement',
nul: 'NUL',
numeroEtage: 'Numéro Étage',
code: 'Code Unité Logement',
batimentId: 'ID Bâtiment',
batimentNub: 'NUB Bâtiment',
superficieAuSol: 'Superficie au Sol (m²)',
superficieLouee: 'Superficie Louée (m²)',
observation: 'Observation',
dateConstruction: 'Date Construction',
personneId: 'ID Propriétaire',
personneNom: 'Nom Propriétaire',
personnePrenom: 'Prénom Propriétaire',
personneRaisonSociale: 'Raison Sociale Propriétaire',
enqueteUniteLogementCourantId: 'ID Enquête Courante',
categorieBatimentId: 'ID Catégorie Bâtiment',
categorieBatimentCode: 'Code Catégorie Bâtiment',
categorieBatimentStanding: 'Standing Bâtiment',
montantMensuelLocation: 'Loyer Mensuel (FCFA)',
montantLocatifAnnuelDeclare: 'Loyer Annuel Déclaré (FCFA)',
montantLocatifAnnuelCalcule: 'Loyer Annuel Calculé (FCFA)',
montantLocatifAnnuelEstime: 'Loyer Annuel Estimé (FCFA)',
valeurUniteLogementEstime: 'Valeur Estimée U.L. (FCFA)',
valeurUniteLogementReel: 'Valeur Réelle U.L. (FCFA)',
valeurUniteLogementCalcule: 'Valeur Calculée U.L. (FCFA)',
nombrePiscine: 'Nombre Piscines',
usageId: 'ID Usage',
usageNom: 'Usage',
};
private readonly CHAMPS_EXCLUS = new Set([
'id', 'batimentId', 'personneId',
'enqueteUniteLogementCourantId', 'categorieBatimentId', 'usageId',
]);
constructor(
private fb: FormBuilder,
private tokenStorage: TokenStorage,
private router: Router,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
private globalService: GlobalService,
private modalService: NzModalService,
private viewContainerRef: ViewContainerRef,
private excelExportService: ExcelExportService,
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
ngOnInit(): void {
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
if (this.user) {
this.crudService.getAll('secteur-decoupage/arbre/user-id/' + this.user?.id).subscribe(
(data: any) => {
this.arbreUtilisateurCourant = data.object ? data.object : [];
if (this.arbreUtilisateurCourant && this.arbreUtilisateurCourant.length > 0) {
this.constructionArbreUtilisateurs();
}
this.message.success(`Chargement des découpages de l'utilisateur ${this.user?.nom} réussi`);
},
(error: any) => {
this.message.error(`Chargement des découpages de l'utilisateur ${this.user?.nom} échoué`);
});
}
this.initFilterForm();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['quartierId'] && this.quartierSelected?.quartierId) {
this.pageNo = 0;
this.reinitialiserFiltre();
this.charger();
}
}
// ── Utilitaire ────────────────────────────────────────
constructionArbreUtilisateurs() {
const data: any[] = this.arbreUtilisateurCourant;
// Grouper par département
const deptMap = new Map<number, any>();
data.forEach(item => {
if (!deptMap.has(item.departementId)) {
deptMap.set(item.departementId, {
...item,
communes: new Map<number, any>()
});
}
const dept = deptMap.get(item.departementId);
if (!dept.communes.has(item.communeId)) {
dept.communes.set(item.communeId, {
...item,
arrondissements: new Map<number, any>()
});
}
const commune = dept.communes.get(item.communeId);
if (!commune.arrondissements.has(item.arrondissementId)) {
commune.arrondissements.set(item.arrondissementId, {
...item,
quartiers: []
});
}
const arr = commune.arrondissements.get(item.arrondissementId);
arr.quartiers.push(item);
});
// Construire les noeuds
this.nodes = Array.from(deptMap.values()).map(dept => ({
title: `${dept.departementCode}${dept.departementNom}`,
key: `dept-${dept.departementId}`,
icon: 'bank',
isLeaf: false,
expanded: false,
nbParcelles: dept.nbParcellesDepartement,
children: Array.from(dept.communes.values()).map((comm: any) => ({
title: `${comm.communeCode}${comm.communeNom}`,
key: `comm-${comm.communeId}`,
icon: 'home',
isLeaf: false,
expanded: false,
nbParcelles: comm.nbParcellesCommune,
children: Array.from(comm.arrondissements.values()).map((arr: any) => ({
title: arr.arrondissementNom,
key: `arr-${arr.arrondissementId}`,
icon: 'apartment',
isLeaf: false,
expanded: false,
nbParcelles: arr.nbParcellesArrondissement,
children: arr.quartiers.map((quart: any) => ({
title: quart.quartierNom,
key: `quart-${quart.quartierId}`,
icon: 'environment',
isLeaf: true,
nbParcelles: quart.nbParcellesQuartier,
quartier: quart
}))
}))
}))
}));
}
onNodeClick(event: any) {
const node = event.node;
if (node.isLeaf && node.key.startsWith('quart-')) {
this.quartierSelected = node.origin.quartier;
console.log(' this.quartierSelected ==>', this.quartierSelected);
this.message.create('success', `Quartier ${this.quartierSelected.quartierNom} sélectionné.`);
// votre logique de sélection...
this.charger();
}
}
getBadgeColor(niveau: string): string {
const colors: any = {
'dept': '#204e10',
'comm': '#10b981',
'arr': '#f59e0b',
'quart': '#ef6972'
};
return colors[niveau] ?? '#6b7280';
}
getNiveau(key: string): string {
if (key.startsWith('dept')) return 'dept';
if (key.startsWith('comm')) return 'comm';
if (key.startsWith('arr')) return 'arr';
if (key.startsWith('quart')) return 'quart';
return '';
}
// ── Init formulaire de filtre ─────────────────────────
initFilterForm(): void {
this.crudService.getAll('usage/all').subscribe((data: any) => {
this.usageList = data.object ?? [];
});
this.crudService.getAll('categorie-batiment/all').subscribe((data: any) => {
this.categorieBatimentList = data.object ?? [];
});
this.filterForm = this.fb.group({
nul: [null],
code: [null],
numeroEtage: [null],
batimentNub: [null],
personneNom: [null],
personnePrenom: [null],
personneRaisonSociale: [null],
categorieBatimentId: [null],
usageId: [null],
});
}
// ── Chargement ────────────────────────────────────────
charger(): void {
if (!this.quartierSelected) return;
this.loading = true;
const url = `unite-logement/all/by-quartier-id/${this.quartierSelected?.quartierId}`;
this.crudService.getAll(url).subscribe({
next: (data: any) => {
this.donnees = data?.object ?? [];
this.pageNo = 0;
this.loading = false;
},
error: () => {
this.message.error('Erreur lors du chargement des unités de logement.');
this.loading = false;
}
});
}
// ── Filtre client-side ────────────────────────────────
get filteredList(): UniteLogementPagedItem[] {
if (!this.filterApplied) return this.donnees;
const f = this.filterForm.value;
const match = (
filterVal: string | null | undefined,
itemVal: string | null | undefined
): boolean => {
if (!filterVal?.trim()) return true;
if (!itemVal?.trim()) return false;
return itemVal.toLowerCase().includes(filterVal.trim().toLowerCase());
};
return this.donnees.filter(u =>
match(f.nul, u.nul) &&
match(f.code, u.code) &&
match(f.numeroEtage, u.numeroEtage) &&
match(f.batimentNub, u.batimentNub) &&
(
match(f.personneNom, u.personneNom) ||
match(f.personneNom, u.personneRaisonSociale)
) &&
match(f.personnePrenom, u.personnePrenom) &&
(!f.categorieBatimentId || u.categorieBatimentId?.toString() === f.categorieBatimentId) &&
(!f.usageId || u.usageId?.toString() === f.usageId)
);
}
get pageCourante(): UniteLogementPagedItem[] {
const debut = this.pageNo * this.pageSize;
return this.filteredList.slice(debut, debut + this.pageSize);
}
get totalElements(): number { return this.filteredList.length; }
get totalPages(): number {
return this.pageSize > 0 ? Math.ceil(this.totalElements / this.pageSize) : 0;
}
onPageChange(page: number): void { this.pageNo = page - 1; }
onPageSizeChange(size: number): void {
this.pageSize = size;
this.pageNo = 0;
}
appliquerFiltre(): void {
this.filterApplied = true;
this.pageNo = 0;
}
reinitialiserFiltre(): void {
this.filterForm?.reset();
this.filterApplied = false;
this.pageNo = 0;
}
toggleFiltre(): void { this.filterVisible = !this.filterVisible; }
toggleRow(id: number | undefined): void {
if (id == null) return;
this.expandedIds.has(id) ? this.expandedIds.delete(id) : this.expandedIds.add(id);
}
isExpanded(id: number | undefined): boolean {
return id != null && this.expandedIds.has(id);
}
formatMontant(val: number | null | undefined): string {
if (val == null) return '—';
return new Intl.NumberFormat('fr-FR').format(val) + ' FCFA';
}
private nettoyerLigneExport(item: any): { [label: string]: any } {
const ligne: { [label: string]: any } = {};
for (const key of Object.keys(this.EXPORT_LABELS)) {
if (this.CHAMPS_EXCLUS.has(key)) continue;
const val = item[key];
ligne[this.EXPORT_LABELS[key]] =
typeof val === 'boolean' ? (val ? 'OUI' : 'NON') :
val == null ? '—' : val;
}
return ligne;
}
exportPageCourante(): void {
if (!this.filteredList.length) {
this.message.warning('Aucune donnée à exporter.');
return;
}
const data = this.filteredList.map(item => this.nettoyerLigneExport(item));
this.excelExportService.exportAsExcelFile(data, 'unites-logement', 'Unités Logement');
}
afficherDetail(id: any): void {
this.router.navigate(['/core/consultation/detail-unite-logement/' + id]);
}
}

View File

@@ -0,0 +1,606 @@
/* ══════════════════════════════════════════════════════════════
RESET NZ-CARD BODY
══════════════════════════════════════════════════════════════ */
.kpi-card .ant-card-body,
.stat-banner-card .ant-card-body,
.chart-card .ant-card-body,
.stats-table-card .ant-card-body,
.alert-card .ant-card-body {
padding: 0 !important;
}
/* ══════════════════════════════════════════════════════════════
DASHBOARD CONTAINER
══════════════════════════════════════════════════════════════ */
.dashboard-container {
padding: 24px;
width: 100%;
min-height: 100vh;
}
/* ══════════════════════════════════════════════════════════════
KPI CARDS
══════════════════════════════════════════════════════════════ */
.kpi-cards-section {
margin-bottom: 32px;
}
.kpi-card {
border-radius: 12px;
border: none;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin-bottom: 16px;
}
.kpi-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(26, 88, 144, 0.18);
}
.kpi-content {
display: flex;
align-items: center;
padding: 20px 24px;
gap: 18px;
position: relative;
overflow: hidden;
}
.kpi-content::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 4px;
}
.kpi-icon {
width: 56px; height: 56px;
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
background: transparent;
}
.kpi-icon [nz-icon],
.kpi-icon span[nz-icon] {
font-size: 30px;
}
.kpi-details { flex: 1; }
.kpi-value {
font-size: 34px;
font-weight: 700;
line-height: 1;
margin-bottom: 6px;
}
.kpi-value-small {
font-size: 18px !important;
font-weight: 700;
line-height: 1.2;
margin-bottom: 6px;
}
.kpi-unit {
font-size: 18px;
font-weight: 600;
color: #6b7280;
margin-left: 2px;
}
.kpi-label {
font-size: 10px;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.6px;
}
/*.kpi-card-blue .kpi-content::before { background: linear-gradient(90deg, #1a5890, #0e3660); }*/
.kpi-card-blue .kpi-icon [nz-icon] { color: #1a5890; }
.kpi-card-blue .kpi-value { color: #1a5890; }
/*.kpi-card-green .kpi-content::before { background: linear-gradient(90deg, #10b981, #059669); }*/
.kpi-card-green .kpi-icon [nz-icon] { color: #10b981; }
.kpi-card-green .kpi-value { color: #10b981; }
.kpi-card-green .kpi-value-small { color: #10b981; }
/*.kpi-card-purple .kpi-content::before { background: linear-gradient(90deg, #1a5890, #6d28d9); }*/
.kpi-card-purple .kpi-icon [nz-icon] { color: #1a5890; }
.kpi-card-purple .kpi-value { color: #1a5890; }
/*.kpi-card-red .kpi-content::before { background: linear-gradient(90deg, #ef4444, #dc2626); }*/
.kpi-card-red .kpi-icon [nz-icon] { color: #ef4444; }
.kpi-card-red .kpi-value { color: #ef4444; }
/* ══════════════════════════════════════════════════════════════
STAT BANNER
══════════════════════════════════════════════════════════════ */
.stat-banner-card {
border-radius: 12px;
border: none;
box-shadow: 0 2px 12px rgba(26, 88, 144, 0.10);
overflow: hidden;
}
.stat-banner-container {
display: flex;
align-items: stretch;
min-height: 110px;
}
.stat-banner-item {
flex: 1;
display: flex;
align-items: center;
gap: 16px;
padding: 18px 20px;
transition: filter 0.2s ease;
}
.stat-banner-item:hover { filter: brightness(0.96); }
.stat-banner-green { background: linear-gradient(135deg, #f0fdf4, #dcfce7); }
.stat-banner-blue { background: linear-gradient(135deg, #e8f1fb, #cce0f5); }
.stat-banner-purple { background: linear-gradient(135deg, #eef2fb, #d6e4f5); }
.stat-banner-orange { background: linear-gradient(135deg, #fffbeb, #fef3c7); }
.stat-banner-icon {
font-size: 28px;
flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
width: 48px; height: 48px;
border-radius: 10px;
}
.stat-banner-green .stat-banner-icon { color: #10b981; background: rgba(16, 185, 129, 0.12); }
.stat-banner-blue .stat-banner-icon { color: #1a5890; background: rgba(26, 88, 144, 0.12); }
.stat-banner-purple .stat-banner-icon { color: #1a5890; background: rgba(26, 88, 144, 0.10); }
.stat-banner-orange .stat-banner-icon { color: #f59e0b; background: rgba(245, 158, 11, 0.12); }
.stat-banner-body {
flex: 1;
display: flex; flex-direction: column; gap: 3px;
}
.stat-banner-value {
font-size: 18px;
font-weight: 700;
line-height: 1;
color: #111827;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-banner-unit {
font-size: 15px;
font-weight: 600;
color: #6b7280;
margin-left: 2px;
}
.stat-banner-label {
font-size: 10px;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-banner-bar {
width: 100%; height: 5px;
background: rgba(0,0,0,0.07);
border-radius: 999px;
overflow: hidden;
margin-top: 4px;
}
.stat-banner-bar-fill {
height: 100%;
border-radius: 999px;
transition: width 0.9s cubic-bezier(0.4, 0, 0.2, 1);
}
.stat-banner-bar-fill.green { background: #10b981; }
.stat-banner-bar-fill.blue { background: #1a5890; }
.stat-banner-bar-fill.purple { background: #1a5890; }
.stat-banner-bar-fill.orange { background: #f59e0b; }
.stat-banner-percent {
font-size: 11px;
color: #9ca3af;
font-weight: 500;
}
.stat-banner-divider {
width: 1px;
/*background: rgba(0,0,0,0.07);*/
margin: 12px 0;
flex-shrink: 0;
margin-right: 3px;
margin-left: 0px;
}
/* ══════════════════════════════════════════════════════════════
PIPELINE STATUTS
══════════════════════════════════════════════════════════════ */
.statuts-pipeline {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(26, 88, 144, 0.08);
flex-wrap: wrap;
}
.pipeline-item {
flex: 1;
min-width: 120px;
padding: 12px 16px;
border-radius: 10px;
display: flex;
flex-direction: column;
gap: 4px;
}
.pipeline-count {
font-size: 28px;
font-weight: 700;
line-height: 1;
}
.pipeline-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.75;
}
.pipeline-bar {
width: 100%; height: 4px;
background: rgba(0,0,0,0.08);
border-radius: 999px;
overflow: hidden;
margin-top: 6px;
}
.pipeline-bar-fill {
height: 100%;
border-radius: 999px;
transition: width 0.9s cubic-bezier(0.4, 0, 0.2, 1);
}
.pipeline-warning { background: linear-gradient(135deg, #fffbeb, #fef3c7); }
.pipeline-warning .pipeline-count { color: #d97706; }
.pipeline-blue { background: linear-gradient(135deg, #e8f1fb, #cce0f5); }
.pipeline-blue .pipeline-count { color: #1a5890; }
.pipeline-cyan { background: linear-gradient(135deg, #ecfeff, #cffafe); }
.pipeline-cyan .pipeline-count { color: #0891b2; }
.pipeline-green { background: linear-gradient(135deg, #f0fdf4, #dcfce7); }
.pipeline-green .pipeline-count { color: #16a34a; }
.pipeline-red { background: linear-gradient(135deg, #fff1f2, #ffe4e6); }
.pipeline-red .pipeline-count { color: #dc2626; }
.pipeline-arrow {
font-size: 18px;
color: #c5d9ef;
font-weight: 700;
flex-shrink: 0;
}
.pipeline-separator {
width: 2px; height: 50px;
background: #e0ecf8;
border-radius: 999px;
flex-shrink: 0;
}
/* ══════════════════════════════════════════════════════════════
ONGLETS THÉMATIQUES
══════════════════════════════════════════════════════════════ */
.thematique-tabs-section {
margin-top: 28px;
}
.thematique-tabs {
display: flex;
gap: 4px;
flex-wrap: wrap;
margin-bottom: 24px;
border-bottom: 2px solid #e0ecf8;
padding-bottom: 0;
}
.ttab {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 10px 18px;
font-size: 13px;
font-weight: 500;
color: #6b7280;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px 6px 0 0;
line-height: 1;
}
.ttab:hover {
color: #1a5890;
background: #e8f1fb;
}
.ttab-active {
color: #1a5890 !important;
border-bottom-color: #1a5890 !important;
background: #e8f1fb !important;
font-weight: 700;
}
.thematique-content {
animation: fadeInUp 0.3s ease-out;
}
/* ══════════════════════════════════════════════════════════════
CHART CARDS
══════════════════════════════════════════════════════════════ */
.chart-card {
border-radius: 12px;
border: none;
box-shadow: 0 2px 12px rgba(26, 88, 144, 0.08);
height: 100%;
margin-bottom: 16px;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 2px solid #e0ecf8;
}
.chart-title {
font-size: 14px;
font-weight: 700;
color: #1a5890;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.chart-title [nz-icon] {
color: #1a5890;
font-size: 18px;
}
.chart-content {
padding: 16px 20px;
min-height: 320px;
}
.chart-footer {
padding: 16px 20px;
background: #f4f8fd;
border-top: 1px solid #e0ecf8;
}
.chart-summary {
display: flex;
justify-content: space-around;
gap: 16px;
}
.summary-item {
display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.summary-label {
font-size: 11px; color: #6b7280; font-weight: 500;
text-transform: uppercase; letter-spacing: 0.5px;
}
.summary-value {
font-size: 18px; font-weight: 700; color: #1a5890;
}
/* ══════════════════════════════════════════════════════════════
STATS TABLE CARD
══════════════════════════════════════════════════════════════ */
.stats-table-card {
border-radius: 12px;
border: none;
box-shadow: 0 2px 12px rgba(26, 88, 144, 0.08);
}
.table-header {
display: flex; justify-content: space-between; align-items: center;
padding: 16px 20px;
border-bottom: 2px solid #e0ecf8;
}
.table-title {
font-size: 14px; font-weight: 700; color: #1a5890;
margin: 0; display: flex; align-items: center; gap: 8px;
}
.table-title [nz-icon] { color: #1a5890; font-size: 18px; }
.fonction-name { font-weight: 600; color: #1a5890; }
.ant-table { font-size: 13px; }
.ant-table thead > tr > th {
background: #e8f1fb !important;
font-weight: 700 !important;
color: #1a5890 !important;
border-bottom: 2px solid #c5d9ef !important;
}
.ant-table tbody > tr:hover > td { background: #f4f8fd !important; }
.ant-table tbody > tr > td { padding: 12px 16px !important; }
/* ══════════════════════════════════════════════════════════════
MONTANT CELLS
══════════════════════════════════════════════════════════════ */
.montant-cell { font-weight: 600; color: #1a5890; }
.montant-cell-green { font-weight: 600; color: #10b981; }
.montant-total { font-size: 14px; font-weight: 700; color: #0e3660; }
/* ══════════════════════════════════════════════════════════════
EVOLUTION BADGE
══════════════════════════════════════════════════════════════ */
.evol-badge {
display: inline-flex; align-items: center; gap: 3px;
padding: 3px 8px; border-radius: 999px;
font-size: 11px; font-weight: 700;
background: #f3f4f6; color: #6b7280;
}
.evol-badge.evol-up { background: #dcfce7; color: #16a34a; }
.evol-badge.evol-zero { background: #f3f4f6; color: #9ca3af; }
/* ══════════════════════════════════════════════════════════════
SYNTHESE LIST
══════════════════════════════════════════════════════════════ */
.synthese-list {
padding: 12px 16px;
display: flex; flex-direction: column; gap: 12px;
}
.synthese-item {
display: flex; align-items: center; gap: 12px;
padding: 10px 14px;
background: #f4f8fd;
border-radius: 8px;
border-left: 3px solid #e0ecf8;
}
.synthese-icon {
width: 36px; height: 36px;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
}
.synthese-icon.blue { background: rgba(26,88,144,0.10); color: #1a5890; }
.synthese-icon.green { background: rgba(16,185,129,0.10); color: #10b981; }
.synthese-icon.orange { background: rgba(245,158,11,0.10); color: #f59e0b; }
.synthese-icon.red { background: rgba(239,68,68,0.10); color: #ef4444; }
.synthese-body {
flex: 1;
display: flex; justify-content: space-between; align-items: center;
}
.synthese-label { font-size: 12px; font-weight: 500; color: #6b7280; }
.synthese-val { font-size: 14px; font-weight: 700; color: #1a5890; }
/* ══════════════════════════════════════════════════════════════
ALERT CARDS
══════════════════════════════════════════════════════════════ */
.alert-card {
border-radius: 12px; border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
margin-bottom: 16px;
}
.alert-content {
display: flex; align-items: flex-start;
gap: 16px; padding: 20px; border-radius: 10px;
}
.alert-icon { font-size: 30px; flex-shrink: 0; margin-top: 2px; }
.alert-body { flex: 1; }
.alert-title {
font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px;
}
.alert-value {
font-size: 26px; font-weight: 700; line-height: 1.1; margin-bottom: 4px;
}
.alert-desc { font-size: 12px; opacity: 0.72; }
.alert-warning { background: linear-gradient(135deg, #fffbeb, #fef3c7); }
.alert-warning .alert-icon { color: #f59e0b; }
.alert-warning .alert-title { color: #92400e; }
.alert-warning .alert-value { color: #d97706; }
.alert-warning .alert-desc { color: #78350f; }
.alert-danger { background: linear-gradient(135deg, #fff1f2, #ffe4e6); }
.alert-danger .alert-icon { color: #ef4444; }
.alert-danger .alert-title { color: #991b1b; }
.alert-danger .alert-value { color: #dc2626; }
.alert-danger .alert-desc { color: #7f1d1d; }
.alert-success { background: linear-gradient(135deg, #f0fdf4, #dcfce7); }
.alert-success .alert-icon { color: #10b981; }
.alert-success .alert-title { color: #14532d; }
.alert-success .alert-value { color: #16a34a; }
.alert-success .alert-desc { color: #15803d; }
.alert-info { background: linear-gradient(135deg, #e8f1fb, #cce0f5); }
.alert-info .alert-icon { color: #1a5890; }
.alert-info .alert-title { color: #0e3660; }
.alert-info .alert-value { color: #1a5890; }
.alert-info .alert-desc { color: #2563eb; }
/* ══════════════════════════════════════════════════════════════
ANIMATIONS
══════════════════════════════════════════════════════════════ */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.kpi-card { animation: fadeInUp 0.45s ease-out both; }
.chart-card { animation: fadeInUp 0.45s ease-out both; }
.stats-table-card { animation: fadeInUp 0.45s ease-out both; }
.kpi-card:nth-child(1) { animation-delay: 0.05s; }
.kpi-card:nth-child(2) { animation-delay: 0.12s; }
.kpi-card:nth-child(3) { animation-delay: 0.19s; }
.kpi-card:nth-child(4) { animation-delay: 0.26s; }
/* ══════════════════════════════════════════════════════════════
RESPONSIVE
══════════════════════════════════════════════════════════════ */
@media (max-width: 992px) {
.stat-banner-container { flex-wrap: wrap; }
.stat-banner-item { flex: 0 0 50%; min-width: 0; }
.stat-banner-divider { display: none; }
.statuts-pipeline { gap: 6px; }
.pipeline-item { min-width: 90px; padding: 10px 12px; }
.pipeline-count { font-size: 22px; }
.pipeline-arrow { display: none; }
.pipeline-separator { display: none; }
}
@media (max-width: 768px) {
.dashboard-container { padding: 12px; }
.stat-banner-container { flex-direction: column; }
.stat-banner-item { flex: 1 1 100%; }
.thematique-tabs { gap: 2px; }
.ttab { padding: 8px 10px; font-size: 12px; }
.kpi-content { padding: 14px 16px; }
.kpi-value { font-size: 26px; }
}

View File

@@ -0,0 +1,743 @@
<div class="row">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review">
<div>
<!-- ── KPIs principaux ── -->
<div class="kpi-cards-section">
<div class="row">
<div class="col-lg-4">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Selectionner l'exercice fiscale">
<nz-option *ngFor="let item of getAnneeList()" [nzLabel]="item"
[nzValue]="item"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Exercice Fiscale <span
class="text-danger"> *</span> </label>
</div>
</div>
<div class="col-lg-4">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear
nzPlaceHolder="Selectionner la commune">
<nz-option *ngFor="let item of getCommuneList()" [nzLabel]="item"
[nzValue]="item"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Commune <span
class="text-danger"> *</span> </label>
</div>
</div>
<div class="col-lg-4">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear
nzPlaceHolder="Selectionner la direction / centre d'impôt">
<nz-option *ngFor="let item of getStructureList()" [nzLabel]="item"
[nzValue]="item"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Direction / centre d'impôt <span
class="text-danger"> *</span> </label>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-6">
<nz-card class="kpi-card kpi-card-red" [nzLoading]="loading">
<div class="kpi-content">
<div class="kpi-icon"><span nz-icon nzType="home" nzTheme="outline"></span>
</div>
<div class="kpi-details">
<div class="kpi-value">
{{ statsGlobales.totalParcelles | number:'1.0-0':'fr' }}</div>
<div class="kpi-label">Parcelles Concernées</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-3 col-md-6">
<nz-card class="kpi-card kpi-card-blue" [nzLoading]="loading">
<div class="kpi-content">
<div class="kpi-icon"><span nz-icon nzType="file-text" nzTheme="outline"></span>
</div>
<div class="kpi-details">
<div class="kpi-value">
{{ statsGlobales.totalLiquidations | number:'1.0-0':'fr' }}</div>
<div class="kpi-label">Total Liquidations</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-3 col-md-6">
<nz-card class="kpi-card kpi-card-green" [nzLoading]="loading">
<div class="kpi-content">
<div class="kpi-icon"><span nz-icon nzType="dollar-circle"
nzTheme="outline"></span></div>
<div class="kpi-details">
<div class="kpi-value kpi-value-small">{{ getMontantTotalFormatted() }}
</div>
<div class="kpi-label">Montant Total Généré</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-3 col-md-6">
<nz-card class="kpi-card kpi-card-purple" [nzLoading]="loading">
<div class="kpi-content">
<div class="kpi-icon"><span nz-icon nzType="check-circle"
nzTheme="outline"></span></div>
<div class="kpi-details">
<div class="kpi-value">{{ getTauxCloture() }}<span class="kpi-unit">%</span>
</div>
<div class="kpi-label">Taux de Clôture</div>
</div>
</div>
</nz-card>
</div>
</div>
<!-- ── Bandeaux secondaires ── -->
<div class="row mt-2">
<div class="col-lg-12">
<nz-card class="stat-banner-card" [nzLoading]="loading">
<div class="stat-banner-container">
<div class="stat-banner-item stat-banner-blue">
<div class="stat-banner-icon"><span nz-icon nzType="fund"
nzTheme="outline"></span></div>
<div class="stat-banner-body">
<div class="stat-banner-value">{{ getMontantTFUFormatted() }}</div>
<div class="stat-banner-label">Montant TFU</div>
<div class="stat-banner-bar">
<div class="stat-banner-bar-fill blue"
[style.width]="(statsParNature.length > 0 ? statsParNature[0].pourcentage : 0) + '%'"></div>
</div>
<div class="stat-banner-percent">{{ (statsParNature.length > 0 ? statsParNature[0].pourcentage : 0) }}%
du total —
{{ (statsParNature.length > 0 ? statsParNature[0].nombreAssujettis : 0) | number:'1.0-0':'fr' }}
assujettis</div>
</div>
</div>
<div class="stat-banner-divider"></div>
<div class="stat-banner-item stat-banner-green">
<div class="stat-banner-icon"><span nz-icon nzType="bank"
nzTheme="outline"></span></div>
<div class="stat-banner-body">
<div class="stat-banner-value">{{ getMontantIRFFormatted() }}</div>
<div class="stat-banner-label">Montant IRF</div>
<div class="stat-banner-bar">
<div class="stat-banner-bar-fill green"
[style.width]="(statsParNature.length > 1 ? statsParNature[1].pourcentage : 0) + '%'"></div>
</div>
<div class="stat-banner-percent">{{ (statsParNature.length > 1 ? statsParNature[1].pourcentage : 0) }}%
du total —
{{ (statsParNature.length > 1 ? statsParNature[1].nombreAssujettis : 0) | number:'1.0-0':'fr' }}
assujettis</div>
</div>
</div>
<div class="stat-banner-divider"></div>
<div class="stat-banner-item stat-banner-orange">
<div class="stat-banner-icon"><span nz-icon nzType="apartment"
nzTheme="outline"></span></div>
<div class="stat-banner-body">
<div class="stat-banner-value">
{{ statsGlobales.totalBatiments | number:'1.0-0':'fr' }}</div>
<div class="stat-banner-label">Bâtiments / Unités logement</div>
<div class="stat-banner-bar">
<div class="stat-banner-bar-fill orange" [style.width]="'100%'">
</div>
</div>
<div class="stat-banner-percent">
{{ statsGlobales.totalUnitesLogement | number:'1.0-0':'fr' }} unités
— {{ statsGlobales.totalPiscines }} piscines</div>
</div>
</div>
<div class="stat-banner-divider"></div>
<div class="stat-banner-item stat-banner-purple">
<div class="stat-banner-icon"><span nz-icon nzType="close-circle"
nzTheme="outline"></span></div>
<div class="stat-banner-body">
<div class="stat-banner-value">
{{ statsGlobales.totalParcellesExhonerees | number:'1.0-0':'fr' }}
</div>
<div class="stat-banner-label">Parcelles Exhonérées</div>
<div class="stat-banner-bar">
<div class="stat-banner-bar-fill purple"
[style.width]="(statsGlobales.totalParcellesExhonerees / statsGlobales.totalParcelles * 100) + '%'">
</div>
</div>
<div class="stat-banner-percent">
{{ (statsGlobales.totalParcellesExhonerees / statsGlobales.totalParcelles * 100).toFixed(1) }}%
des parcelles</div>
</div>
</div>
</div>
</nz-card>
</div>
</div>
<!-- ── Statuts sous forme de badges ── -->
<div class="row mt-2">
<div class="col-lg-12">
<div class="statuts-pipeline">
<div class="pipeline-item pipeline-warning">
<div class="pipeline-count">{{ statsGlobales.enCours }}</div>
<div class="pipeline-label">En cours</div>
<div class="pipeline-bar">
<div class="pipeline-bar-fill"
[style.width]="(statsGlobales.enCours / statsGlobales.totalLiquidations * 100) + '%'"
style="background:#f59e0b"></div>
</div>
</div>
<div class="pipeline-arrow"></div>
<div class="pipeline-item pipeline-cyan ">
<div class="pipeline-count">{{ statsGlobales.cloture }}</div>
<div class="pipeline-label">Clôturé</div>
<div class="pipeline-bar">
<div class="pipeline-bar-fill"
[style.width]="(statsGlobales.cloture / statsGlobales.totalLiquidations * 100) + '%'"
style="background:#06b6d4"></div>
</div>
</div>
<div class="pipeline-arrow"></div>
<div class="pipeline-item pipeline-blue">
<div class="pipeline-count">{{ statsGlobales.generationAutorisee }}</div>
<div class="pipeline-label">Génération autorisée</div>
<div class="pipeline-bar">
<div class="pipeline-bar-fill"
[style.width]="(statsGlobales.generationAutorisee / statsGlobales.totalLiquidations * 100) + '%'"
style="background:#1a5890"></div>
</div>
</div>
<div class="pipeline-arrow"></div>
<div class="pipeline-item pipeline-green">
<div class="pipeline-count">{{ statsGlobales.genere }}</div>
<div class="pipeline-label">Généré</div>
<div class="pipeline-bar">
<div class="pipeline-bar-fill"
[style.width]="(statsGlobales.genere / statsGlobales.totalLiquidations * 100) + '%'"
style="background:#10b981"></div>
</div>
</div>
<div class="pipeline-separator"></div>
<div class="pipeline-item pipeline-red">
<div class="pipeline-count">{{ statsGlobales.rejete }}</div>
<div class="pipeline-label">Rejeté</div>
<div class="pipeline-bar">
<div class="pipeline-bar-fill"
[style.width]="(statsGlobales.rejete / statsGlobales.totalLiquidations * 100) + '%'"
style="background:#ef4444"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<br>
<div class="row">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review">
<div>
<!-- ── Onglets thématiques ── -->
<div class="thematique-tabs-section">
<div class="thematique-tabs">
<button class="ttab" [class.ttab-active]="activeThematique === 'vue-globale'"
(click)="activeThematique = 'vue-globale'">
<span nz-icon nzType="dashboard" nzTheme="outline"></span> Vue globale
</button>
<button class="ttab" [class.ttab-active]="activeThematique === 'evolution'"
(click)="activeThematique = 'evolution'">
<span nz-icon nzType="line-chart" nzTheme="outline"></span> Évolution annuelle
</button>
<button class="ttab" [class.ttab-active]="activeThematique === 'territoire'"
(click)="activeThematique = 'territoire'">
<span nz-icon nzType="environment" nzTheme="outline"></span> Par territoire
</button>
<button class="ttab" [class.ttab-active]="activeThematique === 'dette'"
(click)="activeThematique = 'dette'">
<span nz-icon nzType="alert" nzTheme="outline"></span> Dette fiscale
</button>
<button class="ttab" [class.ttab-active]="activeThematique === 'patrimoine'"
(click)="activeThematique = 'patrimoine'">
<span nz-icon nzType="home" nzTheme="outline"></span> Patrimoine
</button>
</div>
<!-- ── VUE GLOBALE ── -->
<div *ngIf="activeThematique === 'vue-globale'" class="thematique-content">
<div class="row">
<div class="col-lg-4">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="pie-chart"></span> Statuts des
liquidations</h3>
</div>
<div class="chart-content">
<apx-chart [series]="pieStatutsOptions.series"
[chart]="pieStatutsOptions.chart" [labels]="pieStatutsOptions.labels"
[colors]="pieStatutsOptions.colors" [legend]="pieStatutsOptions.legend"
[plotOptions]="pieStatutsOptions.plotOptions"
[dataLabels]="pieStatutsOptions.dataLabels"
[responsive]="pieStatutsOptions.responsive">
</apx-chart>
</div>
</nz-card>
</div>
<div class="col-lg-4">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="pie-chart"></span> Répartition
TFU / IRF</h3>
</div>
<div class="chart-content">
<apx-chart [series]="pieNatureOptions.series"
[chart]="pieNatureOptions.chart" [labels]="pieNatureOptions.labels"
[colors]="pieNatureOptions.colors" [legend]="pieNatureOptions.legend"
[plotOptions]="pieNatureOptions.plotOptions"
[dataLabels]="pieNatureOptions.dataLabels"
[responsive]="pieNatureOptions.responsive">
</apx-chart>
</div>
<div class="chart-footer">
<div class="chart-summary">
<div class="summary-item" *ngFor="let n of statsParNature">
<span class="summary-label">{{ n.code }} :</span>
<span class="summary-value"
[style.color]="n.couleur">{{ formatMontantCourt(n.montant) }}</span>
</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-4">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="table"></span> Synthèse
patrimoine</h3>
</div>
<div class="synthese-list">
<div class="synthese-item">
<span class="synthese-icon blue"><span nz-icon
nzType="global"></span></span>
<div class="synthese-body">
<span class="synthese-label">Superficie totale</span>
<span
class="synthese-val">{{ statsGlobales.superficieTotale | number:'1.0-0':'fr' }}
</span>
</div>
</div>
<div class="synthese-item">
<span class="synthese-icon green"><span nz-icon
nzType="home"></span></span>
<div class="synthese-body">
<span class="synthese-label">Parcelles bâties</span>
<span
class="synthese-val">{{ statsGlobales.totalParcellesBaties | number:'1.0-0':'fr' }}</span>
</div>
</div>
<div class="synthese-item">
<span class="synthese-icon blue"><span nz-icon
nzType="apartment"></span></span>
<div class="synthese-body">
<span class="synthese-label">Unités de logement</span>
<span
class="synthese-val">{{ statsGlobales.totalUnitesLogement | number:'1.0-0':'fr' }}</span>
</div>
</div>
<div class="synthese-item">
<span class="synthese-icon orange"><span nz-icon
nzType="control"></span></span>
<div class="synthese-body">
<span class="synthese-label">Piscines recensées</span>
<span class="synthese-val">{{ statsGlobales.totalPiscines }}</span>
</div>
</div>
<div class="synthese-item">
<span class="synthese-icon red"><span nz-icon
nzType="stop"></span></span>
<div class="synthese-body">
<span class="synthese-label">Exhonérations</span>
<span
class="synthese-val">{{ statsGlobales.totalParcellesExhonerees | number:'1.0-0':'fr' }}
parc.</span>
</div>
</div>
</div>
</nz-card>
</div>
</div>
</div>
<!-- ── ÉVOLUTION ANNUELLE ── -->
<div *ngIf="activeThematique === 'evolution'" class="thematique-content">
<div class="row">
<div class="col-lg-8">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="line-chart"></span> Évolution
des montants par année</h3>
</div>
<div class="chart-content">
<apx-chart [series]="lineEvolutionOptions.series"
[chart]="lineEvolutionOptions.chart"
[xaxis]="lineEvolutionOptions.xaxis"
[yaxis]="lineEvolutionOptions.yaxis"
[colors]="lineEvolutionOptions.colors"
[legend]="lineEvolutionOptions.legend"
[stroke]="lineEvolutionOptions.stroke"
[markers]="lineEvolutionOptions.markers"
[grid]="lineEvolutionOptions.grid"
[dataLabels]="lineEvolutionOptions.dataLabels"
[tooltip]="lineEvolutionOptions.tooltip"
[fill]="lineEvolutionOptions.fill">
</apx-chart>
</div>
</nz-card>
</div>
<div class="col-lg-4">
<nz-card class="stats-table-card" [nzLoading]="loading">
<div class="table-header">
<h3 class="table-title"><span nz-icon nzType="table"></span> Détail par
année</h3>
</div>
<nz-table #anneeTable [nzData]="statsParAnnee" [nzShowPagination]="false"
nzSize="small">
<thead>
<tr>
<th>Année</th>
<th nzAlign="right">Total</th>
<th nzAlign="center">Évol.</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of anneeTable.data">
<td><strong>{{ item.annee }}</strong></td>
<td nzAlign="right" class="montant-cell">
{{ formatMontantCourt(item.montantTotal) }}</td>
<td nzAlign="center">
<span class="evol-badge" [class.evol-up]="item.evolution > 0"
[class.evol-zero]="item.evolution === 0">
<span nz-icon
[nzType]="item.evolution > 0 ? 'rise' : 'minus'"></span>
{{ item.evolution > 0 ? '+' : '' }}{{ item.evolution }}%
</span>
</td>
</tr>
</tbody>
</nz-table>
</nz-card>
</div>
</div>
</div>
<!-- ── PAR TERRITOIRE ── -->
<div *ngIf="activeThematique === 'territoire'" class="thematique-content">
<div class="row">
<div class="col-lg-6">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="bar-chart"></span> Montants
par commune</h3>
</div>
<div class="chart-content">
<apx-chart [series]="barCommuneOptions.series"
[chart]="barCommuneOptions.chart" [xaxis]="barCommuneOptions.xaxis"
[yaxis]="barCommuneOptions.yaxis" [colors]="barCommuneOptions.colors"
[legend]="barCommuneOptions.legend" [stroke]="barCommuneOptions.stroke"
[markers]="barCommuneOptions.markers" [grid]="barCommuneOptions.grid"
[dataLabels]="barCommuneOptions.dataLabels"
[plotOptions]="barCommuneOptions.plotOptions"
[tooltip]="barCommuneOptions.tooltip" [fill]="barCommuneOptions.fill">
</apx-chart>
</div>
</nz-card>
</div>
<div class="col-lg-6">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="bank"></span> Montants par
structure</h3>
</div>
<div class="chart-content">
<apx-chart [series]="barStructureOptions.series"
[chart]="barStructureOptions.chart" [xaxis]="barStructureOptions.xaxis"
[yaxis]="barStructureOptions.yaxis"
[colors]="barStructureOptions.colors"
[legend]="barStructureOptions.legend"
[stroke]="barStructureOptions.stroke"
[markers]="barStructureOptions.markers"
[grid]="barStructureOptions.grid"
[dataLabels]="barStructureOptions.dataLabels"
[plotOptions]="barStructureOptions.plotOptions"
[tooltip]="barStructureOptions.tooltip"
[fill]="barStructureOptions.fill">
</apx-chart>
</div>
</nz-card>
</div>
</div>
<div class="row mt-3">
<div class="col-lg-12">
<nz-card class="stats-table-card">
<div class="table-header">
<h3 class="table-title"><span nz-icon nzType="table"></span> Détail par
commune</h3>
</div>
<nz-table #communeTable [nzData]="statsParCommune" [nzPageSize]="5">
<thead>
<tr>
<th>Commune</th>
<th nzAlign="right">Montant TFU</th>
<th nzAlign="right">Montant IRF</th>
<th nzAlign="right">Total</th>
<th nzAlign="center">Taux recouvrement</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of communeTable.data">
<td><span class="fonction-name">{{ item.commune }}</span></td>
<td nzAlign="right" class="montant-cell">
{{ formatMontantCourt(item.montantTFU) }}</td>
<td nzAlign="right" class="montant-cell-green">
{{ formatMontantCourt(item.montantIRF) }}</td>
<td nzAlign="right"><strong
class="montant-total">{{ formatMontantCourt(item.montantTotal) }}</strong>
</td>
<td nzAlign="center">
<nz-progress [nzPercent]="item.tauxRecouvrement"
[nzStrokeColor]="getProgressColor(item.tauxRecouvrement)"
[nzShowInfo]="true" nzSize="small">
</nz-progress>
</td>
</tr>
</tbody>
</nz-table>
</nz-card>
</div>
</div>
</div>
<!-- ── DETTE FISCALE ── -->
<div *ngIf="activeThematique === 'dette'" class="thematique-content">
<div class="row">
<div class="col-lg-4">
<nz-card class="alert-card">
<div class="alert-content alert-danger">
<span nz-icon nzType="warning" nzTheme="fill" class="alert-icon"></span>
<div class="alert-body">
<div class="alert-title">Solde restant total</div>
<div class="alert-value">{{ formatMontantCourt(totalSoldeRestant) }}
</div>
<div class="alert-desc">Montant non encore recouvré sur l'ensemble des
périodes.</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-4">
<nz-card class="alert-card">
<div class="alert-content alert-success">
<span nz-icon nzType="check-circle" nzTheme="fill"
class="alert-icon"></span>
<div class="alert-body">
<div class="alert-title">Montant recouvré total</div>
<div class="alert-value">{{ formatMontantCourt(totalDetteRecouvree) }}
</div>
<div class="alert-desc">Somme des montants effectivement recouvrés.
</div>
</div>
</div>
</nz-card>
</div>
<div class="col-lg-4">
<nz-card class="alert-card">
<div class="alert-content alert-info">
<span nz-icon nzType="rise" nzTheme="outline" class="alert-icon"></span>
<div class="alert-body">
<div class="alert-title">Taux moyen de recouvrement</div>
<div class="alert-value">{{ tauxMoyenRecouvrement.toFixed(1) }}%</div>
<div class="alert-desc">Moyenne sur toutes les communes et périodes.
</div>
</div>
</div>
</nz-card>
</div>
</div>
<div class="row mt-3">
<div class="col-lg-8">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="bar-chart"></span> Dette
fiscale par commune et par année</h3>
</div>
<div class="chart-content">
<apx-chart [series]="barDetteOptions.series" [chart]="barDetteOptions.chart"
[xaxis]="barDetteOptions.xaxis" [yaxis]="barDetteOptions.yaxis"
[colors]="barDetteOptions.colors" [legend]="barDetteOptions.legend"
[stroke]="barDetteOptions.stroke" [markers]="barDetteOptions.markers"
[grid]="barDetteOptions.grid" [dataLabels]="barDetteOptions.dataLabels"
[plotOptions]="barDetteOptions.plotOptions"
[tooltip]="barDetteOptions.tooltip" [fill]="barDetteOptions.fill">
</apx-chart>
</div>
</nz-card>
</div>
<div class="col-lg-4">
<nz-card class="stats-table-card">
<div class="table-header">
<h3 class="table-title"><span nz-icon nzType="table"></span> Détail dette
</h3>
</div>
<nz-table #detteTable [nzData]="statsDette" [nzPageSize]="6" nzSize="small">
<thead>
<tr>
<th>Année</th>
<th>Commune</th>
<th nzAlign="center">Taux</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of detteTable.data">
<td><nz-tag [nzColor]="'blue'">{{ item.annee }}</nz-tag></td>
<td class="fonction-name">{{ item.commune }}</td>
<td nzAlign="center">
<nz-progress [nzPercent]="item.tauxRecouvrement"
[nzStrokeColor]="getProgressColor(item.tauxRecouvrement)"
[nzShowInfo]="true" nzSize="small">
</nz-progress>
</td>
</tr>
</tbody>
</nz-table>
</nz-card>
</div>
</div>
</div>
<!-- ── PATRIMOINE ── -->
<div *ngIf="activeThematique === 'patrimoine'" class="thematique-content">
<div class="row">
<div class="col-lg-6">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="bar-chart"></span> TFU par
standing de bâtiment</h3>
</div>
<div class="chart-content" style="min-height: 280px;">
<apx-chart [series]="barStandingOptions.series"
[chart]="barStandingOptions.chart" [xaxis]="barStandingOptions.xaxis"
[yaxis]="barStandingOptions.yaxis" [colors]="barStandingOptions.colors"
[legend]="barStandingOptions.legend"
[stroke]="barStandingOptions.stroke"
[markers]="barStandingOptions.markers" [grid]="barStandingOptions.grid"
[dataLabels]="barStandingOptions.dataLabels"
[plotOptions]="barStandingOptions.plotOptions"
[tooltip]="barStandingOptions.tooltip" [fill]="barStandingOptions.fill">
</apx-chart>
</div>
</nz-card>
</div>
<div class="col-lg-6">
<nz-card class="chart-card" [nzLoading]="loading">
<div class="chart-header">
<h3 class="chart-title"><span nz-icon nzType="stop"></span> Exhonérations
fiscales</h3>
</div>
<div class="chart-content" style="min-height: 280px;">
<apx-chart [series]="barExhonerationOptions.series"
[chart]="barExhonerationOptions.chart"
[xaxis]="barExhonerationOptions.xaxis"
[yaxis]="barExhonerationOptions.yaxis"
[colors]="barExhonerationOptions.colors"
[legend]="barExhonerationOptions.legend"
[stroke]="barExhonerationOptions.stroke"
[markers]="barExhonerationOptions.markers"
[grid]="barExhonerationOptions.grid"
[dataLabels]="barExhonerationOptions.dataLabels"
[plotOptions]="barExhonerationOptions.plotOptions"
[tooltip]="barExhonerationOptions.tooltip"
[fill]="barExhonerationOptions.fill">
</apx-chart>
</div>
</nz-card>
</div>
</div>
<div class="row mt-3">
<div class="col-lg-12">
<nz-card class="stats-table-card">
<div class="table-header">
<h3 class="table-title"><span nz-icon nzType="table"></span> Détail par
standing</h3>
</div>
<nz-table #standingTable [nzData]="statsStanding" [nzShowPagination]="false"
nzSize="small">
<thead>
<tr>
<th>Standing</th>
<th nzAlign="center">Nb. bâtiments</th>
<th nzAlign="right">Montant TFU</th>
<th nzAlign="right">Superficie</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of standingTable.data">
<td><span class="fonction-name">{{ item.standing }}</span></td>
<td nzAlign="center"><nz-tag
[nzColor]="'blue'">{{ item.nombreBatiments | number:'1.0-0':'fr' }}</nz-tag>
</td>
<td nzAlign="right" class="montant-cell">
{{ formatMontantCourt(item.montantTFU) }}</td>
<td nzAlign="right">{{ item.superficie | number:'1.0-0':'fr' }} m²
</td>
</tr>
</tbody>
</nz-table>
</nz-card>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,575 @@
import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { NzMessageService } from 'ng-zorro-antd/message';
import {
ChartComponent,
ApexChart, ApexAxisChartSeries, ApexXAxis, ApexYAxis,
ApexLegend, ApexStroke, ApexMarkers, ApexGrid,
ApexDataLabels, ApexTooltip, ApexPlotOptions,
ApexNonAxisChartSeries, ApexResponsive, ApexFill
} from 'ng-apexcharts';
// ── Interfaces ────────────────────────────────────────────────
export interface StatLiquidationGlobale {
totalLiquidations: number;
enCours: number;
generationAutorisee: number;
rejete: number;
genere: number;
cloture: number;
totalMontantTFU: number;
totalMontantIRF: number;
totalMontantGeneral: number;
totalParcelles: number;
totalParcellesBaties: number;
totalParcellesExhonerees: number;
totalBatiments: number;
totalUnitesLogement: number;
totalPiscines: number;
superficieTotale: number;
}
export interface StatParAnnee {
annee: number;
montantTFU: number;
montantIRF: number;
montantTotal: number;
nombreDossiers: number;
evolution: number; // % vs année précédente
}
export interface StatParCommune {
commune: string;
montantTFU: number;
montantIRF: number;
montantTotal: number;
nombreParcelles: number;
tauxRecouvrement: number;
}
export interface StatParStructure {
structure: string;
montantTFU: number;
montantIRF: number;
montantTotal: number;
nombreDossiers: number;
tauxRecouvrement: number;
}
export interface StatParNatureImpot {
nature: string;
code: 'TFU' | 'IRF';
couleur: string;
montant: number;
pourcentage: number;
nombreAssujettis: number;
}
export interface StatDetteFiscale {
annee: number;
commune: string;
detteInitiale: number;
detteRecouvree: number;
soldeRestant: number;
tauxRecouvrement: number;
}
export interface StatParStanding {
standing: string;
nombreBatiments: number;
montantTFU: number;
superficie: number;
}
export interface StatExhoneration {
categorie: string;
nombreExhoneres: number;
montantExhonere: number;
pourcentage: number;
}
export type PieChartOptions = {
series: ApexNonAxisChartSeries; chart: ApexChart;
labels: string[]; colors: string[]; legend: ApexLegend;
plotOptions: ApexPlotOptions; dataLabels: ApexDataLabels;
responsive: ApexResponsive[];
};
export type BarChartOptions = {
series: ApexAxisChartSeries; chart: ApexChart;
xaxis: ApexXAxis; yaxis: ApexYAxis | ApexYAxis[];
colors: string[]; legend: ApexLegend; stroke: ApexStroke;
markers: ApexMarkers; grid: ApexGrid; dataLabels: ApexDataLabels;
tooltip: ApexTooltip; plotOptions: ApexPlotOptions; fill: ApexFill;
};
export type LineChartOptions = {
series: ApexAxisChartSeries; chart: ApexChart;
xaxis: ApexXAxis; yaxis: ApexYAxis; colors: string[];
legend: ApexLegend; stroke: ApexStroke; markers: ApexMarkers;
grid: ApexGrid; dataLabels: ApexDataLabels; tooltip: ApexTooltip;
fill: ApexFill;
};
@Component({
selector: 'app-sommaire-consultation',
templateUrl: './sommaire-consultation.component.html',
styleUrls: ['./sommaire-consultation.component.css'],
encapsulation: ViewEncapsulation.None
})
export class SommaireConsultationComponent implements OnInit {
@ViewChild('chart') chart!: ChartComponent;
loading = false;
activeThematique = 'vue-globale';
readonly statusLabels: { [key: string]: string } = {
'EN_COURS': 'EN COURS',
'GENERATION_AUTORISE': 'GÉNÉRATION AUTORISÉE',
'REJETE': 'REJETÉ',
'GENERE': 'GÉNÉRÉ',
'CLOTURE': 'CLÔTURÉ'
};
readonly statusColors: { [key: string]: string } = {
'EN_COURS': '#f59e0b',
'GENERATION_AUTORISE': '#1a5890',
'REJETE': '#ef4444',
'GENERE': '#06b6d4',
'CLOTURE': '#10b981'
};
// ── Data ──────────────────────────────────────────────────────────────
statsGlobales: StatLiquidationGlobale = this.initStatsGlobales();
statsParAnnee: StatParAnnee[] = [];
statsParCommune: StatParCommune[] = [];
statsParStructure: StatParStructure[] = [];
statsParNature: StatParNatureImpot[] = [];
statsDette: StatDetteFiscale[] = [];
statsStanding: StatParStanding[] = [];
statsExhoneration: StatExhoneration[] = [];
// ── Charts ────────────────────────────────────────────────────────────
pieStatutsOptions: Partial<PieChartOptions> = {};
pieNatureOptions: Partial<PieChartOptions> = {};
lineEvolutionOptions: Partial<LineChartOptions> = {};
barCommuneOptions: Partial<BarChartOptions> = {};
barStructureOptions: Partial<BarChartOptions> = {};
barStandingOptions: Partial<BarChartOptions> = {};
barDetteOptions: Partial<BarChartOptions> = {};
barExhonerationOptions: Partial<BarChartOptions> = {};
constructor(private message: NzMessageService) { }
ngOnInit(): void { this.loadData(); }
initStatsGlobales(): StatLiquidationGlobale {
return {
totalLiquidations: 0, enCours: 0, generationAutorisee: 0,
rejete: 0, genere: 0, cloture: 0,
totalMontantTFU: 0, totalMontantIRF: 0, totalMontantGeneral: 0,
totalParcelles: 0, totalParcellesBaties: 0, totalParcellesExhonerees: 0,
totalBatiments: 0, totalUnitesLogement: 0, totalPiscines: 0,
superficieTotale: 0
};
}
loadData(): void {
this.loading = true;
setTimeout(() => {
this.statsGlobales = {
totalLiquidations: 142, enCours: 28, generationAutorisee: 15,
rejete: 8, genere: 61, cloture: 30,
totalMontantTFU: 487_650_000, totalMontantIRF: 213_440_000,
totalMontantGeneral: 701_090_000,
totalParcelles: 4820, totalParcellesBaties: 2974,
totalParcellesExhonerees: 312, totalBatiments: 3210,
totalUnitesLogement: 8640, totalPiscines: 47,
superficieTotale: 1_248_600
};
this.statsParAnnee = [
{ annee: 2019, montantTFU: 310_000_000, montantIRF: 120_000_000, montantTotal: 430_000_000, nombreDossiers: 18, evolution: 0 },
{ annee: 2020, montantTFU: 345_000_000, montantIRF: 138_000_000, montantTotal: 483_000_000, nombreDossiers: 24, evolution: 12.3 },
{ annee: 2021, montantTFU: 389_000_000, montantIRF: 155_000_000, montantTotal: 544_000_000, nombreDossiers: 31, evolution: 12.6 },
{ annee: 2022, montantTFU: 421_000_000, montantIRF: 178_000_000, montantTotal: 599_000_000, nombreDossiers: 38, evolution: 10.1 },
{ annee: 2023, montantTFU: 456_000_000, montantIRF: 196_000_000, montantTotal: 652_000_000, nombreDossiers: 45, evolution: 8.8 },
{ annee: 2024, montantTFU: 487_650_000, montantIRF: 213_440_000, montantTotal: 701_090_000, nombreDossiers: 54, evolution: 7.5 },
];
this.statsParCommune = [
{ commune: 'Cotonou', montantTFU: 198_000_000, montantIRF: 87_000_000, montantTotal: 285_000_000, nombreParcelles: 1550, tauxRecouvrement: 82 },
{ commune: 'Porto-Novo', montantTFU: 112_000_000, montantIRF: 48_000_000, montantTotal: 160_000_000, nombreParcelles: 1150, tauxRecouvrement: 75 },
{ commune: 'Parakou', montantTFU: 87_000_000, montantIRF: 38_000_000, montantTotal: 125_000_000, nombreParcelles: 900, tauxRecouvrement: 68 },
{ commune: 'Abomey-Calavi', montantTFU: 68_000_000, montantIRF: 28_000_000, montantTotal: 96_000_000, nombreParcelles: 700, tauxRecouvrement: 71 },
{ commune: 'Natitingou', montantTFU: 22_650_000, montantIRF: 12_440_000, montantTotal: 35_090_000, nombreParcelles: 520, tauxRecouvrement: 79 },
];
this.statsParStructure = [
{ structure: 'DGI Cotonou', montantTFU: 198_000_000, montantIRF: 87_000_000, montantTotal: 285_000_000, nombreDossiers: 42, tauxRecouvrement: 82 },
{ structure: 'DGI Porto-Novo', montantTFU: 112_000_000, montantIRF: 48_000_000, montantTotal: 160_000_000, nombreDossiers: 35, tauxRecouvrement: 75 },
{ structure: 'Centre Impôts Sud', montantTFU: 87_000_000, montantIRF: 38_000_000, montantTotal: 125_000_000, nombreDossiers: 28, tauxRecouvrement: 68 },
{ structure: 'Centre Impôts Nord', montantTFU: 68_000_000, montantIRF: 28_000_000, montantTotal: 96_000_000, nombreDossiers: 22, tauxRecouvrement: 71 },
{ structure: 'Service Calavi', montantTFU: 22_650_000, montantIRF: 12_440_000, montantTotal: 35_090_000, nombreDossiers: 15, tauxRecouvrement: 79 },
];
this.statsParNature = [
{ nature: 'TFU — Taxe Foncière Unique', code: 'TFU', couleur: '#1a5890', montant: 487_650_000, pourcentage: 69.6, nombreAssujettis: 3210 },
{ nature: 'IRF — Impôt sur Revenu Foncier', code: 'IRF', couleur: '#10b981', montant: 213_440_000, pourcentage: 30.4, nombreAssujettis: 1840 },
];
this.statsDette = [
{ annee: 2022, commune: 'Cotonou', detteInitiale: 285_000_000, detteRecouvree: 233_700_000, soldeRestant: 51_300_000, tauxRecouvrement: 82 },
{ annee: 2022, commune: 'Porto-Novo', detteInitiale: 160_000_000, detteRecouvree: 120_000_000, soldeRestant: 40_000_000, tauxRecouvrement: 75 },
{ annee: 2023, commune: 'Cotonou', detteInitiale: 310_000_000, detteRecouvree: 264_550_000, soldeRestant: 45_450_000, tauxRecouvrement: 85 },
{ annee: 2023, commune: 'Porto-Novo', detteInitiale: 175_000_000, detteRecouvree: 140_000_000, soldeRestant: 35_000_000, tauxRecouvrement: 80 },
{ annee: 2024, commune: 'Cotonou', detteInitiale: 340_000_000, detteRecouvree: 278_000_000, soldeRestant: 62_000_000, tauxRecouvrement: 82 },
{ annee: 2024, commune: 'Abomey-Calavi', detteInitiale: 96_000_000, detteRecouvree: 68_160_000, soldeRestant: 27_840_000, tauxRecouvrement: 71 },
];
this.statsStanding = [
{ standing: 'Standing A', nombreBatiments: 420, montantTFU: 198_000_000, superficie: 312_000 },
{ standing: 'Standing B', nombreBatiments: 860, montantTFU: 156_000_000, superficie: 487_000 },
{ standing: 'Standing C', nombreBatiments: 1240, montantTFU: 89_650_000, superficie: 298_000 },
{ standing: 'Standing D', nombreBatiments: 690, montantTFU: 44_000_000, superficie: 151_600 },
];
this.statsExhoneration = [
{ categorie: 'Parcelles exhonérées', nombreExhoneres: 312, montantExhonere: 48_200_000, pourcentage: 6.5 },
{ categorie: 'Bâtiments exhonérés', nombreExhoneres: 184, montantExhonere: 31_500_000, pourcentage: 5.7 },
{ categorie: 'Unités logement exhonérées', nombreExhoneres: 423, montantExhonere: 22_800_000, pourcentage: 4.9 },
];
this.loading = false;
this.buildAllCharts();
}, 800);
}
buildAllCharts(): void {
this.buildPieStatuts();
this.buildPieNature();
this.buildLineEvolution();
this.buildBarCommune();
this.buildBarStructure();
this.buildBarStanding();
this.buildBarDette();
this.buildBarExhoneration();
}
buildPieStatuts(): void {
const statuts = [
{ label: 'En cours', val: this.statsGlobales.enCours, col: '#f59e0b' },
{ label: 'Génération autorisée', val: this.statsGlobales.generationAutorisee, col: '#1a5890' },
{ label: 'Rejeté', val: this.statsGlobales.rejete, col: '#ef4444' },
{ label: 'Généré', val: this.statsGlobales.genere, col: '#06b6d4' },
{ label: 'Clôturé', val: this.statsGlobales.cloture, col: '#10b981' },
];
this.pieStatutsOptions = {
series: statuts.map(s => s.val),
chart: { type: 'donut', height: 320, fontFamily: 'Inter, sans-serif', animations: { enabled: true, speed: 700 } },
labels: statuts.map(s => s.label),
colors: statuts.map(s => s.col),
legend: { position: 'bottom', fontSize: '12px' },
plotOptions: {
pie: {
donut: {
size: '65%',
labels: {
show: true,
total: {
show: true, label: 'Total', fontSize: '13px',
formatter: (w: any) => w.globals.seriesTotals.reduce((a: number, b: number) => a + b, 0) + ' liquid.'
}
}
}
}
},
dataLabels: { enabled: false },
responsive: [{ breakpoint: 480, options: { chart: { height: 260 } } }]
};
}
buildPieNature(): void {
this.pieNatureOptions = {
series: this.statsParNature.map(n => n.montant),
chart: { type: 'pie', height: 320, fontFamily: 'Inter, sans-serif', animations: { enabled: true, speed: 700 } },
labels: this.statsParNature.map(n => n.nature),
colors: this.statsParNature.map(n => n.couleur),
legend: { position: 'bottom', fontSize: '12px' },
plotOptions: { pie: { expandOnClick: true } },
dataLabels: {
enabled: true, formatter: (val: number) => val.toFixed(1) + '%',
style: { fontSize: '12px', fontWeight: 700, colors: ['#fff'] }
},
responsive: [{ breakpoint: 480, options: { chart: { height: 260 } } }]
};
}
buildLineEvolution(): void {
this.lineEvolutionOptions = {
series: [
{ name: 'TFU (FCFA)', data: this.statsParAnnee.map(a => a.montantTFU) },
{ name: 'IRF (FCFA)', data: this.statsParAnnee.map(a => a.montantIRF) },
{ name: 'Total (FCFA)', data: this.statsParAnnee.map(a => a.montantTotal) },
],
chart: {
type: 'line', height: 360, fontFamily: 'Inter, sans-serif',
toolbar: { show: true }, animations: { enabled: true, speed: 700 }
},
xaxis: {
categories: this.statsParAnnee.map(a => a.annee.toString()),
labels: { style: { colors: '#6b7280', fontSize: '12px' } }
},
yaxis: {
labels: {
formatter: (val: number) => this.formatMontantCourt(val),
style: { colors: '#6b7280', fontSize: '11px' }
}
},
colors: ['#1a5890', '#10b981', '#f59e0b'],
legend: { position: 'bottom', fontSize: '13px' },
stroke: { curve: 'smooth', width: 3 },
markers: { size: 6, strokeWidth: 2, strokeColors: '#fff', hover: { size: 8 } },
grid: { borderColor: '#e0ecf8', strokeDashArray: 4 },
dataLabels: { enabled: false },
tooltip: {
shared: true, intersect: false,
y: { formatter: (val: number) => this.formatMontant(val) }
},
fill: { type: 'solid', opacity: 1 }
};
}
buildBarCommune(): void {
this.barCommuneOptions = {
series: [
{ name: 'TFU', data: this.statsParCommune.map(c => c.montantTFU) },
{ name: 'IRF', data: this.statsParCommune.map(c => c.montantIRF) },
],
chart: {
type: 'bar', height: 340, stacked: true, fontFamily: 'Inter, sans-serif',
toolbar: { show: true }, animations: { enabled: true, speed: 700 }
},
plotOptions: { bar: { horizontal: false, columnWidth: '55%', borderRadius: 4 } },
xaxis: {
categories: this.statsParCommune.map(c => c.commune),
labels: { style: { colors: '#6b7280', fontSize: '12px' } }
},
yaxis: {
labels: {
formatter: (val: number) => this.formatMontantCourt(val),
style: { colors: '#6b7280' }
}
},
colors: ['#1a5890', '#10b981'],
legend: { position: 'bottom', fontSize: '13px' },
stroke: { show: false, width: 0, colors: ['transparent'] },
markers: { size: 0 },
grid: { borderColor: '#e0ecf8', strokeDashArray: 4 },
dataLabels: { enabled: false },
tooltip: { shared: true, intersect: false, y: { formatter: (val: number) => this.formatMontant(val) } },
fill: { opacity: 1 }
};
}
buildBarStructure(): void {
this.barStructureOptions = {
series: [
{ name: 'TFU', data: this.statsParStructure.map(s => s.montantTFU) },
{ name: 'IRF', data: this.statsParStructure.map(s => s.montantIRF) },
],
chart: {
type: 'bar', height: 340, stacked: true, fontFamily: 'Inter, sans-serif',
toolbar: { show: true }, animations: { enabled: true, speed: 700 }
},
plotOptions: { bar: { horizontal: true, borderRadius: 4 } },
xaxis: {
categories: this.statsParStructure.map(s => s.structure),
labels: {
formatter: (val: string) => this.formatMontantCourt(Number(val)),
style: { colors: '#6b7280', fontSize: '11px' }
}
},
yaxis: { labels: { style: { colors: '#6b7280', fontSize: '12px' } } },
colors: ['#1a5890', '#10b981'],
legend: { position: 'bottom', fontSize: '13px' },
stroke: { show: false, width: 0, colors: ['transparent'] },
markers: { size: 0 },
grid: { borderColor: '#e0ecf8', strokeDashArray: 4 },
dataLabels: {
enabled: true,
formatter: (val: number) => val > 10_000_000 ? this.formatMontantCourt(val) : '',
style: { fontSize: '10px', fontWeight: 600, colors: ['#fff'] }
},
tooltip: { shared: true, intersect: false, y: { formatter: (val: number) => this.formatMontant(val) } },
fill: { opacity: 1 }
};
}
buildBarStanding(): void {
this.barStandingOptions = {
series: [
{ name: 'Montant TFU', data: this.statsStanding.map(s => s.montantTFU) },
{ name: 'Nb. bâtiments', data: this.statsStanding.map(s => s.nombreBatiments) },
],
chart: {
type: 'bar', height: 320, fontFamily: 'Inter, sans-serif',
toolbar: { show: false }, animations: { enabled: true, speed: 700 }
},
plotOptions: { bar: { horizontal: false, columnWidth: '50%', borderRadius: 4 } },
xaxis: {
categories: this.statsStanding.map(s => s.standing),
labels: { style: { colors: '#6b7280', fontSize: '12px' } }
},
yaxis: [
{
title: { text: 'Montant (FCFA)', style: { color: '#1a5890' } },
labels: { formatter: (val: number) => this.formatMontantCourt(val) }
},
{
opposite: true, title: { text: 'Nb. bâtiments', style: { color: '#10b981' } },
labels: { formatter: (val: number) => val.toFixed(0) }
}
],
colors: ['#1a5890', '#10b981'],
legend: { position: 'bottom', fontSize: '13px' },
stroke: { show: true, width: [0, 2], colors: ['transparent', '#10b981'] },
markers: { size: 0 },
grid: { borderColor: '#e0ecf8', strokeDashArray: 4 },
dataLabels: { enabled: false },
tooltip: { shared: true, intersect: false },
fill: { opacity: 1 }
};
}
buildBarDette(): void {
const annees = [...new Set(this.statsDette.map(d => d.annee.toString()))];
const communes = [...new Set(this.statsDette.map(d => d.commune))];
this.barDetteOptions = {
series: [
{ name: 'Dette initiale', data: this.statsDette.map(d => d.detteInitiale) },
{ name: 'Recouvrée', data: this.statsDette.map(d => d.detteRecouvree) },
{ name: 'Solde restant', data: this.statsDette.map(d => d.soldeRestant) },
],
chart: {
type: 'bar', height: 360, fontFamily: 'Inter, sans-serif',
toolbar: { show: true }, animations: { enabled: true, speed: 700 }
},
plotOptions: { bar: { horizontal: false, columnWidth: '60%', borderRadius: 3 } },
xaxis: {
categories: this.statsDette.map(d => d.annee + '\n' + d.commune),
labels: { style: { colors: '#6b7280', fontSize: '10px' } }
},
yaxis: {
labels: {
formatter: (val: number) => this.formatMontantCourt(val),
style: { colors: '#6b7280' }
}
},
colors: ['#1a5890', '#10b981', '#ef4444'],
legend: { position: 'bottom', fontSize: '13px' },
stroke: { show: true, width: 2, colors: ['transparent'] },
markers: { size: 0 },
grid: { borderColor: '#e0ecf8', strokeDashArray: 4 },
dataLabels: { enabled: false },
tooltip: { shared: true, intersect: false, y: { formatter: (val: number) => this.formatMontant(val) } },
fill: { opacity: 1 }
};
}
buildBarExhoneration(): void {
this.barExhonerationOptions = {
series: [
{ name: 'Nombre exhonérés', data: this.statsExhoneration.map(e => e.nombreExhoneres) },
{ name: 'Montant (FCFA)', data: this.statsExhoneration.map(e => e.montantExhonere) },
],
chart: {
type: 'bar', height: 280, fontFamily: 'Inter, sans-serif',
toolbar: { show: false }, animations: { enabled: true, speed: 700 }
},
plotOptions: { bar: { horizontal: false, columnWidth: '50%', borderRadius: 4 } },
xaxis: {
categories: this.statsExhoneration.map(e => e.categorie),
labels: { style: { colors: '#6b7280', fontSize: '11px' } }
},
yaxis: [
{ labels: { formatter: (val: number) => val.toFixed(0) } },
{ opposite: true, labels: { formatter: (val: number) => this.formatMontantCourt(val) } }
],
colors: ['#1a5890', '#f59e0b'],
legend: { position: 'bottom', fontSize: '12px' },
stroke: { show: true, width: 2, colors: ['transparent'] },
markers: { size: 0 },
grid: { borderColor: '#e0ecf8', strokeDashArray: 4 },
dataLabels: { enabled: false },
tooltip: { shared: true, intersect: false },
fill: { opacity: 1 }
};
}
// ── Utilitaires ───────────────────────────────────────────────────────
formatMontant(val: number): string {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'XOF', maximumFractionDigits: 0 }).format(val);
}
formatMontantCourt(val: number): string {
if (val >= 1_000_000_000) return (val / 1_000_000_000).toFixed(1) + ' Mrd';
if (val >= 1_000_000) return (val / 1_000_000).toFixed(1) + ' M';
if (val >= 1_000) return (val / 1_000).toFixed(0) + ' K';
return val.toFixed(0);
}
getTauxCloture(): number {
const total = this.statsGlobales.totalLiquidations;
return total > 0 ? Math.round((this.statsGlobales.cloture / total) * 100) : 0;
}
getTauxRejet(): number {
const total = this.statsGlobales.totalLiquidations;
return total > 0 ? Math.round((this.statsGlobales.rejete / total) * 100) : 0;
}
getProgressColor(percent: number): string {
if (percent >= 80) return '#10b981';
if (percent >= 65) return '#f59e0b';
return '#ef4444';
}
getMontantTotalFormatted(): string { return this.formatMontant(this.statsGlobales.totalMontantGeneral); }
getMontantTFUFormatted(): string { return this.formatMontant(this.statsGlobales.totalMontantTFU); }
getMontantIRFFormatted(): string { return this.formatMontant(this.statsGlobales.totalMontantIRF); }
refreshData(): void { this.loadData(); }
get totalSoldeRestant(): number {
return this.statsDette.reduce((a, d) => a + d.soldeRestant, 0);
}
get totalDetteRecouvree(): number {
return this.statsDette.reduce((a, d) => a + d.detteRecouvree, 0);
}
get tauxMoyenRecouvrement(): number {
if (!this.statsDette.length) return 0;
return this.statsDette.reduce((a, d) => a + d.tauxRecouvrement, 0) / this.statsDette.length;
}
getAnneeList(): number[] {
const annees = new Set<number>();
this.statsParAnnee.forEach(s => annees.add(s.annee));
return Array.from(annees).sort((a, b) => b - a);
}
getCommuneList(): string[] {
const communes = new Set<string>();
this.statsParCommune.forEach(s => communes.add(s.commune));
return Array.from(communes).sort();
}
getStructureList(): string[] {
const structures = new Set<string>();
this.statsParStructure.forEach(s => structures.add(s.structure));
return Array.from(structures).sort();
}
}

View File

@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashbordComponent } from './dashbord.component';
const routes: Routes = [
{ path: 'dashbord', component: DashbordComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DashbordRoutingModule { }

View File

@@ -0,0 +1,308 @@
<!--<div class="row" [ngClass]="isActionInProgress ? 'hidden-for-loading': 'visible-for-loading'">
<div class="col-lg-6 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4 class="card-title" style="font-size: 18px!important;font-weight: 900;">ÉVOLUTION DES ENQUÊTES</h4>
<p class="card-description">
Statistique de l'évolution des enquêtes par statut
</p>
<div class="table-responsive text-center" style="overflow-x: unset!important;">
<div class="text-center" id="chart" *ngIf="chartOptions && chartOptions.series">
<apx-chart [series]="chartOptions.series" [chart]="chartOptions.chart" [labels]="chartOptions.labels"
[responsive]="chartOptions.responsive" [fill]="fillColorList"></apx-chart>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4 class="card-title" style="font-size: 18px!important;font-weight: 900;">ÉVALUATION DES PERSONNES</h4>
<p class="card-description">
Statistique de l'évaluation du nombre de personne contacté par catégorie.
</p>
<div class="table-responsive text-center" style="overflow-x: unset!important;">
<div class="text-center" id="chartPersonne" *ngIf="chartPersonneOptions && chartPersonneOptions.series">
<apx-chart [series]="chartPersonneOptions.series" [chart]="chartPersonneOptions.chart" [labels]="chartPersonneOptions.labels"
[responsive]="chartPersonneOptions.responsive" [fill]="fillColorList"></apx-chart>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-4">
<div class="card card-statistics" style="border-bottom: solid 3px #e0bb62;">
<div class="card-body">
<div class="clearfix">
<div class="float-left">
<i class="mdi mdi-download text-warning icon-lg"></i>
</div>
<div class="float-right">
<div class="fluid-container">
<h3 class="font-weight-medium text-right mb-0">
{{ (nombreSynchronise ? nombreSynchronise.nombre : 0) | number:'':'fr-FR' }} </h3>
</div>
<p class="mb-0 text-right text-warning" style="font-size: 1.1em;">Finalisées <i
class="mdi mdi-arrow-up"></i> </p>
</div>
</div>
<p class="text-muted mt-3 mb-0 text-center">
<i class="mdi mdi-bookmark-outline mr-1" aria-hidden="true"></i> Nombre total des enquêtes synchronisées
</p>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card card-statistics" style="border-bottom: solid 3px #00ce68;">
<div class="card-body">
<div class="clearfix">
<div class="float-left">
<i class="mdi mdi-check-circle text-success icon-lg"></i>
</div>
<div class="float-right">
<div class="fluid-container">
<h3 class="font-weight-medium text-right mb-0">
{{ (nombreValide ? nombreValide.nombre : 0) | number:'':'fr-FR' }} </h3>
</div>
<p class="mb-0 text-right text-success" style="font-size: 1.1em;">Validées <i class="mdi mdi-arrow-up"></i>
</p>
</div>
</div>
<p class="text-muted mt-3 mb-0 text-center">
<i class="mdi mdi-bookmark-outline mr-1" aria-hidden="true"></i> Nombre total des enquêtes validées
</p>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card card-statistics" style="border-bottom: solid 3px #e65251;">
<div class="card-body">
<div class="clearfix">
<div class="float-left">
<i class="mdi mdi-cancel text-danger icon-lg"></i>
</div>
<div class="float-right">
<div class="fluid-container">
<h3 class="font-weight-medium text-right mb-0">
{{ (nombreRejete ? nombreRejete.nombre : 0) | number:'':'fr-FR' }} </h3>
</div>
<p class="mb-0 text-right text-danger" style="font-size: 1.1em;">Rejetées <i class="mdi mdi-arrow-down"></i>
</p>
</div>
</div>
<p class="text-muted mt-3 mb-0 text-center">
<i class="mdi mdi-bookmark-outline mr-1" aria-hidden="true"></i> Nombre total des enquêtes rejetées
</p>
</div>
</div>
</div>
</div>
<div class="row mt-3" [ngClass]="isActionInProgress ? 'hidden-for-loading': 'visible-for-loading'">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4 class="card-title" style="font-size: 18px!important;font-weight: 900;">POINT DES ENQUÊTES</h4>
<p class="card-description">
Point sur l'évolution du nombre des enquêtes par statut
</p>
<div class="table-responsive">
<nz-tabset *ngIf="isRoles(['ROLE_ADMIN'])">
<nz-tab nzTitle="Point des enquêtes des centres d'impôts">
<table class="table table-striped">
<thead>
<tr>
<th>
Centre d'impôts
</th>
<th class="text-center">
Nombre d'enquête total
</th>
<th class="text-center">
Nombre d'enquête validé
</th>
<th class="text-center">
Nombre d'enquête synchronisé
</th>
<th class="text-center">
Nombre d'enquête rejeté
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let todo of structureEnqueteList; let i=index" class="border-bottom-light">
<td>
{{ todo.structure }}
</td>
<td class="text-center">
<span class="badge badge-dark" style="font-size: 16px;">
{{ todo.total | number:'':'fr-FR' }}
<i class="mdi mdi-arrow-right"> </i>
</span>
</td>
<td class="text-center">
<span class="badge badge-success" style="font-size: 16px;">
<i class="mdi mdi-arrow-up"> </i> {{ todo.valide | number:'':'fr-FR' }}
</span>
</td>
<td class="text-center">
<span class="badge badge-info" style="font-size: 16px;">
<i class="mdi mdi-arrow-up"> </i> {{ todo.synchronise | number:'':'fr-FR' }}
</span>
</td>
<td class="text-center">
<span class="badge badge-danger" style="font-size: 16px;">
<i class="mdi mdi-arrow-down"> </i> {{ todo.rejet | number:'':'fr-FR' }}
</span>
</td>
</tr>
</tbody>
</table>
</nz-tab>
<nz-tab nzTitle="Point des enquêtes par arrondissement">
<div class="row">
<div class="col-lg-12">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Selectionner la commune"
(ngModelChange)="filterArrondissementByCommune($event)" [(ngModel)]="commune"
[compareWith]="compareFn">
<nz-option *ngFor="let item of communeList" [nzLabel]="item.nom" [nzValue]="item"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Commune <span class="text-danger"> *</span>
</label>
</div>
</div>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>
Arrondissement
</th>
<th class="text-center">
Nombre d'enquête total
</th>
<th class="text-center">
Nombre d'enquête validé
</th>
<th class="text-center">
Nombre d'enquête synchronisé
</th>
<th class="text-center">
Nombre d'enquête rejeté
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let todo of arrondissementEnqueteList; let i=index" class="border-bottom-light">
<td>
{{ todo.arrondissement }}
</td>
<td class="text-center">
<span class="badge badge-dark" style="font-size: 16px;">
{{ todo.total | number:'':'fr-FR' }}
<i class="mdi mdi-arrow-right"> </i>
</span>
</td>
<td class="text-center">
<span class="badge badge-success" style="font-size: 16px;">
<i class="mdi mdi-arrow-up"> </i> {{ todo.valide | number:'':'fr-FR' }}
</span>
</td>
<td class="text-center">
<span class="badge badge-info" style="font-size: 16px;">
<i class="mdi mdi-arrow-up"> </i> {{ todo.synchronise | number:'':'fr-FR' }}
</span>
</td>
<td class="text-center">
<span class="badge badge-danger" style="font-size: 16px;">
<i class="mdi mdi-arrow-down"> </i> {{ todo.rejet | number:'':'fr-FR' }}
</span>
</td>
</tr>
</tbody>
</table>
</nz-tab>
</nz-tabset>
<table class="table table-striped" *ngIf="isRoles(['ROLE_SUPERVISEUR', 'ROLE_DIRECTEUR', 'ROLE_ENQUETEUR'])">
<thead>
<tr>
<th>
Bloc
</th>
<th class="text-center">
Nombre d'enquête total
</th>
<th class="text-center">
Nombre d'enquête validé
</th>
<th class="text-center">
Nombre d'enquête synchronisé
</th>
<th class="text-center">
Nombre d'enquête rejeté
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let todo of blocEnqueteList; let i=index" class="border-bottom-light">
<td>
{{ todo.bloc }}
</td>
<td class="text-center">
<span class="badge badge-dark" style="font-size: 16px;">
{{ (todo.valide + todo.synchronise + todo.rejet) | number:'':'fr-FR' }}
<i class="mdi mdi-arrow-right"> </i>
</span>
</td>
<td class="text-center">
<span class="badge badge-success" style="font-size: 16px;">
<i class="mdi mdi-arrow-up"> </i> {{ todo.valide | number:'':'fr-FR' }}
</span>
</td>
<td class="text-center">
<span class="badge badge-info" style="font-size: 16px;">
<i class="mdi mdi-arrow-up"> </i> {{ todo.synchronise | number:'':'fr-FR' }}
</span>
</td>
<td class="text-center">
<span class="badge badge-danger" style="font-size: 16px;">
<i class="mdi mdi-arrow-down"> </i> {{ todo.rejet | number:'':'fr-FR' }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>-->

View File

@@ -0,0 +1,235 @@
import { HttpClient } from '@angular/common/http';
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { firstValueFrom } from 'rxjs';
import { CrudService } from 'src/app/crud.service';
import { GlobalService } from 'src/app/global.service';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
import { environment } from 'src/environments/environment';
import { ApexFill, ChartComponent } from "ng-apexcharts";
import {
ApexNonAxisChartSeries,
ApexResponsive,
ApexChart
} from "ng-apexcharts";
export type ChartOptions = {
series: ApexNonAxisChartSeries;
chart: ApexChart;
responsive: ApexResponsive[];
labels: any;
};
@Component({
selector: 'app-dashbord',
templateUrl: './dashbord.component.html',
styleUrls: ['./dashbord.component.css']
})
export class DashbordComponent implements OnInit {
communeList: any[] = [];
commune: any = null;
user: any = null;
isActionInProgress = false;
blocEnqueteList: any[] = [
{ libelle: 'C1373717633131', nombre: 120 },
{ libelle: 'C3286242849482', nombre: 230 },
{ libelle: 'C3286242849482', nombre: 50 }
];
arrondissementEnqueteList: any[] = [
{ libelle: 'C1373717633131', nombre: 120 },
{ libelle: 'C3286242849482', nombre: 230 },
{ libelle: 'C3286242849482', nombre: 50 }
];
structureEnqueteList: any[] = [
{ libelle: 'C1373717633131', nombre: 120 },
{ libelle: 'C3286242849482', nombre: 230 },
{ libelle: 'C3286242849482', nombre: 50 }
];
blocEnqueteAllStatutList: any[] = [
{ libelle: 'C1373717633131', valide: 120, rejete: 12, synchronise: 23 },
{ libelle: 'C3286242849482', valide: 120, rejete: 12, synchronise: 23 },
{ libelle: 'C3286242849482', valide: 120, rejete: 12, synchronise: 23 }
];
@ViewChild("chart") chart!: ChartComponent;
public chartOptions!: Partial<ChartOptions>;
@ViewChild("chartPersonne") chartPersonne!: ChartComponent;
public chartPersonneOptions!: Partial<ChartOptions>;
nombreValide: any = null;
nombreRejete: any = null;
nombreSynchronise: any = null;
fillColorList: ApexFill = {
colors: ["#00ce68", "#e0bb62", "#e65251"]
};
statPersonne: any = {
nbrePersonnePhysique: 10,
nbrePersonneMorale: 5,
nbrePersonneInformel: 3,
}
constructor(
private fb: FormBuilder,
private router: Router,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
private globalService: GlobalService,
private http: HttpClient,
private tokenStorage: TokenStorage,
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
async ngOnInit(): Promise<void> {
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
console.log(this.user);
this.globalService.setLodingSuccess(true);
if(this.isRoles(['ROLE_ADMIN'])) {
const resultStructure: any = await firstValueFrom(this.crudService.getAll('statistique/user/enquete-par-structure'));
console.log('resultStructure ===> ', resultStructure);
if(resultStructure && resultStructure.object) {
this.structureEnqueteList = resultStructure.object;
}
const communes = await firstValueFrom(this.http.get<any>(`${environment.backend}/commune/all`));
console.log('decoupages ===> ', communes);
if (communes && communes.object.length > 0) {
this.communeList = communes?.object;
this.commune = this.communeList[0];
this.filterArrondissementByCommune(this.communeList[0]);
}
}
if(this.isRoles(['ROLE_SUPERVISEUR', 'ROLE_DIRECTEUR', 'ROLE_ENQUETEUR'])) {
const resultBlocs: any = await firstValueFrom(this.crudService.getAll('statistique/user/enquete-par-bloc'));
console.log('resultBlocs ===> ', resultBlocs);
this.blocEnqueteList = resultBlocs.object;
const compareBlocFn = (a: any, b: any) => (a.id < b.id ? 0 : -1);
this.blocEnqueteList.sort(compareBlocFn);
}
const result: any = await firstValueFrom(this.crudService.getAll('statistique/user/enquete-par-statut'));
console.log('enquete ===> ', result);
if(result && result.object.length > 0) {
this.nombreValide = result.object.find((element: any) => element.statutEnquete == 'VALIDE');
this.nombreRejete = result.object.find((element: any) => element.statutEnquete == 'REJETE');
this.nombreSynchronise = result.object.find((element: any) => element.statutEnquete == 'FINALISE');
}
this.chartOptions = {
series: [(this.nombreValide ? this.nombreValide.nombre : 0), (this.nombreSynchronise ? this.nombreSynchronise.nombre : 0), (this.nombreRejete ? this.nombreRejete.nombre : 0)],
chart: {
width: '100%',
type: "pie"
},
labels: ["Validées", "Finalisées", "Rejetées"],
responsive: [
{
breakpoint: 480,
options: {
chart: {
width: 400
},
legend: {
position: "bottom"
}
}
}
]
};
this.chartPersonneOptions = {
series: [(this.statPersonne.nbrePersonnePhysique), (this.statPersonne.nbrePersonneMorale), (this.statPersonne.nbrePersonneInformel)],
chart: {
width: '100%',
type: "pie"
},
labels: ["Personne physique", "Personne morale", "Groupe informel"],
responsive: [
{
breakpoint: 480,
options: {
chart: {
width: 300
},
legend: {
position: "bottom"
}
}
}
]
};
this.globalService.setLodingSuccess(false);
}
compareFn = (o1: any, o2: any) => (o1 && o2 ? o1.id === o2.id : o1 === o2);
async filterArrondissementByCommune(event: any): Promise<void> {
this.commune = event;
if(this.commune && this.commune != null) {
this.globalService.setLodingSuccess(true);
const result: any = await firstValueFrom(this.crudService.getAll('statistique/enquete-par-arrondissement/'+this.commune.id));
console.log('arrondissement statistique ===> ', result);
this.arrondissementEnqueteList = result.object;
const compareBlocFn = (a: any, b: any) => (a.id < b.id ? 0 : -1);
this.arrondissementEnqueteList.sort(compareBlocFn);
this.globalService.setLodingSuccess(false);
}
}
isRoles(params: any[]): boolean {
if(this.user != null) {
return params.indexOf(this.user.roles[0]?.nom) > -1;
}
return false;
}
notIsRoles(params: any[]): boolean {
if(this.user != null) {
return params.indexOf(this.user.roles[0]?.nom) == -1;
}
return false;
}
}

View File

@@ -0,0 +1,27 @@
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DashbordRoutingModule } from './dashbord-routing.module';
import { DashbordComponent } from './dashbord.component';
import { SharedModule } from 'src/app/shared/shared.module';
import { NgApexchartsModule } from 'ng-apexcharts';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [
DashbordComponent,
],
imports: [
CommonModule,
DashbordRoutingModule,
FormsModule,
ReactiveFormsModule,
SharedModule,
NgApexchartsModule,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class DashbordModule { }

View File

@@ -0,0 +1,231 @@
<div class="row">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review">
<div class="row">
<div class="col-md-10" style="display: table;">
<h6 class="card-title"
style="font-size: 16px!important;text-transform: none;display: table-cell;vertical-align: middle;">
Filtre bârème TFU bâtie
</h6>
</div>
<div class="col-md-2">
<button class="btn btn-review btn-dark btn-outline-review btn-fw" (click)="showHideForm(true)"
style="float: right;">
<i class="mdi mdi-file-outline"> </i> Fiche bârème TFU bâtie
</button>
</div>
</div>
<nz-divider></nz-divider>
<div class="row">
<div class="col-lg-12">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Selectionner la catégorie de bâtiment"
(ngModelChange)="listBaremeTFUByCategorieBatiment($event)"
[(ngModel)]="categorieBatimentPaylod" [compareWith]="compareFn">
<nz-option *ngFor="let item of categorieBatimentList" [nzLabel]="item.nom"
[nzValue]="item"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Catégories de bâtiment <span
class="text-danger"> *</span> </label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row" *ngIf="isForm == true">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review mt-0">
<h6 class="card-title" style="font-size: 16px!important;text-transform: none;">Fiche de bârème pour le
calcul de la TFU des bâtiments de la {{ categorieBatimentPaylod? categorieBatimentPaylod.nom : '' }}
</h6>
<nz-divider></nz-divider>
<form [formGroup]="baremeTfuForm" *ngIf="baremeTfuForm">
<div class="row">
<div class="col-lg-4">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Selectionner la commune"
(ngModelChange)="filterArrondissementByCommune($event)" formControlName="communeId">
<nz-option *ngFor="let item of communeList" [nzLabel]="item.nom"
[nzValue]="item.id"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Commune <span
class="text-danger"> *</span> </label>
</div>
</div>
<div class="col-lg-4">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear
(ngModelChange)="filterQuartierByArrondissement($event)"
nzPlaceHolder="Selectionner l'arrondissement"
formControlName="arrondissementId">
<nz-option *ngFor="let item of arrondissementFilteredList" [nzLabel]="item.nom"
[nzValue]="item.id"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Arrondissement <span
class="text-danger"> *</span> </label>
</div>
</div>
<div class="col-lg-4">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear
nzPlaceHolder="Selectionner le quartier" formControlName="quartierId">
<nz-option *ngFor="let item of quartierFilteredList" [nzLabel]="item.nom"
[nzValue]="item.id"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Quartier <span
class="text-danger"> *</span> </label>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-4">
<div class="did-floating-label-content mt-3">
<input class="did-floating-input" type="text" id="valeurLocative"
formControlName="valeurLocative" placeholder="">
<label class="did-floating-label"> Valeur locative (F CFA) <span class="text-danger">
*</span> </label>
</div>
</div>
<div class="col-lg-4">
<div class="did-floating-label-content mt-3">
<input class="did-floating-input" type="text" id="tfuMetreCarre"
formControlName="tfuMetreCarre" placeholder="">
<label class="did-floating-label"> TFU au mètre carré (F CFA) <span class="text-danger">
*</span> </label>
</div>
</div>
<div class="col-lg-4">
<div class="did-floating-label-content mt-3">
<input class="did-floating-input" type="text" id="tfuMinimum"
formControlName="tfuMinimum" placeholder="">
<label class="did-floating-label"> TFU minimum (F CFA) <span class="text-danger">
*</span> </label>
</div>
</div>
</div>
<br>
<button class="btn btn-review btn-secondary btn-fw mr-2" (click)="showHideForm(false)"
[disabled]="isActionInProgress">
Fermer
</button>
<button class="btn btn-review btn-primary btn-fw" (click)="saveForm()"
[disabled]="isActionInProgress || categorieBatimentPaylod == null">
{{ isActionInProgress == false ? 'Enregistrer' : 'Opération en cours ...' }}
</button>
</form>
</div>
</div>
</div>
</div>
<div class="row" [ngClass]="isActionInProgress ? 'hidden-for-loading': 'visible-for-loading'">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4 class="card-title" style="font-size: 16px!important;margin-bottom: 0px;text-transform: unset;">
Liste des bârèmes TFU bâties </h4>
<p class="card-description text-gray" style="margin-bottom: 2%;line-height: 30px;font-size: 12px;">
Liste des différents barêmes pour le calcul de la TFU bâtie en fonction de la catégorie de bâtiment
<strong> {{ categorieBatimentPaylod? categorieBatimentPaylod.nom : '-' }} </strong> dans les
différentes communes et arrondissements.
</p>
<div class="table-responsive">
<table class="table table-striped" datatable [dtOptions]="dtOptions" [dtTrigger]="dtTrigger">
<thead>
<tr>
<th>
Commune
</th>
<th>
Arrondissement
</th>
<th>
Quartier
</th>
<th>
Valeur locative (f cfa)
</th>
<th>
TFU au mètre carré (f cfa)
</th>
<th>
TFU minimum (f cfa)
</th>
<th class="text-center">
Actions
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let todo of baremeTfuList; let i=index" class="border-bottom-light">
<td>
{{ todo.communeNom ? todo.communeNom : '-' }}
</td>
<td>
{{ todo.arrondissementNom ? todo.arrondissementNom : '-' }}
</td>
<td>
{{ todo.quartierNom ? todo.quartierNom : '-' }}
</td>
<td>
{{ todo.valeurLocative | number:'1.0-0': 'fr' }}
</td>
<td>
{{ todo.tfuMetreCarre | number:'1.0-0': 'fr' }}
</td>
<td>
{{ todo.tfuMinimum | number:'1.0-0': 'fr' }}
</td>
<td class="text-center">
<button type="button" class="btn btn-icons btn-rounded mr-1" nz-popover
style="border-color: #00ce68;background: white;"
[nzPopoverVisible]="visiblePopovers[todo.id] || false"
(nzPopoverVisibleChange)="visiblePopovers[todo.id] = $event"
nzPopoverPlacement="bottom" [nzPopoverContent]="contentTemplate"
[nzPopoverContext]="{ element: todo, i: i }" (click)="togglePopover(todo.id)">
<i class="mdi mdi-format-align-center btn-icon-modify"
style="color: #00ce68;"></i>
</button>
<ng-template #contentTemplate>
<nz-list>
<nz-list-item class="review-list-item-menu">
<a
(click)="makeForm(todo);scrollTo('formulaire');togglePopover(todo.id)">
<i class="mdi mdi-arrow-right"></i> Modifier élément
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a (click)="showDeleteConfirm(todo, i)">
<i class="mdi mdi-arrow-right"></i> Supprimer élément
</a>
</nz-list-item>
</nz-list>
</ng-template>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,341 @@
import { Component, ChangeDetectionStrategy, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DataTableDirective } from 'angular-datatables';
import { Subject } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { CrudService } from 'src/app/crud.service';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzMessageService } from 'ng-zorro-antd/message';
import { GlobalService } from 'src/app/global.service';
export interface BaremeTfu {
id: number;
valeurLocative: number;
tfuMetreCarre: number;
tfuMinimum: number;
categorieBatimentId: number;
categorieBatimentNom: string;
categorieBatimentStanding: string;
arrondissementId: number;
arrondissementCode: string;
arrondissementNom: string;
communeId: number;
communeCode: string;
communeNom: string;
quartierId: number;
quartierCode: string;
quartierNom: string;
}
@Component({
selector: 'app-bareme-tfu-bati',
templateUrl: './bareme-tfu-bati.component.html',
styleUrls: ['./bareme-tfu-bati.component.css']
})
export class BaremeTfuBatiComponent implements OnInit {
@ViewChild(DataTableDirective, { static: false })
dtElement?: DataTableDirective;
dtOptions: DataTables.Settings = {};
dtTrigger: Subject<any> = new Subject<any>();
isForm = false;
baremeTfuList: any[] = [];
categorieBatimentList: any[] = [];
communeList: any[] = [];
arrondissementList: any[] = [];
arrondissementFilteredList: any[] = [];
quartierFilteredList: any[] = [];
categorieBatimentPaylod: any = null;
baremeTfuForm?: FormGroup;
isActionInProgress: boolean = false;
visiblePopovers: { [key: number]: boolean } = {};
constructor(
private fb: FormBuilder,
private router: Router,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
private globalService: GlobalService
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
ngOnInit(): void {
this.globalService.setLodingSuccess(false);
this.crudService.getAll('commune/all').subscribe(
(data: any) => {
this.communeList = data != null && data.object ? data.object : [];
});
this.crudService.getAll('arrondissement/all').subscribe(
(data: any) => {
this.arrondissementList = data != null && data.object ? data.object : [];
});
this.crudService.getAll('categorie-batiment/all').subscribe(
(data: any) => {
this.categorieBatimentList = data != null && data.object ? data.object : [];
if (this.categorieBatimentList.length > 0) {
this.categorieBatimentPaylod = this.categorieBatimentList[0];
this.listBaremeTFUByCategorieBatiment(this.categorieBatimentPaylod);
}
});
this.dtOptions = {
pagingType: 'full_numbers',
pageLength: 10,
processing: true,
language: {
search: "Rechercher&nbsp;:",
emptyTable: "Aucune donnée disponible",
lengthMenu: "Afficher _MENU_ &eacute;l&eacute;ments",
info: "Affichage de l'&eacute;lement _START_ &agrave; _END_ sur _TOTAL_ &eacute;l&eacute;ments",
infoEmpty: "Affichage de l'&eacute;lement 0 &agrave; 0 sur 0 &eacute;l&eacute;ments",
infoFiltered: "(filtr&eacute; de _MAX_ &eacute;l&eacute;ments au total)",
paginate: {
first: "<i class='menu-icon mdi mdi-chevron-double-left'></i>",
previous: "<i class='menu-icon mdi mdi-chevron-left'></i>",
next: "<i class='menu-icon mdi mdi-chevron-right'></i>",
last: "<i class='menu-icon mdi mdi-chevron-double-right'></i>"
},
}
};
this.makeForm(null);
//this.list();
}
ngAfterViewInit(): void {
this.dtTrigger.next(this.dtOptions);
}
filterArrondissementByCommune(value: any): void {
console.log('value ==> ', value)
const communePaylod = this.baremeTfuForm?.get('communeId')?.value;
this.baremeTfuForm?.get('arrondissement')?.setValue(null);
this.arrondissementFilteredList = [];
if (communePaylod != null) {
this.arrondissementFilteredList = this.arrondissementList.filter((element) => element.communeId == communePaylod);
}
}
filterQuartierByArrondissement(value: any): void {
console.log('value ==> ', value)
const arrondissementPaylod = this.baremeTfuForm?.get('arrondissementId')?.value;
this.baremeTfuForm?.get('quartierId')?.setValue(null);
this.quartierFilteredList = [];
if (arrondissementPaylod != null) {
this.crudService.getAll('quartier/arrondissement/' + arrondissementPaylod).subscribe(
(data: any) => {
this.quartierFilteredList = data != null ? data.object : [];
});
}
}
compareFn = (o1: any, o2: any) => (o1 && o2 ? o1.id === o2.id : o1 === o2);
makeForm(baremeTfu: BaremeTfu | null): void {
this.baremeTfuForm = this.fb.group({
id: [baremeTfu != null ? baremeTfu.id : null],
valeurLocative: [baremeTfu != null ? baremeTfu.valeurLocative : null,
[Validators.required]],
tfuMetreCarre: [baremeTfu != null ? baremeTfu.tfuMetreCarre : null,
[Validators.required]],
tfuMinimum: [baremeTfu != null ? baremeTfu.tfuMinimum : null,
[Validators.required]],
communeId: [baremeTfu != null ? baremeTfu.communeId : null,
[Validators.required]],
categorieBatimentId: [baremeTfu != null ? baremeTfu.categorieBatimentId : null],
arrondissementId: [baremeTfu != null ? baremeTfu.arrondissementId : null,
[Validators.required]],
quartierId: [baremeTfu != null ? baremeTfu.quartierId : null],
});
if (baremeTfu != null) {
this.showHideForm(true);
}
}
resetForm(e: MouseEvent): void {
e.preventDefault();
this.baremeTfuForm?.reset();
for (const key in this.baremeTfuForm?.controls) {
this.baremeTfuForm?.controls[key].markAsPristine();
this.baremeTfuForm?.controls[key].updateValueAndValidity();
}
this.makeForm(null);
}
ngOnDestroy(): void {
this.dtTrigger.unsubscribe();
}
listBaremeTFUByCategorieBatiment(value: any): void {
this.categorieBatimentPaylod = value;
this.baremeTfuList = [];
this.refreshDataTable();
if (this.categorieBatimentPaylod != null) {
this.globalService.setLodingSuccess(true);
this.crudService.getAll('barem-rfu/by-categorie-batiment-id/' + this.categorieBatimentPaylod?.id).subscribe(
(data: any) => {
this.baremeTfuList = data != null ? data.object : [];
this.baremeTfuList = [...this.baremeTfuList];
this.refreshDataTable();
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
console.log('OK');
this.globalService.setLodingSuccess(false);
}
);
}
}
refreshDataTable(): void {
this.dtElement?.dtInstance.then((dtInstance: DataTables.Api) => {
// Destroy the table first
dtInstance.destroy();
// Call the dtTrigger to rerender again
this.dtTrigger.next(null);
});
}
showDeleteConfirm(element: any, index: number): void {
this.modal.confirm({
nzTitle: 'Confirmez-vous ?',
nzContent: 'suppression de cette barême de TFU bâti',
nzOkText: 'Oui',
nzOkType: 'primary',
nzOkDanger: true,
nzOnOk: () => {
this.globalService.setLodingSuccess(true);
this.crudService.deleteElement('barem-rfu/delete', element.id).subscribe(
(data: any) => {
this.baremeTfuList.splice(index, 1);
this.refreshDataTable();
this.message.create('success', `Suppression effectuée avec succès.`);
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.globalService.setLodingSuccess(false);
}
);
},
nzCancelText: 'Non',
nzOnCancel: () => console.log('Cancel')
});
}
showHideForm(value: boolean): void {
if (value == true && this.categorieBatimentPaylod == null) {
this.message.create('warning', `Veuillez sélectionner une catégorie de bâtiment pour afficher la fiche bârème TFU bâtie.`);
return;
}
this.isForm = value;
if (!value) {
this.makeForm(null);
}
}
saveForm(): void {
for (const i in this.baremeTfuForm?.controls) {
this.baremeTfuForm?.controls[i].markAsDirty();
this.baremeTfuForm?.controls[i].updateValueAndValidity();
}
if (this.baremeTfuForm?.valid) {
this.isActionInProgress = true;
const formData = this.baremeTfuForm?.value;
formData.categorieBatimentId = this.categorieBatimentPaylod?.id;
delete formData.communeId;
if (
formData.id == null ||
formData.id == undefined ||
formData.id == ''
) {
this.globalService.setLodingSuccess(true);
this.crudService.save('barem-rfu/create', formData).subscribe(
(data: any) => {
this.baremeTfuList.unshift(data.object);
this.message.create('success', `Enregistrement effectué avec succès.`);
this.makeForm(null);
this.refreshDataTable();
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.globalService.setLodingSuccess(false);
}
);
} else {
const i = this.baremeTfuList.findIndex(
(element) => element.id == formData.id
);
this.globalService.setLodingSuccess(true);
this.crudService.update('barem-rfu/update', formData).subscribe(
(data: any) => {
this.baremeTfuList.splice(i, 1);
this.baremeTfuList.unshift(data.object);
this.message.create('success', `Modification effectuée avec succès.`);
this.makeForm(null);
this.refreshDataTable();
this.showHideForm(false);
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.globalService.setLodingSuccess(false);
}
);
}
} else {
this.modal.error({
nzTitle: 'Erreur',
nzContent: 'Formulaire invalid. Un ou plusieurs champs sont vides...'
});
}
}
goto(url: string): void {
this.router.navigate([url]);
}
togglePopover(id: number | undefined) {
if (id == null) return; // sécurité
const wasOpen = this.visiblePopovers[id];
// Ferme tous les autres
Object.keys(this.visiblePopovers).forEach(key => {
this.visiblePopovers[Number(key)] = false;
});
// Toggle
this.visiblePopovers[id] = !wasOpen;
}
scrollTo(elementId: string): void {
const element = document.getElementById(elementId);
if (element) {
element.scrollIntoView({
behavior: 'smooth', // animation fluide
block: 'start', // aligner en haut de la vue
inline: 'nearest'
});
}
}
}

View File

@@ -0,0 +1,149 @@
<div class="row">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review">
<h6 class="card-title" style="font-size: 16px!important;text-transform: none;">Fiche de bârème pour le
calcul de la TFU des parcelles non bâties
</h6>
<nz-divider></nz-divider>
<form [formGroup]="baremeTfuForm" *ngIf="baremeTfuForm">
<div class="row">
<div class="col-lg-3">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Selectionner la commune"
formControlName="communeId">
<nz-option *ngFor="let item of communeList" [nzLabel]="item.nom"
[nzValue]="item.id"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Commune <span
class="text-danger"> *</span> </label>
</div>
</div>
<div class="col-lg-3">
<div class="did-floating-label-content mt-3">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Selectionner la zone"
formControlName="zoneRfuId">
<nz-option *ngFor="let item of zoneList" [nzLabel]="item.nom"
[nzValue]="item.id"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Zone RFU </label>
</div>
</div>
<div class="col-lg-3" style="display: flex;gap: 5px;">
<div class="did-floating-label-content mt-3" style="flex: 1;">
<input class="did-floating-input" type="number" id="taux" formControlName="taux"
placeholder="">
<label class="did-floating-label"> Taux (%) <span class="text-danger"> *</span> </label>
</div>
<div class="did-floating-label-content mt-3" style="flex: 1;">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Au mètre carrée ? "
(ngModelChange)="resetValeurAdministrative($event)"
formControlName="auMetreCarre">
<nz-option *ngFor="let item of reponseList" [nzLabel]="item.label"
[nzValue]="item.value"></nz-option>
</nz-select>
<label class="did-floating-label" style="top: -15px;"> Au mètre carrée ? </label>
</div>
</div>
<div class="col-lg-3" *ngIf="baremeTfuForm.get('auMetreCarre')?.value == false">
<div class="did-floating-label-content mt-3">
<input class="did-floating-input" type="number" id="valeurAdministrative"
formControlName="valeurAdministrative" placeholder="">
<label class="did-floating-label"> Valeur administrative (F CFA) <span
class="text-danger"> *</span> </label>
</div>
</div>
<div class="col-lg-3" *ngIf="baremeTfuForm && baremeTfuForm.get('auMetreCarre')?.value == true">
<div class="did-floating-label-content mt-3">
<input class="did-floating-input" type="number" id="valeurAdministrativeMetreCarre"
formControlName="valeurAdministrativeMetreCarre" placeholder="">
<label class="did-floating-label"> Valeur administrative (F CFA) / m² <span
class="text-danger"> *</span> </label>
</div>
</div>
</div>
<br>
<button class="btn btn-review btn-secondary btn-fw mr-2" (click)="showHideForm(false)"
[disabled]="isActionInProgress">
Fermer
</button>
<button class="btn btn-review btn-primary btn-fw" (click)="saveForm()"
[disabled]="isActionInProgress">
{{ isActionInProgress == false ? 'Enregistrer' : 'Opération en cours ...' }}
</button>
</form>
</div>
</div>
</div>
</div>
<div class="row" [ngClass]="isActionInProgress ? 'hidden-for-loading': 'visible-for-loading'">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4 class="card-title" style="font-size: 16px!important;margin-bottom: 0px;text-transform: unset;">
Liste des bârèmes TFU non bâties </h4>
<p class="card-description text-gray" style="margin-bottom: 2%;line-height: 30px;font-size: 12px;">
Liste des différents barêmes pour le calcul de la TFU des parcelles non bâties.
</p>
<div class="table-responsive">
<table class="table table-striped" datatable [dtOptions]="dtOptions" [dtTrigger]="dtTrigger">
<thead>
<tr>
<th>
Commune
</th>
<th>
Zone RFU
</th>
<th>
Valeur administrative (F CFA)
</th>
<th>
Taux (%)
</th>
<th class="text-center">
Actions
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let todo of baremeTfuList; let i=index" class="border-bottom-light">
<td style="width: 23%;">
{{ todo.communeNom ? todo.communeNom : '-' }}
</td>
<td style="width: 35%;">
{{ todo.zoneRfuNom ? todo.zoneRfuNom : '-' }}
</td>
<td>
{{ todo.auMetreCarre ? (todo.valeurAdministrativeMetreCarre | number:'1.0-0': 'fr') :(todo.valeurAdministrative | number:'1.0-0': 'fr') }} {{ todo.auMetreCarre ? ' / m²' : '' }}
</td>
<td>
{{ todo.taux }}
</td>
<td class="text-center">
<button type="button" class="btn btn-icons btn-rounded btn-success mr-1"
(click)="makeForm(todo)">
<i class="mdi mdi-pencil btn-icon-modify"></i>
</button>
<button type="button" class="btn btn-icons btn-rounded btn-danger"
(click)="showDeleteConfirm(todo, i)">
<i class="mdi mdi-delete btn-icon-modify"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,272 @@
import { Component, ChangeDetectionStrategy, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DataTableDirective } from 'angular-datatables';
import { Subject } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { CrudService } from 'src/app/crud.service';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzMessageService } from 'ng-zorro-antd/message';
import { GlobalService } from 'src/app/global.service';
export interface BaremeTfu {
id: number;
valeurAdministrative: number;
taux: number;
communeId: number;
communeCode: string;
communeNom: string;
zoneRfuId: number;
zoneRfuCode: string;
zoneRfuNom: string;
valeurAdministrativeMetreCarre: number;
auMetreCarre: boolean;
}
@Component({
selector: 'app-bareme-tfu-non-bati',
templateUrl: './bareme-tfu-non-bati.component.html',
styleUrls: ['./bareme-tfu-non-bati.component.css']
})
export class BaremeTfuNonBatiComponent implements OnInit {
@ViewChild(DataTableDirective, { static: false })
dtElement?: DataTableDirective;
dtOptions: DataTables.Settings = {};
dtTrigger: Subject<any> = new Subject<any>();
isForm = false;
baremeTfuList: any[] = [];
communeList: any[] = [];
zoneList: any[] = [];
reponseList: any[] = [{ label: 'Oui', value: true }, { label: 'Non', value: false }];
baremeTfuForm?: FormGroup;
isActionInProgress: boolean = false;
constructor(
private fb: FormBuilder,
private router: Router,
private crudService: CrudService,
private modal: NzModalService,
private message: NzMessageService,
private globalService: GlobalService
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
ngOnInit(): void {
this.globalService.setLodingSuccess(false);
this.crudService.getAll('commune/all').subscribe(
(data: any) => {
this.communeList = data != null && data.object ? data.object : [];
});
this.crudService.getAll('zone-rfu/all').subscribe(
(data: any) => {
this.zoneList = data != null && data.object ? data.object : [];
});
this.dtOptions = {
pagingType: 'full_numbers',
pageLength: 10,
processing: true,
language: {
search: "Rechercher&nbsp;:",
emptyTable: "Aucune donnée disponible",
lengthMenu: "Afficher _MENU_ &eacute;l&eacute;ments",
info: "Affichage de l'&eacute;lement _START_ &agrave; _END_ sur _TOTAL_ &eacute;l&eacute;ments",
infoEmpty: "Affichage de l'&eacute;lement 0 &agrave; 0 sur 0 &eacute;l&eacute;ments",
infoFiltered: "(filtr&eacute; de _MAX_ &eacute;l&eacute;ments au total)",
paginate: {
first: "<i class='menu-icon mdi mdi-chevron-double-left'></i>",
previous: "<i class='menu-icon mdi mdi-chevron-left'></i>",
next: "<i class='menu-icon mdi mdi-chevron-right'></i>",
last: "<i class='menu-icon mdi mdi-chevron-double-right'></i>"
},
}
};
this.makeForm(null);
this.list();
}
ngAfterViewInit(): void {
this.dtTrigger.next(this.dtOptions);
}
/*filterArrondissementByCommune(value: any): void {
console.log('value ==> ', value)
const communePaylod = this.baremeTfuForm?.get('commune')?.value;
this.baremeTfuForm?.get('arrondissement')?.setValue(null);
this.arrondissementFilteredList = [];
if(communePaylod != null) {
this.arrondissementFilteredList = this.arrondissementList.filter((element) => element.commune.id == communePaylod.id);
}
}*/
compareFn = (o1: any, o2: any) => (o1 && o2 ? o1.id === o2.id : o1 === o2);
makeForm(baremeTfu: BaremeTfu | null): void {
this.baremeTfuForm = this.fb.group({
id: [baremeTfu != null ? baremeTfu.id : null],
valeurAdministrative: [baremeTfu != null ? baremeTfu.valeurAdministrative : 0],
taux: [baremeTfu != null ? baremeTfu.taux : null],
zoneRfuId: [baremeTfu != null ? baremeTfu.zoneRfuId : null],
communeId: [baremeTfu != null ? baremeTfu.communeId : null,
[Validators.required]],
valeurAdministrativeMetreCarre: [baremeTfu != null ? baremeTfu.valeurAdministrativeMetreCarre : 0],
auMetreCarre: [baremeTfu != null ? baremeTfu.auMetreCarre : false]
});
if (baremeTfu != null) {
this.showHideForm(true);
}
}
resetValeurAdministrative(value: boolean): void {
if (value) {
this.baremeTfuForm?.get('valeurAdministrative')?.setValue(0);
} else {
this.baremeTfuForm?.get('valeurAdministrativeMetreCarre')?.setValue(0);
}
}
resetForm(e: MouseEvent): void {
e.preventDefault();
this.baremeTfuForm?.reset();
for (const key in this.baremeTfuForm?.controls) {
this.baremeTfuForm?.controls[key].markAsPristine();
this.baremeTfuForm?.controls[key].updateValueAndValidity();
}
this.makeForm(null);
}
ngOnDestroy(): void {
this.dtTrigger.unsubscribe();
}
list(): void {
this.baremeTfuList = [];
this.refreshDataTable();
this.crudService.getAll('barem-rfu-non-bati/all').subscribe(
(data: any) => {
this.baremeTfuList = data != null ? data.object : [];
this.baremeTfuList = [...this.baremeTfuList];
this.refreshDataTable();
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
console.log('OK');
this.globalService.setLodingSuccess(false);
}
);
}
refreshDataTable(): void {
this.dtElement?.dtInstance.then((dtInstance: DataTables.Api) => {
// Destroy the table first
dtInstance.destroy();
// Call the dtTrigger to rerender again
this.dtTrigger.next(null);
});
}
showDeleteConfirm(element: any, index: number): void {
this.modal.confirm({
nzTitle: 'Confirmez-vous ?',
nzContent: 'suppression de cette TFU non bâti',
nzOkText: 'Oui',
nzOkType: 'primary',
nzOkDanger: true,
nzOnOk: () => {
this.globalService.setLodingSuccess(true);
this.crudService.deleteElement('barem-rfu-non-bati/delete', element.id).subscribe(
(data: any) => {
this.baremeTfuList.splice(index, 1);
this.refreshDataTable();
this.message.create('success', `Suppression effectuée avec succès.`);
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.globalService.setLodingSuccess(false);
}
);
},
nzCancelText: 'Non',
nzOnCancel: () => console.log('Cancel')
});
}
showHideForm(value: boolean): void {
this.isForm = value;
if (!value) {
this.makeForm(null);
}
}
saveForm(): void {
for (const i in this.baremeTfuForm?.controls) {
this.baremeTfuForm?.controls[i].markAsDirty();
this.baremeTfuForm?.controls[i].updateValueAndValidity();
}
if (this.baremeTfuForm?.valid) {
this.isActionInProgress = true;
const formData = this.baremeTfuForm?.value;
if (
formData.id == null ||
formData.id == undefined ||
formData.id == ''
) {
this.globalService.setLodingSuccess(true);
this.crudService.save('barem-rfu-non-bati/create', formData).subscribe(
(data: any) => {
this.baremeTfuList.unshift(data.object);
this.message.create('success', `Enregistrement effectué avec succès.`);
this.makeForm(null);
this.refreshDataTable();
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.globalService.setLodingSuccess(false);
}
);
} else {
const i = this.baremeTfuList.findIndex(
(element) => element.id == formData.id
);
this.globalService.setLodingSuccess(true);
this.crudService.update('barem-rfu-non-bati/update', formData).subscribe(
(data: any) => {
this.baremeTfuList.splice(i, 1);
this.baremeTfuList.unshift(data.object);
this.message.create('success', `Modification effectuée avec succès.`);
this.makeForm(null);
this.refreshDataTable();
this.showHideForm(false);
this.globalService.setLodingSuccess(false);
},
(error: HttpErrorResponse) => {
this.message.create('error', `Erreur de connexion internet ou du système`);
this.globalService.setLodingSuccess(false);
}
);
}
} else {
this.modal.error({
nzTitle: 'Erreur',
nzContent: 'Formulaire invalid. Un ou plusieurs champs sont vides...'
});
}
}
}

View File

@@ -0,0 +1,506 @@
/* ══════════════════════════════════════════════════════
CONTAINER
══════════════════════════════════════════════════════ */
.di-container {
padding: 24px;
font-family: 'Inter', 'Segoe UI', Arial, sans-serif;
background: #f4f8fd;
min-height: 100vh;
}
/* ══════════════════════════════════════════════════════
HEADER
══════════════════════════════════════════════════════ */
.di-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.di-header-left {
display: flex;
align-items: center;
gap: 12px;
}
.di-title {
font-size: 20px;
font-weight: 700;
color: #1a5890;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.di-count {
background: #e0ecf8;
color: #1a5890;
font-size: 12px;
font-weight: 700;
padding: 4px 12px;
border-radius: 999px;
}
/* ── Search ── */
.di-search {
position: relative;
display: flex;
align-items: center;
}
.di-search-icon {
position: absolute;
left: 12px;
color: #9ca3af;
font-size: 16px;
pointer-events: none;
}
.di-search-input {
padding: 10px 16px 10px 38px;
border: 1.5px solid #d1dde8;
border-radius: 10px;
font-size: 13px;
width: 320px;
background: #fff;
color: #1f2937;
outline: none;
transition: border-color 0.2s;
}
.di-search-input:focus {
border-color: #1a5890;
box-shadow: 0 0 0 3px rgba(26, 88, 144, 0.10);
}
/* ══════════════════════════════════════════════════════
LOADING
══════════════════════════════════════════════════════ */
.di-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 60px;
color: #6b7280;
font-size: 14px;
}
.di-spinner {
width: 32px; height: 32px;
border: 3px solid #e0ecf8;
border-top-color: #1a5890;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* ══════════════════════════════════════════════════════
EMPTY
══════════════════════════════════════════════════════ */
.di-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
color: #9ca3af;
font-size: 15px;
gap: 12px;
}
.di-empty span[nz-icon] { font-size: 48px; }
/* ══════════════════════════════════════════════════════
CARD
══════════════════════════════════════════════════════ */
.di-card {
background: #fff;
border-radius: 14px;
box-shadow: 0 2px 10px rgba(26, 88, 144, 0.07);
margin-bottom: 16px;
overflow: hidden;
transition: box-shadow 0.2s;
border: 1.5px solid #e8f1fb;
}
.di-card:hover {
box-shadow: 0 6px 24px rgba(26, 88, 144, 0.13);
}
/* ── Ligne principale ── */
.di-card-main {
display: flex;
align-items: stretch;
padding: 20px 24px;
gap: 20px;
flex-wrap: wrap;
}
/* ── Colonnes ── */
.di-col {
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
}
.di-col-identity { flex: 2; min-width: 180px; border-right: 1.5px solid #e8f1fb; padding-right: 20px; }
.di-col-prop { flex: 2; min-width: 180px; border-right: 1.5px solid #e8f1fb; padding-right: 20px; }
.di-col-fiscal { flex: 1.5; min-width: 150px; border-right: 1.5px solid #e8f1fb; padding-right: 20px; }
.di-col-superficies { flex: 1.2; min-width: 130px; border-right: 1.5px solid #e8f1fb; padding-right: 20px; }
.di-col-action { flex: 0 0 auto; align-items: center; justify-content: center; }
/* ── Identité ── */
.di-nup {
font-size: 15px;
font-weight: 700;
color: #1a5890;
}
.di-tf {
font-size: 12px;
color: #6b7280;
font-weight: 500;
}
.di-location {
font-size: 12px;
color: #374151;
display: flex;
align-items: center;
gap: 4px;
}
.di-ref {
font-size: 11px;
color: #9ca3af;
font-weight: 500;
}
/* ── Propriétaire ── */
.di-prop-name {
font-size: 14px;
font-weight: 700;
color: #111827;
}
.di-prop-sub {
font-size: 12px;
color: #6b7280;
display: flex;
align-items: center;
gap: 5px;
}
/* ── Fiscal ── */
.di-badges {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 4px;
}
.di-badge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.03em;
}
.badge-tfu { background: #dbeafe; color: #1e40af; }
.badge-irf { background: #d1fae5; color: #065f46; }
.badge-oui { background: #fef3c7; color: #92400e; }
.badge-non { background: #f3f4f6; color: #6b7280; }
.badge-exh-oui { background: #fee2e2; color: #991b1b; }
.badge-exh-non { background: #f0fdf4; color: #14532d; }
.di-annee {
font-size: 12px;
color: #6b7280;
display: flex;
align-items: center;
gap: 5px;
}
.di-montant {
font-size: 16px;
font-weight: 700;
color: #1a5890;
}
.di-montant-label {
font-size: 10px;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ── Superficies ── */
.di-sup-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.di-sup-label {
font-size: 11px;
color: #9ca3af;
font-weight: 500;
}
.di-sup-val {
font-size: 12px;
font-weight: 700;
color: #374151;
}
/* ── Bouton détail ── */
.di-btn-detail {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 9px 18px;
border-radius: 8px;
border: 1.5px solid #1a5890;
background: transparent;
color: #1a5890;
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.di-btn-detail:hover {
background: #1a5890;
color: #fff;
}
/* ══════════════════════════════════════════════════════
DÉTAILS EXPANDÉS
══════════════════════════════════════════════════════ */
.di-card-detail {
border-top: 2px solid #e8f1fb;
background: #f4f8fd;
padding: 24px;
animation: fadeInDown 0.25s ease;
}
.di-detail-sections {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 20px;
}
.di-detail-section {
background: #fff;
border-radius: 10px;
padding: 16px 20px;
box-shadow: 0 1px 6px rgba(26, 88, 144, 0.07);
border: 1px solid #e8f1fb;
}
.di-detail-section-title {
font-size: 13px;
font-weight: 700;
color: #1a5890;
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 7px;
padding-bottom: 10px;
border-bottom: 1.5px solid #e8f1fb;
}
.di-detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px 16px;
}
.di-detail-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.di-detail-label {
font-size: 10px;
font-weight: 600;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.di-detail-val {
font-size: 13px;
font-weight: 500;
color: #1f2937;
}
.di-detail-val.accent {
font-weight: 700;
color: #1a5890;
}
.di-detail-badge {
align-self: flex-start;
margin-top: 2px;
}
/* ══════════════════════════════════════════════════════
ANIMATIONS
══════════════════════════════════════════════════════ */
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fadeInDown {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
/* ══════════════════════════════════════════════════════
RESPONSIVE
══════════════════════════════════════════════════════ */
@media (max-width: 992px) {
.di-card-main { flex-direction: column; gap: 16px; }
.di-col-identity,
.di-col-prop,
.di-col-fiscal,
.di-col-superficies { border-right: none; border-bottom: 1px solid #e8f1fb; padding-right: 0; padding-bottom: 12px; }
.di-col-action { align-items: flex-start; }
.di-search-input { width: 100%; }
.di-detail-grid { grid-template-columns: 1fr; }
.di-detail-sections { grid-template-columns: 1fr; }
}
.anticon {
margin-top: -5px;
}
/* ══════════════════════════════════════════════════════
PAGINATION
══════════════════════════════════════════════════════ */
.di-pagination {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
padding: 20px 4px 8px;
flex-wrap: wrap;
}
.di-pagination-info {
font-size: 13px;
color: #1b5890;
font-weight: 500;
}
/* Surcharge couleur nz-pagination → #914242 */
.di-container .ant-pagination-item-active {
border-color: #1b5890 !important;
background: #1b5890;
}
.di-container .ant-pagination-item-active a {
color: #fff !important;
}
.di-container .ant-pagination-item:hover {
border-color: var(--primary) !important;
}
.di-container .ant-pagination-item:hover a {
color: var(--primary) !important;
}
.di-container .ant-pagination-prev:hover .ant-pagination-item-link,
.di-container .ant-pagination-next:hover .ant-pagination-item-link {
border-color: var(--primary) !important;
color: var(--primary) !important;
}
.di-container .ant-select:not(.ant-select-disabled):hover .ant-select-selector {
border-color: var(--primary) !important;
}
.di-container .ant-select-focused .ant-select-selector {
border-color: var(--primary) !important;
box-shadow: 0 0 0 2px rgba(145, 66, 66, 0.15) !important;
}
button.ant-pagination-item-link {
justify-content: center!important;
align-items: center!important;
display: flex!important;
}
li.ant-pagination-item, li.ant-pagination-prev, li.ant-pagination-next {
height: 45px;
width: 45px;
justify-content: center!important;
align-items: center!important;
display: inline-flex!important;
border-radius: 25px;
font-size: 11px;
}
button.ant-pagination-item-link {
border-radius: 25px!important;
}
/* ══════════════════════════════════════════════════════
EXPORT
══════════════════════════════════════════════════════ */
.di-header-right {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.di-export-group {
display: flex;
align-items: center;
gap: 8px;
}
.di-btn-export {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 9px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
border: 1.5px solid transparent;
}
.di-btn-export:disabled {
opacity: 0.45;
cursor: not-allowed;
pointer-events: none;
}
/* Page courante — contour bordeaux */
.di-btn-export-page {
background: transparent;
border-color: #1a5891;
color: #1a5891;
}
.di-btn-export-page:hover:not(:disabled) {
background: var(--primary-light);
box-shadow: 0 3px 10px var(--primary-shadow);
}

View File

@@ -0,0 +1,512 @@
<div class="row" *ngIf="pointGeneration != null">
<div class="col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body card-body-review">
<h6 class="card-title" style="font-size: 16px!important; text-transform: none;">
<a class="nav-link ico-header" routerLink="/core/liquidation/generer-donnee-fiscale"
style="display: unset!important;background: transparent;">
<i class="mdi mdi-arrow-left" style="font-size: 17px;color: #2e2e2e;"></i>
</a> Détail des données d'imposition fiscales générées
</h6>
<nz-divider></nz-divider>
<h6 class="text-uppercase" style="margin-bottom: -5px;"> informations clés </h6>
<span style="background-color: #313131;font-size: 3px;">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</span>
<div class="formulaire p-5 mt-3" style="width: 100%;border-radius: 5px;">
<div>
<h2 style="font-size: 16px;">Accès à tous les détails</h2>
<p style="font-size: 11px;color: rgb(99, 99, 99);">Cette interface vous permet de
consulter les données fiscales de la TFU et de l'IRF.</p>
</div>
<div class="row mt-3" style="border-bottom: solid 1px #1212;">
<div class="col-md-3">
<span class="info-label">Exercice : </span>
<p class="info-text">
{{ pointGeneration.exerciceAnnee ? pointGeneration.exerciceAnnee : '—' }}
</p>
</div>
<div class="col-md-3">
<span class="info-label">Référence pièce admin : </span>
<p class="info-text">{{ pointGeneration.referencePieceAdmin || '—' }}</p>
</div>
<div class="col-md-3">
<span class="info-label">Date pièce admin : </span>
<p class="info-text">
{{ pointGeneration.datePieceAdmin ? (pointGeneration.datePieceAdmin | date:'dd/MM/yyyy') : '—' }}
</p>
</div>
<div class="col-md-3">
<span class="info-label">Statut : </span>
<p class="info-text">
<span class="badge" [ngClass]="statusBadges[pointGeneration.statusAvis]">
{{ statusLabels[pointGeneration.statusAvis] }}
</span>
</p>
</div>
</div>
<div class="row mt-3" style="border-bottom: solid 1px #1212;">
<div class="col-md-3">
<span class="info-label">Commune : </span>
<p class="info-text">{{ pointGeneration.communeNom ? pointGeneration.communeNom : '-' }}</p>
</div>
<div class="col-md-3">
<span class="info-label">Structure : </span>
<p class="info-text">{{ pointGeneration.structureNom ? pointGeneration.structureNom : '—' }}
</p>
</div>
<div class="col-md-3">
<span class="info-label">Date de génération : </span>
<p class="info-text">
{{ pointGeneration.dateGeneration ? (pointGeneration.dateGeneration | date:'dd/MM/yyyy') : '—' }}
</p>
</div>
<div class="col-md-3">
<span class="info-label">Date de clôture : </span>
<p class="info-text">
{{ pointGeneration.dateCloture ? (pointGeneration.dateCloture | date:'dd/MM/yyyy') : '—' }}
</p>
</div>
</div>
</div>
<h6 class="text-uppercase" style="margin-bottom: -5px;"> données d'imposition fiscale générées </h6>
<span style="background-color: #313131;font-size: 3px;">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</span>
<div class="formulaire p-2 mt-3" style="width: 100%; border-radius: 5px;">
<div class="di-container">
<!-- ── En-tête ── -->
<div class="di-header">
<div class="di-header-left">
<h2 class="di-title">
<span nz-icon nzType="file-search" nzTheme="outline"></span>
Données d'impositions
</h2>
<span class="di-count">{{ totalElements }} enregistrement(s)</span>
</div>
<div class="di-header-right">
<div class="di-search">
<span nz-icon nzType="search" class="di-search-icon"></span>
<input class="di-search-input" type="text"
placeholder="Rechercher NUP, commune, propriétaire, IFU…"
[(ngModel)]="searchText" (input)="onSearch()" />
</div>
<!-- ── Boutons export ── -->
<div class="di-export-group">
<button class="di-btn-export di-btn-export-page" (click)="exportPageCourante()"
[disabled]="loading || filteredDonnees.length === 0" nz-tooltip
nzTooltipTitle="Exporter les {{ filteredDonnees.length }} lignes de la page courante">
<span nz-icon nzType="file-excel" nzTheme="outline"></span>
Exporter les données de la page courante
</button>
</div>
</div>
</div>
<!-- ── Loading ── -->
<div class="di-loading" *ngIf="loading">
<div class="di-spinner"></div>
<span>Chargement en cours…</span>
</div>
<!-- ── Liste ── -->
<div class="di-list" *ngIf="!loading">
<div class="di-empty" *ngIf="filteredDonnees.length === 0">
<span nz-icon nzType="inbox" nzTheme="outline"></span>
<p>Aucune donnée trouvée</p>
</div>
<div class="di-card" *ngFor="let item of filteredDonnees">
<!-- ══ LIGNE PRINCIPALE ══ -->
<div class="di-card-main">
<!-- Colonne 1 : Identité parcelle -->
<div class="di-col di-col-identity">
<div class="di-badges">
<span class="di-badge" [ngClass]="getNatureClass(item.natureImpot)">
{{ item.natureImpot }}
</span>
<span class="di-badge" [ngClass]="getBatieClass(item.batie)">
{{ item.batie ? 'Bâtie' : 'Non bâtie' }}
</span>
<span class="di-badge" [ngClass]="getExhonereClass(item.exonere)">
{{ item.exonere ? 'Exonérée' : 'Non exonérée' }}
</span>
</div>
<!--<div class="di-nup">{{ item.nup || '—' }}</div>-->
<div class="di-annee">
<span nz-icon nzType="calendar" nzTheme="outline"></span> {{ item.annee }}
— n° TF : {{ item.titreFoncier || '—' }}
</div>
<div class="di-location">
<span nz-icon nzType="environment" nzTheme="outline"></span>
{{ item.nomQuartierVillage }}, {{ item.nomCommune }}
</div>
<div class="di-ref">
Q{{ item.q }} — Îlot {{ item.ilot }} — Parc. {{ item.parcelle }}
</div>
</div>
<!-- Colonne 2 : Propriétaire -->
<div class="di-col di-col-prop">
<div class="di-prop-name">
{{ item.raisonSociale || ((item.nomProp || '') + ' ' + (item.prenomProp || '')) || '—' }}
</div>
<div class="di-prop-sub">
<span nz-icon nzType="idcard" nzTheme="outline"></span> NC / IFU :
{{ item.ifu || '—' }}
</div>
<div class="di-prop-sub">
<span nz-icon nzType="phone" nzTheme="outline"></span>
{{ item.telProp || '—' }}
</div>
<div class="di-prop-sub" *ngIf="item.zoneRfuNom">
<span nz-icon nzType="environment" nzTheme="outline"></span>
{{ item.zoneRfuNom || '—' }}
</div>
</div>
<!-- Colonne 3 : Fiscal -->
<div class="di-col di-col-fiscal">
<div class="di-montant-label">Montant de la taxe</div>
<div class="di-montant">{{ formatMontant(item.montantTaxe) }}</div>
<div class="di-montant-label">Valeur bâtiment</div>
<div class="di-montant">{{ formatMontant(item.valeurBatiment) }}</div>
</div>
<!-- Colonne 4 : Superficies -->
<div class="di-col di-col-superficies">
<div class="di-sup-item">
<span class="di-sup-label">Sup. parcelle</span>
<span class="di-sup-val">{{ item.superficieParc || 0 }} m²</span>
</div>
<div class="di-sup-item">
<span class="di-sup-label">Sup. sol bât.</span>
<span class="di-sup-val">{{ item.superficieAuSolBat || 0 }} m²</span>
</div>
<div class="di-sup-item">
<span class="di-sup-label">TFU / m²</span>
<span class="di-sup-val">{{ item.tfuMetreCarre || 0 }} FCFA</span>
</div>
</div>
<!-- Colonne 5 : Action -->
<div class="di-col di-col-action">
<button class="di-btn-detail" (click)="toggleRow(item.id)">
<span nz-icon [nzType]="isExpanded(item.id) ? 'up' : 'down'"></span>
{{ isExpanded(item.id) ? 'Réduire' : 'Plus de détails' }}
</button>
</div>
</div>
<!-- ══ DÉTAILS EXPANDÉS ══ -->
<div class="di-card-detail" *ngIf="isExpanded(item.id)">
<div class="di-detail-sections">
<!-- Section Localisation -->
<div class="di-detail-section">
<div class="di-detail-section-title">
<span nz-icon nzType="environment"></span> Localisation
</div>
<div class="di-detail-grid">
<div class="di-detail-item">
<span class="di-detail-label">Département</span>
<span class="di-detail-val">{{ item.codeDepartement }} —
{{ item.nomDepartement }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Commune</span>
<span class="di-detail-val">{{ item.codeCommune }} —
{{ item.nomCommune }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Arrondissement</span>
<span class="di-detail-val">{{ item.codeArrondissement }} —
{{ item.nomArrondissement }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Quartier / Village</span>
<span class="di-detail-val">{{ item.codeQuartierVillage }} —
{{ item.nomQuartierVillage }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Zone RFU</span>
<span class="di-detail-val">{{ item.zoneRfuNom || '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Date enquête</span>
<span
class="di-detail-val">{{ item.dateEnquete | date:'dd/MM/yyyy' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Coordonnées GPS</span>
<span class="di-detail-val">
{{ item.longitude ? item.longitude + ', ' + item.latitude : '—' }}
</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Service</span>
<span class="di-detail-val">{{ item.serviceCode || '—' }}</span>
</div>
</div>
</div>
<!-- Section Propriétaire -->
<div class="di-detail-section">
<div class="di-detail-section-title">
<span nz-icon nzType="user"></span> Propriétaire
</div>
<div class="di-detail-grid">
<div class="di-detail-item">
<span class="di-detail-label">Nom / Raison sociale</span>
<span class="di-detail-val">
{{ item.raisonSociale || ((item.nomProp || '') + ' ' + (item.prenomProp || '')) || '—' }}
</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">IFU</span>
<span class="di-detail-val">{{ item.ifu || '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">NPI</span>
<span class="di-detail-val">{{ item.npi || '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Téléphone</span>
<span class="di-detail-val">{{ item.telProp || '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Email</span>
<span class="di-detail-val">{{ item.emailProp || '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Adresse</span>
<span class="di-detail-val">{{ item.adresseProp || '—' }}</span>
</div>
</div>
</div>
<!-- Section Représentant -->
<div class="di-detail-section" *ngIf="item.nomRep">
<div class="di-detail-section-title">
<span nz-icon nzType="team"></span> Représentant
</div>
<div class="di-detail-grid">
<div class="di-detail-item">
<span class="di-detail-label">Nom complet</span>
<span class="di-detail-val">{{ item.nomRep }}
{{ item.prenomRep }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Téléphone</span>
<span class="di-detail-val">{{ item.telRep || '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Email</span>
<span class="di-detail-val">{{ item.emailRep || '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Adresse</span>
<span class="di-detail-val">{{ item.adresseRep || '—' }}</span>
</div>
</div>
</div>
<!-- Section Données fiscales -->
<div class="di-detail-section">
<div class="di-detail-section-title">
<span nz-icon nzType="dollar-circle"></span> Données fiscales
</div>
<div class="di-detail-grid">
<div class="di-detail-item">
<span class="di-detail-label">Nature impôt</span>
<span class="di-badge di-detail-badge"
[ngClass]="getNatureClass(item.natureImpot)">
{{ item.natureImpot }}
</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Valeur bâtiment</span>
<span
class="di-detail-val accent">{{ formatMontant(item.valeurBatiment) }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Valeur locative adm.</span>
<span
class="di-detail-val accent">{{ formatMontant(item.valeurLocativeAdm) }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Val. loc. adm. / m²</span>
<span
class="di-detail-val">{{ formatMontant(item.valeurLocativeAdmMetreCarre) }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Montant loyer annuel</span>
<span
class="di-detail-val accent">{{ formatMontant(item.montantLoyerAnnuel) }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">TFU / m²</span>
<span
class="di-detail-val">{{ formatMontant(item.tfuMetreCarre) }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">TFU minimum</span>
<span
class="di-detail-val">{{ formatMontant(item.tfuMinimum) }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Montant taxe</span>
<span
class="di-detail-val accent">{{ formatMontant(item.montantTaxe) }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Val. adm. parc. non bâtie</span>
<span
class="di-detail-val">{{ formatMontant(item.valeurAdminParcelleNb) }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Val. adm. parc. nb / m²</span>
<span
class="di-detail-val">{{ formatMontant(item.valeurAdminParcelleNbMetreCarre) }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Taux TFU</span>
<span
class="di-detail-val">{{ item.tauxTfu != null ? item.tauxTfu + '%' : '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Exonérée</span>
<span class="di-badge di-detail-badge"
[ngClass]="getExhonereClass(item.exonere)">
{{ item.exonere ? 'OUI' : 'NON' }}
</span>
</div>
</div>
</div>
<!-- Section Bâtiment -->
<div class="di-detail-section">
<div class="di-detail-section-title">
<span nz-icon nzType="home"></span> Bâtiment & Logement
</div>
<div class="di-detail-grid">
<div class="di-detail-item">
<span class="di-detail-label">Bâtie</span>
<span class="di-badge di-detail-badge"
[ngClass]="getBatieClass(item.batie)">
{{ item.batie ? 'OUI' : 'NON' }}
</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Num. bâtiment</span>
<span class="di-detail-val">{{ item.numBatiment || '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Num. unité logement</span>
<span
class="di-detail-val">{{ item.numUniteLogement || '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Standing / Catégorie</span>
<span class="di-detail-val">{{ item.standingBat || '—' }} /
{{ item.categorieBat || '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Sup. sol bâtiment</span>
<span class="di-detail-val">{{ item.superficieAuSolBat || 0 }}
</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Sup. sol unité log.</span>
<span class="di-detail-val">{{ item.superficieAuSolUlog || 0 }}
</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Bâtiment exonéré</span>
<span class="di-badge di-detail-badge"
[ngClass]="getExhonereClass(item.batimentExonere)">
{{ item.batimentExonere ? 'OUI' : 'NON' }}
</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Unité log. exonérée</span>
<span class="di-badge di-detail-badge"
[ngClass]="getExhonereClass(item.uniteLogementExonere)">
{{ item.uniteLogementExonere ? 'OUI' : 'NON' }}
</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Nombre piscines</span>
<span class="di-detail-val">{{ item.nombrePiscine ?? '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Nombre Ulog</span>
<span class="di-detail-val">{{ item.nombreUlog ?? '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Nombre bâtiments</span>
<span class="di-detail-val">{{ item.nombreBat ?? '—' }}</span>
</div>
<div class="di-detail-item">
<span class="di-detail-label">Valeur parcelle</span>
<span
class="di-detail-val">{{ formatMontant(item.valeurParcelle) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- fin di-card -->
<!-- ══ PAGINATION ══ -->
<div class="di-pagination" *ngIf="totalElements > 0">
<nz-pagination [nzPageIndex]="pageNo + 1" [nzTotal]="totalElements"
[nzPageSize]="pageSize" [nzShowSizeChanger]="true"
[nzPageSizeOptions]="[10, 20, 50, 100]" [nzShowTotal]="totalTemplate"
(nzPageIndexChange)="onPageChange($event)"
(nzPageSizeChange)="onPageSizeChange($event)">
</nz-pagination>
<ng-template #totalTemplate let-total let-range="range">
<span class="di-pagination-info">
{{ range[0] }}{{ range[1] }} sur {{ total }} enregistrements
</span>
</ng-template>
</div>
</div>
<!-- fin di-list -->
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,293 @@
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { firstValueFrom } from 'rxjs';
import { CrudService } from 'src/app/crud.service';
import { ExcelExportService } from 'src/app/excel-export.service';
import { GlobalService } from 'src/app/global.service';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
@Component({
selector: 'app-detail-imposition-donnee-fiscale',
templateUrl: './detail-imposition-donnee-fiscale.component.html',
styleUrls: ['./detail-imposition-donnee-fiscale.component.css'],
encapsulation: ViewEncapsulation.None
})
export class DetailImpositionDonneeFiscaleComponent implements OnInit {
isActionInProgress: boolean = false;
id: number = 0;
pointGeneration: any = null;
user: any = null;
readonly statusLabels: { [key: string]: string } = {
'EN_COURS': 'EN COURS',
'GENERATION_AUTORISE': 'GÉNÉRATION AUTORISÉE',
'REJETE': 'REJETÉ',
'GENERE': 'GÉNÉRÉ',
'TFU_FNB_GENERE': 'TFU FNB GÉNÉRÉE',
'CLOTURE': 'CLÔTURÉ'
};
readonly statusBadges: { [key: string]: string } = {
'EN_COURS': 'badge-warning',
'GENERATION_AUTORISE': 'badge-primary',
'REJETE': 'badge-danger',
'GENERE': 'badge-success',
'TFU_FNB_GENERE': 'badge-primary',
'CLOTURE': 'badge-info'
};
donneeImpositionList: any[] = [];
loading = false;
expandedRows: { [id: number]: boolean } = {};
searchText = '';
donnees: any[] = [];
filteredDonnees: any[] = [];
// ── Pagination ────────────────────────────────────────────
pageNo: number = 0;
pageSize: number = 10;
totalElements: number = 0;
totalPages: number = 0;
// ── Mapping des labels français pour l'export ─────────────
private readonly EXPORT_LABELS: { [key: string]: string } = {
annee: 'Année',
natureImpot: 'Nature Impôt',
codeDepartement: 'Code Département',
nomDepartement: 'Département',
codeCommune: 'Code Commune',
nomCommune: 'Commune',
codeArrondissement: 'Code Arrondissement',
nomArrondissement: 'Arrondissement',
codeQuartierVillage: 'Code Quartier/Village',
nomQuartierVillage: 'Quartier/Village',
q: 'Q',
ilot: 'Îlot',
parcelle: 'Parcelle',
nup: 'NUP',
titreFoncier: 'Titre Foncier',
numBatiment: 'N° Bâtiment',
numUniteLogement: 'N° Unité Logement',
ifu: 'IFU',
npi: 'NPI',
telProp: 'Tél. Propriétaire',
emailProp: 'Email Propriétaire',
nomProp: 'Nom Propriétaire',
prenomProp: 'Prénom Propriétaire',
raisonSociale: 'Raison Sociale',
adresseProp: 'Adresse Propriétaire',
telRep: 'Tél. Représentant',
emailRep: 'Email Représentant',
nomRep: 'Nom Représentant',
prenomRep: 'Prénom Représentant',
adresseRep: 'Adresse Représentant',
superficieParc: 'Superficie Parcelle (m²)',
superficieAuSolBat: 'Superficie Sol Bâtiment (m²)',
superficieAuSolUlog: 'Superficie Sol Unité Log. (m²)',
batie: 'Bâtie',
exonere: 'Exonérée',
batimentExonere: 'Bâtiment Exonéré',
uniteLogementExonere: 'Unité Logement Exonérée',
valeurLocativeAdm: 'Valeur Locative Adm. (FCFA)',
valeurLocativeAdmMetreCarre: 'Val. Loc. Adm. / m² (FCFA)',
valeurBatiment: 'Valeur Bâtiment (FCFA)',
valeurParcelle: 'Valeur Parcelle (FCFA)',
montantLoyerAnnuel: 'Montant Loyer Annuel (FCFA)',
tfuMetreCarre: 'TFU / m² (FCFA)',
tfuMinimum: 'TFU Minimum (FCFA)',
montantTaxe: 'Montant Taxe (FCFA)',
standingBat: 'Standing Bâtiment',
categorieBat: 'Catégorie Bâtiment',
nombrePiscine: 'Nombre Piscines',
nombreUlog: 'Nombre Unités Logement',
nombreBat: 'Nombre Bâtiments',
dateEnquete: 'Date Enquête',
serviceId: 'ID Service',
serviceCode: 'Code Service',
zoneRfuId: 'ID Zone RFU',
zoneRfuNom: 'Zone RFU',
tauxTfu: 'Taux TFU (%)',
valeurAdminParcelleNb: 'Val. Adm. Parcelle Non Bâtie (FCFA)',
valeurAdminParcelleNbMetreCarre: 'Val. Adm. Parc. NB / m² (FCFA)'
};
// ── Champs à exclure de l'export ──────────────────────────
private readonly CHAMPS_EXCLUS = new Set([
'id', 'serviceId', 'zoneRfuId'
]);
constructor(
private router: Router,
private route: ActivatedRoute,
private modal: NzModalService,
private message: NzMessageService,
private globalService: GlobalService,
private http: HttpClient,
private crudService: CrudService,
private tokenStorage: TokenStorage,
private excelExportService: ExcelExportService,
) {
this.globalService.getLodingSuccess().subscribe({
next: (data: boolean) => {
this.isActionInProgress = data;
},
error: () => {
this.isActionInProgress = false;
}
});
}
async ngOnInit(): Promise<void> {
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
this.id = this.route.snapshot.params["id"];
if (this.id != undefined && this.id > 0) {
this.globalService.setLodingSuccess(true);
const result: any = await firstValueFrom(
this.crudService.getAll('impositions-tfu/id/' + this.id)
);
if (result && result.object) {
this.pointGeneration = result.object;
this.globalService.setLodingSuccess(false);
}
}
await this.loadData();
}
async loadData(): Promise<void> {
this.loading = true;
try {
const result: any = await firstValueFrom(
this.crudService.getAll(
`donnees-impositions-tfu/all-page/by-imposition-id/${this.id}?pageNo=${this.pageNo}&pageSize=${this.pageSize}`
)
);
if (result && result.object) {
const page = result.object;
this.donnees = page.content || [];
this.filteredDonnees = [...this.donnees];
this.totalElements = page.totalElements || 0;
this.totalPages = page.totalPages || 0;
}
} catch (e) {
console.error('Erreur chargement données imposition', e);
} finally {
this.loading = false;
}
}
onPageChange(page: any): void {
this.pageNo = page - 1; // nz-pagination est 1-based, API est 0-based
this.loadData();
}
onPageSizeChange(size: any): void {
this.pageSize = size;
this.pageNo = 0;
this.loadData();
}
// ── Adapter les méthodes aux vrais champs de l'API ────────
getBatieClass(batie: boolean | null): string {
return batie === true ? 'badge-oui' : 'badge-non';
}
getExhonereClass(exh: boolean | null): string {
return exh === true ? 'badge-exh-oui' : 'badge-exh-non';
}
// ── Recherche : désactivée côté client (server-side) ─────
onSearch(): void {
// à brancher sur un endpoint de recherche si disponible
const q = this.searchText.toLowerCase().trim();
if (!q) {
this.filteredDonnees = [...this.donnees];
return;
}
this.filteredDonnees = this.donnees.filter(d =>
(d.nup || '').toLowerCase().includes(q) ||
(d.nomCommune || '').toLowerCase().includes(q) ||
(d.nomProp || '').toLowerCase().includes(q) ||
(d.prenomProp || '').toLowerCase().includes(q) ||
(d.raisonSociale || '').toLowerCase().includes(q) ||
(d.titreFoncier || '').toLowerCase().includes(q) ||
(d.ifu || '').toLowerCase().includes(q) ||
String(d.annee || '').includes(q)
);
}
toggleRow(id: number): void {
this.expandedRows[id] = !this.expandedRows[id];
}
isExpanded(id: number): boolean {
return !!this.expandedRows[id];
}
getNatureClass(nature: string): string {
return nature === 'TFU' ? 'badge-tfu' : 'badge-irf';
}
formatMontant(val: number): string {
if (!val && val !== 0) return '—';
return new Intl.NumberFormat('fr-FR').format(val) + ' FCFA';
}
// ── Nettoyage et formatage d'une ligne ───────────────────
private nettoyerLigneExport(item: any): { [label: string]: any } {
const ligne: { [label: string]: any } = {};
for (const key of Object.keys(this.EXPORT_LABELS)) {
if (this.CHAMPS_EXCLUS.has(key)) continue;
const label = this.EXPORT_LABELS[key];
const val = item[key];
// Booléens → OUI / NON
if (typeof val === 'boolean') {
ligne[label] = val ? 'OUI' : 'NON';
continue;
}
// Null / undefined → tiret
if (val === null || val === undefined) {
ligne[label] = '—';
continue;
}
ligne[label] = val;
}
return ligne;
}
// ── Export de la page courante ────────────────────────────
exportPageCourante(): void {
if (!this.filteredDonnees || this.filteredDonnees.length === 0) {
this.message.warning('Aucune donnée à exporter.');
return;
}
const data = this.filteredDonnees.map(item => this.nettoyerLigneExport(item));
this.excelExportService.exportAsExcelFile(
data,
`impositions_page_${this.pageNo + 1}`,
'Impositions'
);
}
}

View File

@@ -0,0 +1,51 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BaremeTfuBatiComponent } from './bareme-tfu-bati/bareme-tfu-bati.component';
import { BaremeTfuNonBatiComponent } from './bareme-tfu-non-bati/bareme-tfu-non-bati.component';
import { GenererDonneeFiscaleComponent } from './generer-donnee-fiscale/generer-donnee-fiscale.component';
import { ListeDonneeFiscaleComponent } from './liste-donnee-fiscale/liste-donnee-fiscale.component';
import { NotFoundComponent } from 'src/app/shared/not-found/not-found.component';
import { DonneeFiscaleComponent } from './donnee-fiscale.component';
import { ZoneRfuComponent } from '../reference/zone-rfu/zone-rfu.component';
import { UsageComponent } from '../reference/usage/usage.component';
import { CategorieBatimentComponent } from '../reference/categorie-batiment/categorie-batiment.component';
import { DepartementComponent } from '../reference/departement/departement.component';
import { CommuneComponent } from '../reference/commune/commune.component';
import { ArrondissementComponent } from '../reference/arrondissement/arrondissement.component';
import { ExerciceComponent } from '../reference/exercice/exercice.component';
import { SommaireDonneeFiscaleComponent } from './sommaire-donnee-fiscale/sommaire-donnee-fiscale.component';
import { ProcessGenererDonneeFiscaleComponent } from './process-generer-donnee-fiscale/process-generer-donnee-fiscale.component';
import { DetailImpositionDonneeFiscaleComponent } from './detail-imposition-donnee-fiscale/detail-imposition-donnee-fiscale.component';
const routes: Routes = [
{
path: '', component: DonneeFiscaleComponent,
children: [
{ path: 'reference/exercice', component: ExerciceComponent },
{ path: 'reference/zone-rfu', component: ZoneRfuComponent },
{ path: 'reference/usage', component: UsageComponent },
{ path: 'reference/categorie-batiment', component: CategorieBatimentComponent },
{ path: 'reference/departement', component: DepartementComponent },
{ path: 'reference/commune', component: CommuneComponent },
{ path: 'reference/arrondissement', component: ArrondissementComponent },
{ path: 'bareme-tfu-bati', component: BaremeTfuBatiComponent },
{ path: 'bareme-tfu-non-bati', component: BaremeTfuNonBatiComponent },
{ path: 'generer-donnee-fiscale', component: GenererDonneeFiscaleComponent },
{ path: 'process-generer-donnee-fiscale/:id', component: ProcessGenererDonneeFiscaleComponent },
{ path: 'liste-donnee-fiscale/:id', component: DetailImpositionDonneeFiscaleComponent },
//{ path: 'liste-donnee-fiscale', component: ListeDonneeFiscaleComponent },
{ path: 'sommaire-liquidation', component: SommaireDonneeFiscaleComponent },
{ path: '**', component: NotFoundComponent }
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DonneeFiscaleRoutingModule { }

View File

@@ -0,0 +1,98 @@
<div class="row" style="margin-top: 75px;">
<div class="col-md-12">
<ul nz-menu nzTheme="dark" nzMode="horizontal" class="navBar">
<li nz-menu-item style="margin-left: 6% !important;" class="text-center" (click)="goHome()">
<img src="assets/sigibe/home.svg" alt="" class="icon-home-header">
</li>
<li nz-menu-item nzSelected routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/liquidation/sommaire-liquidation"> Sommaire </a>
</li>
<li nz-menu-item nz-popover [(nzPopoverVisible)]="isVisibleReference"
(nzPopoverVisibleChange)="change($event, 1)" nzPopoverPlacement="bottom"
[nzPopoverContent]="contentTemplateMenuReference">
Références <svg data-icon="chevron-down" height="13" role="img" viewBox="0 0 16 16" width="13">
<path
d="M12 5c-.28 0-.53.11-.71.29L8 8.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42l4 4c.18.18.43.29.71.29s.53-.11.71-.29l4-4A1.003 1.003 0 0012 5z"
fill-rule="evenodd"></path>
</svg>
</li>
<li nz-menu-item routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/liquidation/bareme-tfu-non-bati">Matrice TFU Foncier non bâti </a>
</li>
<li nz-menu-item routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/liquidation/bareme-tfu-bati"> Matrice TFU Foncier bâti </a>
</li>
<li nz-menu-item routerLinkActive="ant-menu-item-selecte">
<a routerLink="/core/liquidation/generer-donnee-fiscale"> Fiche de liquidation des TFU </a>
</li>
</ul>
</div>
</div>
<ng-template #contentTemplateMenuReference>
<nz-list>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/liquidation/reference/departement" >
<i class="mdi mdi-arrow-right"></i> Les départements
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/liquidation/reference/commune">
<i class="mdi mdi-arrow-right"></i> Les communes
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/liquidation/reference/arrondissement">
<i class="mdi mdi-arrow-right"></i> Les arrondissements
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/liquidation/reference/exercice"> <i class="mdi mdi-arrow-right"> </i>
Les exercices fiscales
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/liquidation/reference/zone-rfu"> <i class="mdi mdi-arrow-right"> </i>
Les zones RFU
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/liquidation/reference/usage"> <i class="mdi mdi-arrow-right"> </i>
Les usages des immeubles
</a>
</nz-list-item>
<nz-list-item class="review-list-item-menu">
<a routerLink="/core/liquidation/reference/categorie-batiment"> <i class="mdi mdi-arrow-right"> </i> Les
catégories de bâtiment
</a>
</nz-list-item>
</nz-list>
</ng-template>
<div class="row" [ngStyle]="{ backgroundColor: module ? module.color : '' }" id="formulaire">
<div class="col-md-5" style="padding: 35px;margin-bottom: 2%;">
<div style="margin-left: 16%;">
<h3 class="text-secondary"
style="font-size: 11px;text-transform: uppercase;color: rgba(255, 255, 255, 0.61) !important;">
Module Enregistrement </h3>
<h1 class="text-white" style="font-size: 16px;line-height: 1.5;">
Dossier en cours sur le module liquidation</h1>
</div>
</div>
<div class="col-md-2">
</div>
<div class="col-md-5" style="padding-left: 3.1%;">
<img src="assets/sigibe/logo-mef-white.png" alt=""
style="width: 200px;margin-top: 4%;float: right;margin-right: 20%;">
</div>
</div>
<div style="margin-left: 6%;margin-right: 6%;background-color: rgb(255 255 255 / 75%);">
<router-outlet></router-outlet>
</div>

View File

@@ -0,0 +1,53 @@
import { Component, OnInit } from '@angular/core';
import { TokenStorage } from 'src/app/utilitaire/token-storage';
import { JwtHelperService } from '@auth0/angular-jwt';
import { Router } from '@angular/router';
@Component({
selector: 'app-donnee-fiscale',
templateUrl: './donnee-fiscale.component.html',
styleUrls: ['./donnee-fiscale.component.css']
})
export class DonneeFiscaleComponent {
user: any = null;
isVisibleReference = false;
isVisibleSecteur = false;
isVisibleEquipe = false;
menuNum = 0;
module: any = null;
constructor(
private tokenStorage: TokenStorage,
private router: Router,
) {
}
ngOnInit(): void {
this.module = JSON.parse(this.tokenStorage.getModule());
console.log(JSON.parse(this.tokenStorage.getModule()));
const token = this.tokenStorage.getToken() != null ? this.tokenStorage.getToken() : '';
const helper = new JwtHelperService();
const decodeToken = helper.decodeToken(token ? token : '');
this.user = decodeToken?.user;
console.log(this.user);
}
change(value: any, menuNum: number): void {
if (menuNum == 1)
this.isVisibleReference = value;
if (menuNum == 2)
this.isVisibleSecteur = value;
if (menuNum == 3)
this.isVisibleEquipe = value;
}
goHome(): void {
this.tokenStorage.saveModule(null);
this.router.navigate(['/principale']);
}
}

View File

@@ -0,0 +1,47 @@
import { CUSTOM_ELEMENTS_SCHEMA, NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from 'src/app/shared/shared.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DataTablesModule } from 'angular-datatables';
import { DonneeFiscaleRoutingModule } from './donnee-fiscale-routing.module';
import { DonneeFiscaleComponent } from './donnee-fiscale.component';
import { BaremeTfuBatiComponent } from './bareme-tfu-bati/bareme-tfu-bati.component';
import { BaremeTfuNonBatiComponent } from './bareme-tfu-non-bati/bareme-tfu-non-bati.component';
import { GenererDonneeFiscaleComponent } from './generer-donnee-fiscale/generer-donnee-fiscale.component';
import { ListeDonneeFiscaleComponent } from './liste-donnee-fiscale/liste-donnee-fiscale.component';
import { ReferenceModule } from '../reference/reference.module';
import { SommaireDonneeFiscaleComponent } from './sommaire-donnee-fiscale/sommaire-donnee-fiscale.component';
import { NgApexchartsModule } from 'ng-apexcharts';
import { ProcessGenererDonneeFiscaleComponent } from './process-generer-donnee-fiscale/process-generer-donnee-fiscale.component';
import { DetailImpositionDonneeFiscaleComponent } from './detail-imposition-donnee-fiscale/detail-imposition-donnee-fiscale.component';
@NgModule({
declarations: [
DonneeFiscaleComponent,
BaremeTfuBatiComponent,
BaremeTfuNonBatiComponent,
GenererDonneeFiscaleComponent,
ListeDonneeFiscaleComponent,
SommaireDonneeFiscaleComponent,
ProcessGenererDonneeFiscaleComponent,
DetailImpositionDonneeFiscaleComponent,
],
imports: [
CommonModule,
DonneeFiscaleRoutingModule,
FormsModule,
ReactiveFormsModule,
SharedModule,
ReferenceModule,
DataTablesModule,
NgApexchartsModule,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
})
export class DonneeFiscaleModule { }

Some files were not shown because too many files have changed in this diff Show More