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

16
.editorconfig Normal file
View File

@@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

7
.htaccess Normal file
View File

@@ -0,0 +1,7 @@
RewriteEngine On
# If an existing asset or directory is requested go to it as it is
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
RewriteRule ^ - [L]
# If the requested resource doesn't exist, use index.html
RewriteRule ^ ./index.html

4
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

26
Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
FROM nginx:latest as build
## Replace the default nginx index page with our Angular app
COPY ./.htaccess /usr/share/nginx/html
COPY dist/infocad-back-office /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/nginx.conf
RUN chmod -R 777 /usr/share/nginx/html
CMD ["/bin/bash", "-c", \
"echo API_URL=[$API_URL], && \
sed -i s#MY_APP_API_URL#$API_URL#g /usr/share/nginx/html/main.*.js && \
nginx -g 'daemon off;'"]
#from my MAC
#docker build --platform linux/amd64 -t front-fiscad . ou docker build -t front-fiscad .
#docker save -o ./front-fiscad.tar front-fiscad
#docker load -i front-fiscad.tar
#docker run -d -p 8081:80 -e API_URL=http://localhost:9090/ front-fiscad
#docker ps
#docker stop CONTAINER_ID ==> docker ps (pour trouver le CONTAINER_ID)
#docker images -a
#docker rmi IMAGE_ID (supprimer une image) ==> docker ps (pour trouver le IMAGE_ID)

27
README.md Normal file
View File

@@ -0,0 +1,27 @@
# InfocadBackOffice
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.0.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

121
angular.json Normal file
View File

@@ -0,0 +1,121 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"infocad-back-office": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/infocad-back-office",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
"output": "/assets/"
}
],
"styles": [
"src/theme.less",
"src/styles.css",
"src/assets/static/vendors/iconfonts/mdi/css/materialdesignicons.min.css",
"src/assets/static/vendors/css/vendor.bundle.base.css",
"src/assets/static/css/style.css",
"node_modules/datatables.net-bs4/css/dataTables.bootstrap4.min.css",
"node_modules/datatables.net-responsive-dt/css/responsive.dataTables.css"
],
"scripts": [
"src/assets/static/vendors/js/vendor.bundle.base.js",
"src/assets/static/vendors/js/vendor.bundle.addons.js",
"src/assets/static/js/off-canvas.js",
"src/assets/static/js/misc.js",
"node_modules/datatables.net/js/jquery.dataTables.min.js",
"node_modules/datatables.net-responsive/js/dataTables.responsive.min.js",
"node_modules/datatables.net-bs4/js/dataTables.bootstrap4.min.js",
"node_modules/datatables.net-responsive-bs4/js/responsive.bootstrap4.min.js"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "3mb",
"maximumError": "6mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6mb",
"maximumError": "9mb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "infocad-back-office:build:production"
},
"development": {
"browserTarget": "infocad-back-office:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "infocad-back-office:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": false
}
}

BIN
front-fiscad.tar Normal file

Binary file not shown.

22
nginx.conf Normal file
View File

@@ -0,0 +1,22 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name mysite.com www.mysite.com;
root /usr/share/nginx/html;
location / {
try_files $uri$args $uri$args/ /index.html;
}
}
}

8
note.txt Normal file
View File

@@ -0,0 +1,8 @@
- AGENT DE CONSTATATION => sont des agents qui interviennent dans les secteurs découpages
- INSPECTEUR (SONT DES CHEFS SECTEURS) => peuvent être chef pour plusieurs secteurs
- notion de section qui est nouveau, Structure -> Section -> secteur -> secteur découpages
- Dans une fonction, renseigner obligatoirement la structure, la section et le secteur
afin de régler le problème des profils de management
- Dans structure, il peut ne pas y avoir le choix de la commune, donc dans l'enregistrement de
secteur, il faut renseigner les découpages administratives sans tenir de commune qui est dans structure ou faire le filtre.

16985
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

100
package.json Normal file
View File

@@ -0,0 +1,100 @@
{
"name": "infocad-back-office",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^16.0.0",
"@angular/common": "^16.0.0",
"@angular/compiler": "^16.0.0",
"@angular/core": "^16.0.0",
"@angular/forms": "^16.0.0",
"@angular/platform-browser": "^16.0.0",
"@angular/platform-browser-dynamic": "^16.0.0",
"@angular/router": "^16.0.0",
"@auth0/angular-jwt": "^5.2.0",
"@capacitor-community/file-opener": "^8.0.0",
"@capacitor-community/http": "^1.4.1",
"@capacitor-community/sqlite": "^7.0.3",
"@capacitor/angular": "^2.0.3",
"@capacitor/app": "^8.0.0",
"@capacitor/camera": "^6.0.2",
"@capacitor/core": "^8.0.1",
"@capacitor/device": "^8.0.0",
"@capacitor/filesystem": "^8.0.0",
"@capacitor/geolocation": "^8.0.0",
"@capacitor/haptics": "^8.0.0",
"@capacitor/keyboard": "^8.0.0",
"@capacitor/network": "^8.0.0",
"@capawesome/capacitor-file-picker": "^8.0.0",
"@ionic/angular": "^8.7.17",
"@ionic/pwa-elements": "^3.3.0",
"@tanstack/table-core": "^8.21.3",
"@types/geojson": "^7946.0.16",
"@types/node": "^22.1.0",
"@types/ol": "^7.0.0",
"ag-grid-angular": "^32.3.9",
"ag-grid-community": "^32.3.9",
"angular-datatables": "^16.0.1",
"apexcharts": "^3.54.0",
"buffer": "^6.0.3",
"capacitor-blob-writer": "^1.1.19",
"chart.js": "^4.5.1",
"copyfiles": "^2.4.1",
"cordova-plugin-file": "^8.1.3",
"cordova-plugin-zeep": "^0.0.5",
"cordova-plugin-zip": "^3.1.0",
"datatables.net": "^1.13.1",
"datatables.net-bs4": "^1.10.20",
"datatables.net-dt": "^1.13.1",
"datatables.net-responsive-bs4": "^2.2.3",
"datatables.net-responsive-dt": "^2.4.0",
"file-saver": "^2.0.5",
"install": "^0.13.0",
"ionicons": "^8.0.13",
"jeep-sqlite": "^2.8.0",
"jquery": "^3.6.1",
"jsonfile": "^6.2.0",
"jspdf": "^4.0.0",
"jszip": "^3.10.1",
"ng-apexcharts": "^1.12.0",
"ng-zorro-antd": "^16.2.2",
"ng2-charts": "^8.0.0",
"ngx-image-compress": "^18.1.5",
"npm": "^11.8.0",
"ol": "^10.4.0",
"ol-layerswitcher": "^4.1.2",
"pako": "^2.1.0",
"rxjs": "~7.8.0",
"sql.js": "^1.13.0",
"tslib": "^2.3.0",
"type-is": "^2.0.1",
"xlsx": "^0.18.5",
"zone.js": "~0.13.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.0.0",
"@angular/cli": "~16.0.0",
"@angular/compiler-cli": "^16.0.0",
"@types/datatables.net": "^1.10.24",
"@types/file-saver": "^2.0.7",
"@types/jasmine": "~4.3.0",
"@types/jquery": "^3.5.14",
"css-loader": "^7.1.2",
"jasmine-core": "~4.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"mini-css-extract-plugin": "^2.9.2",
"style-loader": "^4.0.0",
"typescript": "~5.0.2"
}
}

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 { }

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