diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..e8d514c8d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "git.mergeEditor": true, + "merge-conflict.autoNavigateNextConflict.enabled": true +} \ No newline at end of file diff --git a/README.md b/README.md index e03630b34..8b1378917 100644 --- a/README.md +++ b/README.md @@ -1,178 +1 @@ -# [Light Bootstrap Dashboard Angular](https://demos.creative-tim.com/light-bootstrap-dashboard-angular2/dashboard) -[![version][version-badge]][CHANGELOG] ![license][license-badge] -![alt text](src/assets/img/opt_lbd_angular_thumbnail.jpg) - -**[Light Bootstrap Dashboard Angular](https://demos.creative-tim.com/light-bootstrap-dashboard-angular2/dashboard)** is an admin dashboard template designed to be beautiful and simple. It is built on top of Bootstrap 3, using [Light Bootstrap Dashboard](https://www.creative-tim.com/product/light-bootstrap-dashboard) and it is fully responsive. It comes with a big collections of elements that will offer you multiple possibilities to create the app that best fits your needs. It can be used to create admin panels, project management systems, web applications backend, CMS or CRM. - -The product represents a big suite of front-end developer tools that can help you jump start your project. We have created it thinking about things you actually need in a dashboard. Light Bootstrap Dashboard Angular 2 contains multiple handpicked and optimized plugins. Everything is designed to fit with one another. As you will be able to see, the dashboard you can access on Creative Tim is a customization of this product. - -It comes with 6 filter colors for the sidebar (“black”, “azure”,”green”,”orange”,”red”,”purple”) and an option to have a background image. - -Special thanks go to: Robert McIntosh for the notification system Chartist for the wonderful charts We are very excited to share this dashboard with you and we look forward to hearing your feedback! - -## Links: - -+ [Live Preview](https://demos.creative-tim.com/light-bootstrap-dashboard-angular2/dashboard) -+ [Light Bootstrap Dashboard PRO Angular](https://www.creative-tim.com/product/light-bootstrap-dashboard-pro-angular2/?ref=lbd-angular-github) ($49) - -## Quick Start: - -Quick start options: - -+ [Download from Github](https://github.com/creativetimofficial/light-bootstrap-dashboard-angular2/archive/master.zip). -+ [Download from Creative Tim](https://www.creative-tim.com/product/light-bootstrap-dashboard-angular2). -+ Clone the repo: `git clone https://github.com/creativetimofficial/light-bootstrap-dashboard-angular2.git`. - -## Deploy - -:rocket: You can deploy your own version of the template to Genezio with one click: - -[![Deploy to Genezio](https://raw.githubusercontent.com/Genez-io/graphics/main/svg/deploy-button.svg)](https://app.genez.io/start/deploy?repository=https://github.com/creativetimofficial/light-bootstrap-dashboard-angular2&utm_source=github&utm_medium=referral&utm_campaign=github-creativetim&utm_term=deploy-project&utm_content=button-head) - -## Terminal Commands - -1. Install NodeJs from [NodeJs Official Page](https://nodejs.org/en). -2. Open Terminal -3. Go to your file project -4. Run in terminal: ```npm install -g @angular/cli``` -5. Then: ```npm install``` -6. And: ```ng serve``` -7. Navigate to `http://localhost:4200/` - -### What's included - -Within the download you'll find the following directories and files: -``` -light-bootstrap-dashboard-angular -├── CHANGELOG.md -├── LICENSE.md -├── README.md -├── angular.json -├── documentation -│   ├── css -│   └── tutorial-lbd-angular2.html -├── e2e -├── karma.conf.js -├── package-lock.json -├── package.json -├── protractor.conf.js -├── src -│   ├── app -│   │   ├── app.component.css -│   │   ├── app.component.html -│   │   ├── app.component.spec.ts -│   │   ├── app.component.ts -│   │   ├── app.module.ts -│   │   ├── app.routing.ts -│   │   ├── home -│   │   │   ├── home.component.css -│   │   │   ├── home.component.html -│   │   │   ├── home.component.spec.ts -│   │   │   └── home.component.ts -│   │   ├── icons -│   │   │   ├── icons.component.css -│   │   │   ├── icons.component.html -│   │   │   ├── icons.component.spec.ts -│   │   │   └── icons.component.ts -│   │   ├── layouts -│   │   │   └── admin-layout -│   │   │   ├── admin-layout.component.html -│   │   │   ├── admin-layout.component.scss -│   │   │   ├── admin-layout.component.spec.ts -│   │   │   ├── admin-layout.component.ts -│   │   │   ├── admin-layout.module.ts -│   │   │   └── admin-layout.routing.ts -│   │   ├── lbd -│   │   │   ├── lbd-chart -│   │   │   │   ├── lbd-chart.component.html -│   │   │   │   └── lbd-chart.component.ts -│   │   │   └── lbd.module.ts -│   │   ├── maps -│   │   │   ├── maps.component.css -│   │   │   ├── maps.component.html -│   │   │   ├── maps.component.spec.ts -│   │   │   └── maps.component.ts -│   │   ├── notifications -│   │   │   ├── notifications.component.css -│   │   │   ├── notifications.component.html -│   │   │   ├── notifications.component.spec.ts -│   │   │   └── notifications.component.ts -│   │   ├── shared -│   │   │   ├── footer -│   │   │   │   ├── footer.component.html -│   │   │   │   ├── footer.component.ts -│   │   │   │   └── footer.module.ts -│   │   │   └── navbar -│   │   │   ├── navbar.component.html -│   │   │   ├── navbar.component.ts -│   │   │   └── navbar.module.ts -│   │   ├── sidebar -│   │   │   ├── sidebar.component.html -│   │   │   ├── sidebar.component.ts -│   │   │   └── sidebar.module.ts -│   │   ├── tables -│   │   │   ├── tables.component.css -│   │   │   ├── tables.component.html -│   │   │   ├── tables.component.spec.ts -│   │   │   └── tables.component.ts -│   │   ├── typography -│   │   │   ├── typography.component.css -│   │   │   ├── typography.component.html -│   │   │   ├── typography.component.spec.ts -│   │   │   └── typography.component.ts -│   │   ├── upgrade -│   │   │   ├── upgrade.component.css -│   │   │   ├── upgrade.component.html -│   │   │   ├── upgrade.component.spec.ts -│   │   │   └── upgrade.component.ts -│   │   └── user -│   │   ├── user.component.css -│   │   ├── user.component.html -│   │   ├── user.component.spec.ts -│   │   └── user.component.ts -│   ├── assets -│   │   ├── css -│   │   ├── fonts -│   │   ├── img -│   │   └── sass -│   │   ├── lbd -│   │   └── light-bootstrap-dashboard.scss -│   ├── environments -│   ├── favicon.ico -│   ├── index.html -│   ├── main.ts -│   ├── polyfills.ts -│   ├── styles.css -│   ├── test.ts -│   └── tsconfig.json -├── tslint.json -└── typings.json - -``` -## Useful Links - -More products from Creative Tim: - -Tutorials: - -Freebies: - -Affiliate Program (earn money): - -Social Media: - -Twitter: - -Facebook: - -Dribbble: - -Google+: - -Instagram: - -[CHANGELOG]: ./CHANGELOG.md - -[version-badge]: https://img.shields.io/badge/version-1.9.0-blue.svg -[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg diff --git a/angular.json b/angular.json index b232b7f9f..3ca145eff 100644 --- a/angular.json +++ b/angular.json @@ -19,21 +19,37 @@ "assets": [ "src/assets", "src/favicon.ico" + ], "styles": [ "node_modules/perfect-scrollbar/css/perfect-scrollbar.css", "node_modules/animate.css/animate.min.css", "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/assets/sass/light-bootstrap-dashboard.scss", - "src/assets/css/demo.css" + "src/assets/css/demo.css", + "src/styles.css", + "src/assets/css/bootstrap.min.css", + "src/assets/css/tooplate-infinite-loop.css", + "src/assets/fontawesome-5.5/css/all.min.css", + "src/assets/magnific-popup/magnific-popup.css", + "src/assets/slick/slick.css", + "src/assets/slick/slick-theme.css", + "node_modules/intl-tel-input/build/css/intlTelInput.css" ], "scripts": [ "node_modules/jquery/dist/jquery.js", "node_modules/bootstrap/dist/js/bootstrap.js", "node_modules/bootstrap-notify/bootstrap-notify.js", - "node_modules/chartist/dist/chartist.js" - ] - }, + "node_modules/chartist/dist/chartist.js", + "src/assets/js/jquery-1.9.1.min.js", + "src/assets/slick/slick.min.js", + "src/assets/magnific-popup/jquery.magnific-popup.min.js", + "src/assets/js/easing.min.js", + "src/assets/js/jquery.singlePageNav.min.js", + "src/assets/js/bootstrap.min.js", + "node_modules/html2pdf.js/dist/html2pdf.bundle.min.js", + "node_modules/intl-tel-input/build/js/intlTelInput.min.js" + ] }, "configurations": { "production": { "optimization": true, @@ -62,7 +78,7 @@ "minify": false, "inlineCritical": true }, - "fonts": true + "fonts": false }, "outputHashing": "all" } diff --git a/package.json b/package.json index c51c02de7..24491f514 100644 --- a/package.json +++ b/package.json @@ -24,41 +24,53 @@ "@angular/material": "^14.2.0", "@angular/platform-browser": "^14.2.0", "@angular/platform-browser-dynamic": "^14.2.0", - "@angular/router": "^14.2.0", + "@angular/router": "^14.3.0", "@ngui/map": "0.30.3", + "@ngx-translate/core": "^14.0.0", + "@ngx-translate/http-loader": "^7.0.0", + "@popperjs/core": "^2.11.8", "@types/googlemaps": "3.43.3", "animate.css": "4.1.1", "arrive": "2.4.1", - "bootstrap": "3.3.7", + "bootstrap": "^5.3.6", "bootstrap-notify": "3.1.3", "chartist": "0.11.4", + "emailjs-com": "^3.2.0", "googleapis": "66.0.0", - "jquery": "3.5.1", + "html2pdf.js": "^0.10.3", + "intl-tel-input": "^25.3.1", + "jquery": "^3.7.1", + "jwt-decode": "^4.0.0", + "ngx-cookie-service": "^14.0.1", + "ngx-slickjs": "^1.5.2", "perfect-scrollbar": "1.5.0", "rxjs": "~7.5.0", + "slick-carousel": "^1.8.1", + "sweetalert2": "^11.22.0", "tslib": "^2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { - "@angular-devkit/build-angular": "^14.2.3", + "@angular-devkit/build-angular": "^14.2.0", "@angular/cli": "~14.2.3", "@angular/compiler-cli": "^14.2.0", + "@types/chartist": "0.11.0", + "@types/intl-tel-input": "^18.1.4", "@types/jasmine": "~5.1.4", + "@types/jasminewd2": "~2.0.13", + "@types/jquery": "3.5.30", + "@types/node": "20.14.11", + "codelyzer": "^0.0.28", + "cross-env": "^7.0.3", "jasmine-core": "~4.3.0", + "jasmine-spec-reporter": "~7.0.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.0.0", - "typescript": "~4.7.2", - "@types/jasminewd2": "~2.0.13", - "@types/chartist": "0.11.0", - "@types/jquery": "3.5.30", - "@types/node": "20.14.11", - "codelyzer": "6.0.2", - "jasmine-spec-reporter": "~7.0.0", - "protractor": "7.0.0", + "protractor": "^7.0.0", "ts-node": "~10.7.0", - "cross-env": "^7.0.3" + "typescript": "~4.7.2" } } diff --git a/src/app/Services/BlogService.ts b/src/app/Services/BlogService.ts new file mode 100644 index 000000000..fc0c8cd2f --- /dev/null +++ b/src/app/Services/BlogService.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from 'environments/environment'; +import { Blog } from 'app/blogslist/blogslist.component'; + +export interface BlogCreateRequest { + titre: string; + contenu: string; + userId: number; + tags: string[]; + image: File | null; +} + +export interface BlogUpdateRequest { + titre: string; + contenu: string; + newImage?: File; + tags: string[]; +} +@Injectable({ + providedIn: 'root' +}) +export class BlogService { + private apiUrl = environment.apiUrl + '/blog'; + + constructor(private http: HttpClient) { } + + getAllBlogs(): Observable { + return this.http.get(this.apiUrl); + } + + createBlog(request: BlogCreateRequest): Observable { + const formData = new FormData(); + + formData.append('Titre', request.titre); + formData.append('Contenu', request.contenu); + formData.append('UserId', request.userId.toString()); + request.tags.forEach(tag => formData.append('Tags', tag)); + + if (request.image) { + formData.append('Image', request.image, request.image.name); + } + + return this.http.post(this.apiUrl, formData); + } +updateBlog(id: number, request: BlogUpdateRequest): Observable { + const formData = new FormData(); + formData.append('Titre', request.titre); + formData.append('Contenu', request.contenu); + + if (request.newImage) { + formData.append('NewImage', request.newImage, request.newImage.name); + } + + request.tags.forEach(tag => { + formData.append('Tags', tag); + }); + + return this.http.put(`${this.apiUrl}/${id}`, formData); +} + + + +getBlogById(id: number) { + return this.http.get(`${this.apiUrl}/${id}`); +} + +deleteBlog(id: number) { + return this.http.delete(`${this.apiUrl}/${id}`); +} + incrementLike(id: number): Observable<{ message: string; likes: number }> { + return this.http.post<{ message: string; likes: number }>(`${this.apiUrl}/${id}/like`, {}); + } + +} diff --git a/src/app/Services/ProduitAvecDevisService.ts b/src/app/Services/ProduitAvecDevisService.ts new file mode 100644 index 000000000..107cf895c --- /dev/null +++ b/src/app/Services/ProduitAvecDevisService.ts @@ -0,0 +1,103 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from 'environments/environment'; +import { Observable } from 'rxjs'; + +export interface ProduitAvecDevis { + id: number; + titre: string; + description: string; + categorie: string; + imagePath: string; + caracteristiques: { + id: number; + texte: string; + produitAvecDevisId: number; + }[]; + devis?: { + id: number; + nom: string; + prenom: string; + email: string; + entreprise: string; + message: string; + quantite: number; + userId: number; + produitAvecDevisId: number; + }[]; +} + +export interface ProduitAvecDevisCreateRequest { + titre: string; + description: string; + categorie: string; + image: File | null; + caracteristiques: string[]; +} + +export interface ProduitAvecDevisUpdateRequest { + titre: string; + description: string; + categorie: string; + newImage?: File | null; + caracteristiques: string[]; +} + +@Injectable({ + providedIn: 'root' +}) +export class ProduitAvecDevisService { + private apiUrl = environment.apiUrl + '/ProduitsAvecDevis'; + + constructor(private http: HttpClient) {} + + getAllProduits(): Observable { + return this.http.get(this.apiUrl); + } + + getById(id: number): Observable { + return this.http.get(`${this.apiUrl}/${id}`); + } + + create(request: ProduitAvecDevisCreateRequest): Observable { + const formData = new FormData(); + + formData.append('Titre', request.titre); + formData.append('Description', request.description); + formData.append('Categorie', request.categorie); + + request.caracteristiques.forEach(c => { + formData.append('Caracteristiques', c); + }); + + if (request.image) { + formData.append('Image', request.image, request.image.name); + } + + return this.http.post(this.apiUrl, formData); + } + + update(id: number, request: ProduitAvecDevisUpdateRequest): Observable { + const formData = new FormData(); + + formData.append('Titre', request.titre); + formData.append('Description', request.description); + formData.append('Categorie', request.categorie); + + request.caracteristiques.forEach(c => { + formData.append('Caracteristiques', c); + }); + + if (request.newImage) { + formData.append('NewImage', request.newImage, request.newImage.name); + } + + return this.http.put(`${this.apiUrl}/${id}`, formData); + } + + delete(id: number): Observable { + return this.http.delete(`${this.apiUrl}/${id}`); + } + + +} diff --git a/src/app/Services/ProduitSansDevisService.ts b/src/app/Services/ProduitSansDevisService.ts new file mode 100644 index 000000000..171ada791 --- /dev/null +++ b/src/app/Services/ProduitSansDevisService.ts @@ -0,0 +1,99 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from 'environments/environment'; +import { Observable } from 'rxjs'; + +export interface ProduitSansDevis { + id: number; + titre: string; + description: string; + categorie: string; + prix: string; + imagePath: string; + caracteristiques: { + id: number; + texte: string; + produitSansDevisId: number; + }[]; + userId: number; +} + +export interface ProduitSansDevisCreateRequest { + titre: string; + description: string; + categorie: string; + prix: string; + image: File | null; + caracteristiques: string[]; + userId: number; +} + +export interface ProduitSansDevisUpdateRequest { + titre: string; + description: string; + categorie: string; + prix: string; + newImage?: File | null; + caracteristiques: string[]; + userId: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class ProduitSansDevisService { + private apiUrl = environment.apiUrl + '/ProduitsSansDevis'; + + constructor(private http: HttpClient) {} + + getAllProduits(): Observable { + return this.http.get(this.apiUrl); + } + + getById(id: number): Observable { + return this.http.get(`${this.apiUrl}/${id}`); + } + + create(request: ProduitSansDevisCreateRequest): Observable { + const formData = new FormData(); + + formData.append('Titre', request.titre); + formData.append('Description', request.description); + formData.append('Categorie', request.categorie); + formData.append('Prix', request.prix); + formData.append('UserId', request.userId.toString()); + + request.caracteristiques.forEach(c => { + formData.append('Caracteristiques', c); + }); + + if (request.image) { + formData.append('Image', request.image, request.image.name); + } + + return this.http.post(this.apiUrl, formData); + } + + update(id: number, request: ProduitSansDevisUpdateRequest): Observable { + const formData = new FormData(); + + formData.append('Titre', request.titre); + formData.append('Description', request.description); + formData.append('Categorie', request.categorie); + formData.append('Prix', request.prix); + + request.caracteristiques.forEach(c => { + formData.append('Caracteristiques', c); + }); + + if (request.newImage) { + formData.append('NewImage', request.newImage, request.newImage.name); + } + + return this.http.put(`${this.apiUrl}/${id}`, formData); + } + + delete(id: number): Observable { + return this.http.delete(`${this.apiUrl}/${id}`); + } +} diff --git a/src/app/Services/UserService.ts b/src/app/Services/UserService.ts new file mode 100644 index 000000000..9b06a5211 --- /dev/null +++ b/src/app/Services/UserService.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from 'environments/environment'; + +export interface User { + id?: number; + nom: string; + email: string; + role: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + private apiUrl = environment.apiUrl + '/user'; + + constructor(private http: HttpClient) {} + + getUsers(): Observable { + return this.http.get(this.apiUrl); + } +} diff --git a/src/app/Services/auth.service.ts b/src/app/Services/auth.service.ts new file mode 100644 index 000000000..aa6a60788 --- /dev/null +++ b/src/app/Services/auth.service.ts @@ -0,0 +1,115 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { EmailjsService } from 'emailJs/email.service'; +import { environment } from 'environments/environment'; +import { CookieService } from 'ngx-cookie-service'; +import { Observable } from 'rxjs'; +import {jwtDecode} from 'jwt-decode'; +import { UserStatisticsService } from './user-statistics.service'; + +interface DecodedToken { + exp: number; + [key: string]: any; +} + +export interface SignUpRequest { + nom: string; + email: string; + role:number; +} + +export interface SignInRequest { + email: string; + password: string; +} +export interface ForgetPasswordRequest { + email: string; +} +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private apiUrl = `${environment.apiUrl}/auth`; + + constructor(private http: HttpClient,private emailjsService: EmailjsService,private cookieService: CookieService, + private statisticsService: UserStatisticsService + ) {} + +signUp(data: SignUpRequest): Observable<{ message: string; mdp: string }> { + return new Observable(observer => { + this.http.post<{ message: string; mdp: string }>(`${this.apiUrl}/signup`, data).subscribe({ + next: (response) => { + // 🔁 Track signup après succès + this.statisticsService.trackSignUp().subscribe({ + next: () => console.log('📊 Signup tracked'), + error: err => console.error('❌ Failed to track signup') + }); + + observer.next(response); + observer.complete(); + }, + error: (err) => { + observer.error(err); + } + }); + }); +} + + +signIn(data: SignInRequest): Observable { + return new Observable(observer => { + this.http.post(`${this.apiUrl}/signin`, data).subscribe({ + next: (response) => { + this.statisticsService.trackLogin().subscribe({ + next: () => console.log('📊 Login tracked'), + error: err => console.error('❌ Failed to track login') + }); + + observer.next(response); + observer.complete(); + }, + error: (err) => { + observer.error(err); + } + }); + }); +} + + + getToken(): string | null { + return this.cookieService.get('token'); + } + getRole(): string | null { + return this.cookieService.get('role'); + } + logout(): void { + this.cookieService.delete('token'); + this.cookieService.delete('role'); + this.cookieService.delete('name'); + this.cookieService.delete('userId'); + } + + isTokenExpired(token: string): boolean { + try { + const decoded: DecodedToken = jwtDecode(token); + const exp = decoded.exp; + const now = Date.now() / 1000; + /*const tokenIssueTime = exp - 3600; // supposons token valide 1h normalement + const testExpirationTime = tokenIssueTime + 60; // 1 minute après issue + return now > testExpirationTime;*/ + return exp < now; + } catch (e) { + return true; + } + } + + isLoggedIn(): boolean { + const token = this.getToken(); + return token !== null && !this.isTokenExpired(token); + } + forgetPassword(dat: ForgetPasswordRequest): Observable<{ mdp: string }> { + return this.http.post<{ mdp: string }>(`${this.apiUrl}/forget-password`, dat); +} + + +} diff --git a/src/app/Services/country.service.ts b/src/app/Services/country.service.ts new file mode 100644 index 000000000..ec9c793bf --- /dev/null +++ b/src/app/Services/country.service.ts @@ -0,0 +1,17 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class CountryService { + + private apiUrl = 'https://restcountries.com/v3.1/all'; + + constructor(private http: HttpClient) {} + + getCountries(): Observable { + return this.http.get(this.apiUrl); + } +} diff --git a/src/app/Services/devis.service.ts b/src/app/Services/devis.service.ts new file mode 100644 index 000000000..3dc5979d1 --- /dev/null +++ b/src/app/Services/devis.service.ts @@ -0,0 +1,37 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { environment } from 'environments/environment'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class DevisService { +private apiUrl = environment.apiUrl + '/devis'; + + constructor(private http: HttpClient) { } + + getAllDevis(): Observable { + return this.http.get(`${this.apiUrl}`); + } + + getDevisById(id: number): Observable { + return this.http.get(`${this.apiUrl}/${id}`); +} + + addDevis(data :any):Observable + { + return this.http.post(`${this.apiUrl}`, data); + } + getNumberDevisWithProduit(): Observable<{ devisWithProduit: number }> { + return this.http.get<{ devisWithProduit: number }>(`${this.apiUrl}/count/IOT`); + } + + getNumberDevisWithoutProduit(): Observable<{ devisWithoutProduit: number }> { + return this.http.get<{ devisWithoutProduit: number }>(`${this.apiUrl}/count/IT`); + } + updateEtatDevis(id: number, nouvelEtat: string): Observable { + return this.http.patch(`${this.apiUrl}/${id}/etat`, { etat: nouvelEtat }); + } + +} diff --git a/src/app/Services/franchise.service.ts b/src/app/Services/franchise.service.ts new file mode 100644 index 000000000..f5e03dcb1 --- /dev/null +++ b/src/app/Services/franchise.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { Franchise } from 'app/liste-franchises/liste-franchises.component'; +import { HttpClient, HttpParams } from '@angular/common/http'; + +import { Observable } from 'rxjs'; +import { environment } from 'environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class FranchiseService { + private apiUrl = environment.apiUrl + '/franchise'; + + constructor(private http: HttpClient) { } + + getFranchises(): Observable { + return this.http.get(this.apiUrl); + } + + getFranchiseById(id: number): Observable { + return this.http.get(`${this.apiUrl}/${id}`); +} +envoyerDemandeFranchise(payload: any) { + return this.http.post(this.apiUrl, payload); + } +} diff --git a/src/app/Services/language.service.ts b/src/app/Services/language.service.ts new file mode 100644 index 000000000..416f0eff9 --- /dev/null +++ b/src/app/Services/language.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +@Injectable({ + providedIn: 'root' +}) +export class LanguageService { +constructor(private translate: TranslateService) { + const savedLanguage = localStorage.getItem('language') || 'en'; + this.setLanguage(savedLanguage); + } + + setLanguage(lang: string) { + this.translate.use(lang).subscribe({ + next: () => console.log(`Langue activée`), + error: (err) => console.error(`Erreur de chargement pour `) + }); + localStorage.setItem('language', lang); +} + + + getCurrentLanguage(): string { + return this.translate.currentLang || 'en'; + } +} diff --git a/src/app/Services/scroll.service.ts b/src/app/Services/scroll.service.ts new file mode 100644 index 000000000..8a4d150c5 --- /dev/null +++ b/src/app/Services/scroll.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class ScrollService { + private anchor: string | null = null; + + setAnchor(anchor: string) { + this.anchor = anchor; + } + + getAnchor(): string | null { + const a = this.anchor; + this.anchor = null; + return a; + } +} diff --git a/src/app/Services/user-statistics.service.ts b/src/app/Services/user-statistics.service.ts new file mode 100644 index 000000000..1cd06d3f6 --- /dev/null +++ b/src/app/Services/user-statistics.service.ts @@ -0,0 +1,36 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { environment } from 'environments/environment'; +import { Observable } from 'rxjs'; +export interface UserStatistics { + visitors: number; + signUps: number; + logins: number; + lastUpdated: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class UserStatisticsService { + private apiUrl = environment.apiUrl + '/userstatistics'; + + constructor(private http: HttpClient) {} + + trackVisit() { + return this.http.post(`${this.apiUrl}/track/visit`, {}); + } + + trackSignUp() { + return this.http.post(`${this.apiUrl}/track/signup`, {}); + } + + trackLogin() { + return this.http.post(`${this.apiUrl}/track/login`, {}); + } + + getStats(): Observable { + return this.http.get(`${this.apiUrl}/stats`); +} + +} diff --git a/src/app/about-us/about-us.component.html b/src/app/about-us/about-us.component.html new file mode 100644 index 000000000..e608e052b --- /dev/null +++ b/src/app/about-us/about-us.component.html @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + +
+
+

{{ 'ABOUT.TITLE' | translate }}

+
+
+ +
+
+
+
+ + +
+
+

{{ 'ABOUT.FIRST.PARAGRAPH' | translate }}

+ {{ 'ABOUT.FIRST.CTA' | translate }} +
+
+
+
+ +
+
+
+
+ +
+
+
+

{{ 'ABOUT.SECOND.TITLE' | translate }}

+

{{ 'ABOUT.SECOND.PARAGRAPH' | translate }}

+ {{ 'ABOUT.SECOND.CTA' | translate }} +
+
+
+ +
+
+
+

{{ 'ABOUT.THIRD.TITLE' | translate }}

+

{{ 'ABOUT.THIRD.PARAGRAPH' | translate }}

+ {{ 'ABOUT.THIRD.CTA' | translate }} +
+
+
+ +
+
+
+
+ +
+

{{ 'ABOUT.FOURTH.TITLE' | translate }}

+ +
+ +
+
+

{{ 'ABOUT.SIXTH.TITLE' | translate }}

+

{{ 'ABOUT.SIXTH.PARAGRAPH1' | translate }}

+ Collaboration Tunav IT Group +

{{ 'ABOUT.SIXTH.PARAGRAPH2' | translate }}

+ {{ 'ABOUT.SIXTH.CTA' | translate }} +
+
+


+ + + diff --git a/src/app/about-us/about-us.component.scss b/src/app/about-us/about-us.component.scss new file mode 100644 index 000000000..2b7a15b0f --- /dev/null +++ b/src/app/about-us/about-us.component.scss @@ -0,0 +1,503 @@ +/* about-page.scss - Styles spécifiques à la page About */ +.about-page { + margin: 0; + padding: 0; + font-family: 'Roboto', sans-serif; + background-color: #F8F9FA; + color: #222222; + font-weight: 400; + + strong { + font-weight: 700; + } + + img { + max-width: 100%; + } + + p { + font-size: 1.125rem; + color: #222222; + margin: 0 0 15px; + line-height: 1.6; + } + + h1, h2, h3, h4, h5, h6 { + margin: 0; + } + + h1 { + color: #000000; + font-size: 36px; + @media (min-width: 768px) { + font-size: 80px; + } + } + + h2 { + color: #222222; + font-size: 30px; + @media (min-width: 768px) { + font-size: 60px; + } + } + + h3 { + color: #444444; + font-size: 24px; + @media (min-width: 768px) { + font-size: 50px; + } + } + + h4 { + color: #555555; + font-size: 22px; + @media (min-width: 768px) { + font-size: 40px; + } + } + + .about-container { + max-width: 1100px; + margin: 0 auto; + + img { + padding: 0.25rem; + border: 1px solid #bdbdbd; + border-radius: 0.25rem; + } + } + + .about-cta { + padding: 10px 30px; + text-align: center; + text-decoration: none; + background-color: #369; + border: 1px solid #369; + border-radius: 25px; + color: #FFFFFF; + text-transform: uppercase; + display: inline-block; + box-shadow: rgba(100, 100, 111, 0.8) 0px 7px 19px 0px; + transition: all 0.8s ease; + + &:hover { + background-color: #369; + border: 1px solid #3b1215; + color: #000000; + } + } + + /* Sections spécifiques */ + .about-banner { + background: linear-gradient(90deg, rgba(51, 85, 119, 0.8), rgba(0, 0, 0, 0.5)), url("/assets/img/TunavEquipe.png") no-repeat; + background-position: center; + background-size: cover; + padding: 150px 15px; + + h1 { + color: rgba($color: #FFFFFF, $alpha: 0.85); + text-transform: uppercase; + font-weight: 700; + } + + p { + color: #FFFFFF; + font-size: 1.375rem; + letter-spacing: 1.5px; + font-weight: 100; + text-shadow: 2px 2px 7px #222222; + } + } + + .about-section { + padding: 90px 15px; + } + + .about-first { + background-color: #FFFFFF; + background-image: linear-gradient(315deg, #a7a8a8 0%, #E9EBEC 74%); + .about-media-wrapper { + display: flex; + flex-direction: row; + align-items: center; + gap: 100px; + + @media (max-width: 768px) { + flex-direction: column; + gap: 20px; + } + } + + .about-video-left { + flex: 1; + min-width: 0; + position: relative; + + video { + width: 100%; + max-width: 600px; + height: auto; + display: block; + border-radius: 8px; + box-shadow: 0 5px 15px rgba(0,0,0,0.1); + } + .sound-toggle { + position: absolute; + bottom: 15px; + right: 15px; + background: rgba(0,0,0,0.5); + border: none; + color: white; + border-radius: 50%; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 10; + font-size: 14px; + } + + } + + .about-text-right { + flex: 1; + min-width: 0; + } + + .about-container { + max-width: 800px; + } + } + +.about-second { + background-color: #fff8e6; + min-height: 100vh; + display: flex; + align-items: center; + padding: 40px 0; + + .about-container { + width: 100%; + max-width: 1400px; + margin: 0 auto; + padding: 0 20px; + display: flex; + flex-direction: column; + + @media (min-width: 992px) { + flex-direction: row; + gap: 50px; + padding: 0 40px; + } + + .about-left-img { + width: 100%; + margin-bottom: 30px; + + @media (min-width: 992px) { + flex: 0 0 45%; + margin-bottom: 0; + align-self: center; + } + + .image-slider { + position: relative; + width: 100%; + height: 300px; // Mobile first + overflow: hidden; + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0,0,0,0.12); + + @media (min-width: 768px) { + height: 400px; + } + + @media (min-width: 992px) { + height: 500px; + } + + img { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0; + transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1); + + &.active { + opacity: 1; + } + } + } + } + + .about-right-content { + width: 100%; + + @media (min-width: 992px) { + flex: 0 0 50%; + align-self: center; + } + + h2 { + margin: 0 0 25px; + font-size: 2.2rem; + line-height: 1.2; + color: #2c3e50; + + @media (min-width: 768px) { + font-size: 2.8rem; + } + + @media (min-width: 992px) { + margin: 0 0 30px; + font-size: 3.2rem; + } + } + + p { + font-size: 1.1rem; + line-height: 1.7; + margin-bottom: 25px; + + @media (min-width: 768px) { + font-size: 1.2rem; + } + } + + .about-cta { + display: inline-block; + margin-top: 20px; + } + } + } +} + +.about-third { + background-color: #e6f5fc; + min-height: 100vh; + display: flex; + align-items: center; + padding: 40px 0; + + .about-container { + width: 100%; + max-width: 1400px; + margin: 0 auto; + padding: 0 20px; + display: flex; + flex-direction: column; + + @media (min-width: 992px) { + flex-direction: row; + gap: 50px; + padding: 0 40px; + } + + .about-right-img { + flex-basis: 100%; + margin-bottom: 30px; + + @media (min-width: 992px) { + flex: 0 0 45%; + margin-bottom: 0; + align-self: center; + } + + .image-slider { + position: relative; + width: 100%; + height: 300px; + overflow: hidden; + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + + @media (min-width: 768px) { + height: 400px; + } + + @media (min-width: 992px) { + height: 500px; + } + + img { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0; + transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1); + + &.active { + opacity: 1; + } + } + } + } + + .about-left-content { + flex-basis: 100%; + + @media (min-width: 992px) { + flex: 0 0 50%; + align-self: center; + } + + h2 { + margin: 0 0 25px; + font-size: 2.2rem; + line-height: 1.2; + color: #2c3e50; + + @media (min-width: 768px) { + font-size: 2.8rem; + } + + @media (min-width: 992px) { + margin: 0 0 30px; + font-size: 3.2rem; + } + } + + p { + font-size: 1.1rem; + line-height: 1.7; + margin-bottom: 25px; + + @media (min-width: 768px) { + font-size: 1.2rem; + } + } + + .about-cta { + display: inline-block; + margin-top: 20px; + } + } + } +} + + + + + .about-four { + background-color: #FFFFFF; + h2 { + margin: 0 0 25px; + font-size: 2.2rem; + line-height: 1.2; + color: #2c3e50; + text-align: center; + + @media (min-width: 768px) { + font-size: 2.8rem; + } + + @media (min-width: 992px) { + margin: 0 0 30px; + font-size: 3.2rem; + } + } + .about-container { + display: block; + @media (min-width: 768px) { + display: flex; + justify-content: space-between; + } + + + .about-member { + background-color: #c0c6fa; + padding: 20px; + margin: 0 0 60px; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; + @media (min-width: 768px) { + flex-basis: 31%; + display: flex; + flex-direction: column; + justify-content: space-between; + } + + h3 { + @media (min-width: 768px) { + font-size: 32px; + } + } + + .about-social { + display: flex; + justify-content: flex-start; + + a img { + border: none; + max-width: 40px; + } + } + } + } + } + .about-six { + .about-container { + text-align: center; + max-width: 800px; + } + } + +} +.carousel-container { + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + max-width: 100%; + padding: 1rem; + + .nav { + background-color: transparent; + border: none; + font-size: 2rem; + cursor: pointer; + color: #357; + z-index: 10; + padding: 0 1rem; + } + + .carousel-slide { + display: flex; + gap: 2rem; + transition: transform 0.5s ease; + } + + .about-member { + flex: 0 0 300px; + text-align: center; + border-radius: 10px; + box-shadow: 0 0 10px rgba(0,0,0,0.1); + background-color: #fff; + padding: 1rem; + + img { + width: 100%; + height: auto; + border-radius: 10px; + max-height: 300px; + object-fit: cover; + } + + h3 { + margin: 0.5rem 0 0.2rem; + font-size: 1.2rem; + } + + p { + color: #555; + font-style: italic; + } + } +} diff --git a/src/app/about-us/about-us.component.spec.ts b/src/app/about-us/about-us.component.spec.ts new file mode 100644 index 000000000..fbc856a81 --- /dev/null +++ b/src/app/about-us/about-us.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AboutUsComponent } from './about-us.component'; + +describe('AboutUsComponent', () => { + let component: AboutUsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AboutUsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AboutUsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/about-us/about-us.component.ts b/src/app/about-us/about-us.component.ts new file mode 100644 index 000000000..646e1a56c --- /dev/null +++ b/src/app/about-us/about-us.component.ts @@ -0,0 +1,168 @@ +import { AfterViewInit, Component, OnDestroy, OnInit, ElementRef, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-about-us', + templateUrl: './about-us.component.html', + styleUrls: ['./about-us.component.scss'] +}) +export class AboutUsComponent implements OnInit, AfterViewInit, OnDestroy { + constructor(private router: Router) {} + + @ViewChild('aboutVideo', { static: false }) aboutVideo!: ElementRef; + + EventImages = [ + { src: '/assets/img/FITA/FITA1.jfif', alt: 'Event 1', active: true }, + { src: '/assets/img/FITA/FITA2.jfif', alt: 'Event 2', active: false }, + { src: '/assets/img/FITA/FITA3.jfif', alt: 'Event 3', active: false }, + { src: '/assets/img/FITA/FITA4.jfif', alt: 'Event 4', active: false }, + { src: '/assets/img/FITA/FITA5.jfif', alt: 'Event 5', active: false }, + { src: '/assets/img/FITA/FITA6.jfif', alt: 'Event 6', active: false }, + { src: '/assets/img/FITA/FITA7.jfif', alt: 'Event 7', active: false } + ]; + + TeamBuildingImages = [ + { src: '/assets/img/TeamBuilding/TM1.jpeg', alt: 'TM 1', active: true }, + { src: '/assets/img/TeamBuilding/TM2.jpeg', alt: 'TM 2', active: false }, + { src: '/assets/img/TeamBuilding/TM3.jpeg', alt: 'TM 3', active: false }, + { src: '/assets/img/TeamBuilding/TM4.jpeg', alt: 'TM 4', active: false }, + { src: '/assets/img/TeamBuilding/TM5.jpeg', alt: 'TM 5', active: false }, + { src: '/assets/img/TeamBuilding/TM6.jpeg', alt: 'TM 6', active: false }, + { src: '/assets/img/TeamBuilding/TM7.jpeg', alt: 'TM 7', active: false }, + { src: '/assets/img/TeamBuilding/TM8.jpeg', alt: 'TM 8', active: false } + ]; + + membres = [ + { nom: 'Najet', prenom: 'Boukadi', profession: 'Financial Manager', image: '/assets/img/Equipe/NajetBoukadi.jfif' }, + { nom: 'Anis', prenom: 'Kalel', profession: 'PDG', image: '/assets/img/Equipe/AnisKallel.jfif' }, + { nom: 'Mariem', prenom: 'Ayari', profession: 'Marketing Mnager', image: '/assets/img/Equipe/MariemAyari.jpeg' }, + { nom: 'Skander', prenom: 'Elj', profession: 'Information System Mnager', image: '/assets/img/Equipe/SkanderElj.jfif' }, + { nom: 'Chawki', prenom: 'Zorgui', profession: 'Chef Service Vente et Aprés vente', image: '/assets/img/Equipe/ChawkiZorgui.jpeg' }, + { nom: 'Sarra', prenom: 'Dabbebi', profession: 'Responsable RH', image: '/assets/img/Equipe/SarraDabbebi.jpeg' }, + { nom: 'Marwa', prenom: 'Henchir', profession: 'Cheffe de projet IT', image: '/assets/img/Equipe/MarwaHenchir.jpeg' }, + + ]; + + slideConfig = { + slidesToShow: 3, + slidesToScroll: 3, + dots: true, + infinite: false, + arrows: true + }; + + currentIndexEvent = 0; + currentIndexTeamBuilding = 0; + + slideIntervalEvent: any; + slideIntervalTeamBuilding: any; + + private videoObserver?: IntersectionObserver; + + visibleMembres: any[] = []; + membreIndex: number = 0; + membresParSlide: number = 3; + + ngOnInit(): void { + this.startSlider(this.EventImages, 'event'); + this.startSlider(this.TeamBuildingImages, 'teamBuilding'); + this.updateVisibleMembres(); + } + + ngAfterViewInit(): void { + const videoEl = this.aboutVideo?.nativeElement; + if (!videoEl) return; + + videoEl.muted = true; + videoEl.playsInline = true; + videoEl.play().catch(err => console.warn('Autoplay failed:', err)); + + this.videoObserver = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + videoEl.play().catch(err => console.warn('Autoplay failed:', err)); + } else { + videoEl.pause(); + } + }, + { threshold: 0.1 } + ); + + this.videoObserver.observe(videoEl); + } + + startSlider(tab: any[], sliderType: 'event' | 'teamBuilding'): void { + const interval = setInterval(() => { + this.nextSlide(tab, sliderType); + }, 3000); + + if (sliderType === 'event') { + this.slideIntervalEvent = interval; + } else { + this.slideIntervalTeamBuilding = interval; + } + } + + nextSlide(tab: any[], sliderType: 'event' | 'teamBuilding'): void { + let currentIndex = sliderType === 'event' ? this.currentIndexEvent : this.currentIndexTeamBuilding; + + tab[currentIndex].active = false; + currentIndex = (currentIndex + 1) % tab.length; + tab[currentIndex].active = true; + + if (sliderType === 'event') { + this.currentIndexEvent = currentIndex; + } else { + this.currentIndexTeamBuilding = currentIndex; + } + } + + // === Fonctions carrousel membres === + updateVisibleMembres(): void { + this.visibleMembres = this.membres.slice(this.membreIndex, this.membreIndex + this.membresParSlide); + } + + nextMembre(): void { + if (this.membreIndex + this.membresParSlide < this.membres.length) { + this.membreIndex++; + this.updateVisibleMembres(); + } + } + + prevMembre(): void { + if (this.membreIndex > 0) { + this.membreIndex--; + this.updateVisibleMembres(); + } + } + + ngOnDestroy(): void { + if (this.slideIntervalEvent) { + clearInterval(this.slideIntervalEvent); + } + if (this.slideIntervalTeamBuilding) { + clearInterval(this.slideIntervalTeamBuilding); + } + if (this.videoObserver && this.aboutVideo) { + this.videoObserver.unobserve(this.aboutVideo.nativeElement); + this.videoObserver.disconnect(); + } + } + + soundOn = false; + + toggleSound(): void { + const videoEl = this.aboutVideo.nativeElement; + this.soundOn = !this.soundOn; + videoEl.muted = !this.soundOn; + + if (this.soundOn) { + videoEl.play().catch(err => console.warn('Play with sound failed:', err)); + } + } + + contact(event: Event) { + event.preventDefault(); + this.router.navigate(['contact']); + } +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 21d81f5ed..a6d4b0fbc 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,18 +1,56 @@ -import { Component, OnInit } from '@angular/core'; +import { AfterViewInit, Component, OnInit } from '@angular/core'; import { LocationStrategy, PlatformLocation, Location } from '@angular/common'; - +import { Router ,NavigationEnd} from '@angular/router'; +import { ScrollService } from './Services/scroll.service'; +import { filter } from 'rxjs/operators'; +import { LanguageService } from './Services/language.service'; +import { TranslateService } from '@ngx-translate/core';import { UserStatisticsService } from './Services/user-statistics.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) -export class AppComponent implements OnInit { +export class AppComponent implements OnInit,AfterViewInit { + constructor(private translate: TranslateService, + public location: Location,private router: Router, + private scrollService: ScrollService, + private statsService: UserStatisticsService, + private languageService: LanguageService) + { + translate.setDefaultLang('en'); + + const savedLang = localStorage.getItem('language') || 'en'; + - constructor(public location: Location) {} + translate.use(savedLang).subscribe({ + next: () => console.log(`Langue chargée avec succès`), + error: (err) => console.error(`Erreur lors du chargement de la langue`) + }); + + + } ngOnInit(){ + this.statsService.trackVisit().subscribe({ + next: () => console.log('✅ Visit tracked'), + error: err => console.error('❌ Visit tracking failed') + }); } - + ngAfterViewInit(): void { + this.router.events + .pipe(filter(event => event instanceof NavigationEnd)) + .subscribe(() => { + const anchor = this.scrollService.getAnchor(); + if (anchor) { + setTimeout(() => { + const element = document.getElementById(anchor); + if (element) { + element.scrollIntoView({ behavior: 'smooth' }); + } + }, 100); + } + }); + } isMap(path){ var titlee = this.location.prepareExternalUrl(this.location.path()); titlee = titlee.slice( 1 ); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 24da5d994..9b5b779c6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,17 +1,51 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { HttpClientModule } from '@angular/common/http'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; - -import { AppRoutingModule } from './app.routing'; import { NavbarModule } from './shared/navbar/navbar.module'; import { FooterModule } from './shared/footer/footer.module'; import { SidebarModule } from './sidebar/sidebar.module'; - import { AppComponent } from './app.component'; - import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.component'; +import { VitrineComponent } from './vitrine/vitrine.component'; +import { CarouselHomePageComponent } from './carousel-home-page/carousel-home-page.component'; +import { HeaderHomePageComponent } from './header-home-page/header-home-page.component'; +import { ClientsCarouselComponent } from './clients-carousel/clients-carousel.component'; +import { IOTCarouselComponent } from './iotcarousel/iotcarousel.component'; +import { RegisterComponent } from './register/register.component'; +import { FooterHomePageComponent } from './footer-home-page/footer-home-page.component'; +import { AboutUsComponent } from './about-us/about-us.component'; +import { ContactUsComponent } from './contact-us/contact-us.component'; +import { ChatBotComponent } from './chat-bot/chat-bot.component'; +import { BlogsComponent } from './blogs/blogs.component'; +import { FormProductsComponent } from './form-products/form-products.component'; +import { UsersListsComponent } from './users-lists/users-lists.component'; +import { BlogslistComponent } from './blogslist/blogslist.component'; +import { FormblogComponent } from './formblog/formblog.component'; +import { ProductsComponent } from './products/products.component'; +import { Carousel2Component } from './carousel2/carousel2.component'; +import { CarteProduitComponent } from './carte-produit/carte-produit.component'; +import { CarteProduitNodevisComponent } from './carte-produit-nodevis/carte-produit-nodevis.component'; +import { PetitCadreComponent } from './petit-cadre/petit-cadre.component'; +import { TitreproduitComponent } from './titreproduit/titreproduit.component'; +import { NavbarComponent } from './navbar/navbar.component'; +import { FormulaireIotItComponent } from './formulaire-iot-it/formulaire-iot-it.component'; +import { FormFranchiseComponent } from './form-franchise/form-franchise.component'; +import { AppRoutingModule } from './app.routing'; +import { ListeFranchisesComponent } from './liste-franchises/liste-franchises.component'; +import { FranchiseDetailComponent } from './franchise-detail/franchise-detail.component'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { ListDevisComponent } from './list-devis/list-devis.component'; +import { DevisDetailComponent } from './devis-detail/devis-detail.component'; +import { AuthInterceptor } from './auth/auth.interceptor'; +import { ProduitAvecDevisDetailComponent } from './produit-avec-devis-detail/produit-avec-devis-detail.component'; +import { ProduitSansDevisDetailComponent } from './produit-sans-devis-detail/produit-sans-devis-detail.component'; + +export function HttpLoaderFactory(http: HttpClient) { + return new TranslateHttpLoader(http, './assets/i18n/', '.json'); +} @NgModule({ imports: [ @@ -21,14 +55,57 @@ import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.compon HttpClientModule, NavbarModule, FooterModule, + FormsModule, SidebarModule, - AppRoutingModule + AppRoutingModule, + ReactiveFormsModule, + HttpClientModule, + TranslateModule.forRoot({ + defaultLanguage: 'en', + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }), ], declarations: [ AppComponent, - AdminLayoutComponent + AdminLayoutComponent, + VitrineComponent, + CarouselHomePageComponent, + HeaderHomePageComponent, + ClientsCarouselComponent, + IOTCarouselComponent, + RegisterComponent, + FooterHomePageComponent, + AboutUsComponent, + ContactUsComponent, + ChatBotComponent, + BlogsComponent, + FormProductsComponent, + UsersListsComponent, + BlogslistComponent, + FormblogComponent, + ProductsComponent, + Carousel2Component, + CarteProduitComponent, + CarteProduitNodevisComponent, + PetitCadreComponent, + TitreproduitComponent, + NavbarComponent, + FormulaireIotItComponent, + FormFranchiseComponent, + ListeFranchisesComponent, + FranchiseDetailComponent, + ListDevisComponent, + DevisDetailComponent, + ProduitAvecDevisDetailComponent, + ProduitSansDevisDetailComponent + ], + providers: [ + { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ], - providers: [], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index 993dc346d..d054cbf15 100644 --- a/src/app/app.routing.ts +++ b/src/app/app.routing.ts @@ -1,12 +1,76 @@ -import { NgModule } from '@angular/core'; +import { Component, NgModule } from '@angular/core'; import { CommonModule, } from '@angular/common'; import { BrowserModule } from '@angular/platform-browser'; import { Routes, RouterModule } from '@angular/router'; import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.component'; +import { VitrineComponent } from './vitrine/vitrine.component'; +import { RegisterComponent } from './register/register.component'; +import { AboutUsComponent } from './about-us/about-us.component'; +import { ContactUsComponent } from './contact-us/contact-us.component'; +import { BlogsComponent } from './blogs/blogs.component'; +import { FormProductsComponent } from './form-products/form-products.component'; +import { TablesComponent } from './tables/tables.component'; +import { UsersListsComponent } from './users-lists/users-lists.component'; +import { BlogslistComponent } from './blogslist/blogslist.component'; +import { FormblogComponent } from './formblog/formblog.component'; +import {ProductsComponent} from './products/products.component'; +import {FormulaireIotItComponent} from './formulaire-iot-it/formulaire-iot-it.component'; +import { FormFranchiseComponent } from './form-franchise/form-franchise.component'; +import { ListeFranchisesComponent } from './liste-franchises/liste-franchises.component'; +import { FranchiseDetailComponent } from './franchise-detail/franchise-detail.component'; +import { ListDevisComponent } from './list-devis/list-devis.component'; +import { DevisDetailComponent } from './devis-detail/devis-detail.component'; +import { AuthGuard } from './auth/auth.guard'; +import { ProduitAvecDevisDetailComponent } from './produit-avec-devis-detail/produit-avec-devis-detail.component'; +import { ProduitSansDevisDetailComponent } from './produit-sans-devis-detail/produit-sans-devis-detail.component'; const routes: Routes =[ + + { path: '', component: VitrineComponent }, + { path: 'products', component: ProductsComponent }, + { path: 'home', component: VitrineComponent }, + { path: 'auth', component: RegisterComponent }, + { path: 'about', component: AboutUsComponent }, + { path: 'contact', component: ContactUsComponent }, + {path:'blogs',component:BlogsComponent}, + { path: 'formulaireiotit', component: FormulaireIotItComponent ,canActivate: [AuthGuard], + data: { role: 'Client' },}, + { path: 'formulairefranchise', component: FormFranchiseComponent ,canActivate: [AuthGuard], + data: { role: 'Client' },}, { + path: 'admin', + redirectTo: 'dashboard', + pathMatch: 'full', + }, { + path: '', + component: AdminLayoutComponent, + canActivate: [AuthGuard], + data: { role: 'Administrateur' }, + children: [ + { + path: '', + loadChildren: () => import('./layouts/admin-layout/admin-layout.module').then(x => x.AdminLayoutModule) + }, + { path: 'add-product', component: FormProductsComponent }, + { path: 'update-product/:id', component: FormProductsComponent }, + { path: 'listProducts', component: TablesComponent }, + { path:'listblogs',component:BlogslistComponent}, + { path:'listusers',component:UsersListsComponent}, + { path: 'add-blog', component: FormblogComponent }, + { path: 'update-blog/:id', component: FormblogComponent }, + { path:'franchises',component:ListeFranchisesComponent}, + { path: 'franchises/:id', component: FranchiseDetailComponent }, + { path: 'listDevis', component: ListDevisComponent }, + { path: 'devis/:id', component: DevisDetailComponent }, + { path: 'admin', redirectTo: 'dashboard', pathMatch: 'full' }, + {path:'produit-iot/:id',component:ProduitAvecDevisDetailComponent}, + {path:'produit-gps/:id',component:ProduitSansDevisDetailComponent} + ] + }, + // { path: '**', redirectTo: 'home', pathMatch: 'full' } +] + /*{ path: '', redirectTo: 'dashboard', pathMatch: 'full', @@ -14,15 +78,25 @@ const routes: Routes =[ path: '', component: AdminLayoutComponent, children: [ - { - path: '', - loadChildren: () => import('./layouts/admin-layout/admin-layout.module').then(x => x.AdminLayoutModule) - }]}, + { + path: '', + loadChildren: () => import('./layouts/admin-layout/admin-layout.module').then(x => x.AdminLayoutModule) + }, + { path: 'add-product', component: FormProductsComponent }, + { path: 'update-product/:id', component: FormProductsComponent }, + { path: 'listProducts', component: TablesComponent }, + {path:'listblogs',component:BlogslistComponent}, + {path:'listusers',component:UsersListsComponent}, + { path: 'add-blog', component: FormblogComponent }, + { path: 'update-blog/:id', component: FormblogComponent }, + { path: 'admin', redirectTo: 'dashboard', pathMatch: 'full' }, + ] + }, { - path: '**', - redirectTo: 'dashboard' + path: '**',component: VitrineComponent + } -]; +];*/ @NgModule({ imports: [ diff --git a/src/app/auth/auth.guard.ts b/src/app/auth/auth.guard.ts new file mode 100644 index 000000000..5bec5b41b --- /dev/null +++ b/src/app/auth/auth.guard.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { AuthService } from 'app/Services/auth.service'; +import { CookieService } from 'ngx-cookie-service'; +import { Observable } from 'rxjs'; +import Swal from 'sweetalert2'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate { + constructor(private router: Router,private authService: AuthService,private cookieService: CookieService) {} + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { + + const token = this.authService.getToken(); + const role = this.authService.getRole(); + const expectedRole = route.data['role']; + + const url = state.url; + if ((url.includes('formulairefranchise') || url.includes('formulaireiotit')) + &&(!token|| role === 'Administrateur') + ) { + Swal.fire({ + icon: 'error', + title: 'Login Required', + text: 'You must be logged in as a client to access this form.', + confirmButtonText: 'Login' + }).then(() => { + this.cookieService.set('redirectAfterLogin', state.url); + this.router.navigate(['/auth']); + }); + return false; + } + + + if (expectedRole !== undefined && role !== expectedRole) { + Swal.fire({ + icon: 'error', + title: 'Access Denied', + text: 'You do not have permission to access this page.', + confirmButtonText: 'Go to Login' + }).then(() => { + event.preventDefault(); + this.router.navigate(['/auth']); + }); + return false; + } + if (!this.authService.isLoggedIn()) { + Swal.fire({ + icon: 'warning', + title: 'Session Expired', + text: 'Your session has expired, please log in again.', + confirmButtonText: 'Log In' + }).then(() => { + this.authService.logout(); + this.router.navigate(['/auth']); + }); + return false; + } + + // ✅ Sinon, tout est bon + return true; + + } + +} diff --git a/src/app/auth/auth.interceptor.ts b/src/app/auth/auth.interceptor.ts new file mode 100644 index 000000000..1e7a7d043 --- /dev/null +++ b/src/app/auth/auth.interceptor.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor +} from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { AuthService } from 'app/Services/auth.service'; +import { Router } from '@angular/router'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + constructor(private authService: AuthService,private router: Router) {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + const token = this.authService.getToken(); + if (token) { + request = request.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); + } + return next.handle(request); +} + +} diff --git a/src/app/blogs/blogs.component.html b/src/app/blogs/blogs.component.html new file mode 100644 index 000000000..75baf4204 --- /dev/null +++ b/src/app/blogs/blogs.component.html @@ -0,0 +1,70 @@ + + +
+ + + + + + + + + +




+ +
+ +
+
+
+
+

+
+

{{ blog.titre }}

+

+
+
+

+
+ +
+
+ + + +
+



+ +
+ + + + + + + + + diff --git a/src/app/blogs/blogs.component.scss b/src/app/blogs/blogs.component.scss new file mode 100644 index 000000000..c5fc87244 --- /dev/null +++ b/src/app/blogs/blogs.component.scss @@ -0,0 +1,315 @@ +.co-blog-component { + background: url('/assets/img/infinite-loop-01.jpg') no-repeat center center; + background-size: cover; + min-height: 100vh; + position: relative; + top:0; + margin: 0; + padding: 0; + .blog-container { + background: #fff; + border-radius: 5px; + box-shadow: hsla(0, 0, 0, .2) 0 4px 2px -2px; + font-family: "adelle-sans", sans-serif; + font-weight: 100; + width: 20rem; + margin: 0 auto 48px; + + @media screen and (min-width: 480px) { + width: 28rem; + } + @media screen and (min-width: 767px) { + width: 40rem; + } + @media screen and (min-width: 959px) { + width: 50rem; + } + } + + .blog-container a { + color: #4d4dff; + text-decoration: none; + transition: .25s ease; + + &:hover { + border-color: #ff4d4d; + color: #ff4d4d; + } + } + + .blog-cover { + background-size: cover; + border-radius: 5px 5px 0 0; + height: 25rem; + box-shadow: inset hsla(0, 0, 0, .2) 0 64px 64px 16px; + } + + .blog-author, + .blog-author--no-cover { + margin: 0 auto; + padding-top: .125rem; + width: 80%; + } + + .blog-author h3::before, + .blog-author--no-cover h3::before { + background: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/17779/russ.jpeg"); + background-size: cover; + border-radius: 50%; + content: " "; + display: inline-block; + height: 32px; + margin-right: .5rem; + position: relative; + top: 8px; + width: 32px; + } + + .blog-author h3 { + color: #fff; + font-weight: 100; + } + + .blog-author--no-cover h3 { + color: lighten(#333, 40%); + font-weight: 100; + } + + .blog-body { + margin: 0 auto; + width: 80%; + } + + .video-body { + height: 100%; + width: 100%; + } + + .blog-title h1 a { + color: #333; + font-weight: 100; + } + + .blog-summary p { + color: lighten(#333, 10%); + } + + .blog-tags ul { + display: flex; + flex-direction: row; + flex-wrap: wrap; + list-style: none; + padding-left: 0; + } + + .blog-tags li + li { + margin-left: .5rem; + } + + .blog-tags a { + border: 1px solid lighten(#333, 40%); + border-radius: 3px; + color: lighten(#333, 40%); + font-size: .75rem; + height: 1.5rem; + line-height: 1.5rem; + letter-spacing: 1px; + padding: 0 .5rem; + text-align: center; + text-transform: uppercase; + white-space: nowrap; + width: 5rem; + } + + .blog-footer { + border-top: 1px solid lighten(#333, 70%); + margin: 0 auto; + padding-bottom: .125rem; + width: 80%; + } + + .blog-footer ul { + list-style: none; + display: flex; + flex: row wrap; + justify-content: flex-end; + padding-left: 0; + } + + .blog-footer li:first-child { + margin-right: auto; + } + + .blog-footer li + li { + margin-left: .5rem; + } + + .blog-footer li { + color: lighten(#333, 40%); + font-size: .75rem; + height: 1.5rem; + letter-spacing: 1px; + line-height: 1.5rem; + text-align: center; + text-transform: uppercase; + position: relative; + white-space: nowrap; + + & a { + color: lighten(#333, 40%); + } + } + + + .published-date { + border: 1px solid lighten(#333, 40%); + border-radius: 3px; + padding: 0 .5rem; + } + + + .icon-star, + .icon-bubble { + fill: lighten(#333, 40%); + height: 35px; + margin-right: .5rem; + transition: .25s ease; + width: 35px; + + &:hover { + fill: #ff4d4d; + } + } +} +.like-button { + background: transparent; + border: none; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 16px; + padding: 0; + vertical-align: middle; +} + +.like-button svg.icon-heart { + width: 24px; + height: 24px; + transition: fill 0.3s ease; + display: block; +} + +.like-button .numero { + color: #333; + font-weight: 500; + line-height: 1; + vertical-align: middle; + margin-top: 2px; /* Ajustement fin */ + user-select: none; +} + +@media screen and (max-width: 767px) { + .co-blog-component { + padding: 20px; + + .blog-container { + width: 90%; + margin-bottom: 32px; + } + + .blog-cover { + height: 12rem; + } + + .blog-author, + .blog-author--no-cover { + width: 100%; + padding-top: 0.5rem; + text-align: center; + + h3::before { + height: 28px; + width: 28px; + margin-right: 0.3rem; + top: 4px; + } + + h3 { + font-size: 1rem; + } + } + + .blog-body, + .blog-footer { + width: 100%; + } + + .blog-title h1 a { + font-size: 1.4rem; + } + + .blog-summary p { + font-size: 1rem; + } + + .blog-tags ul { + justify-content: center; + } + + .blog-tags a { + font-size: 0.65rem; + width: auto; + padding: 0 0.4rem; + } + + .blog-footer ul { + justify-content: center; + flex-wrap: wrap; + } + + .blog-footer li { + font-size: 0.65rem; + margin-bottom: 0.3rem; + } + + .icon-star, + .icon-bubble { + width: 28px; + height: 28px; + } + + .published-date { + font-size: 0.7rem; + padding: 0 0.3rem; + } + } + + .like-button { + font-size: 14px; + } + + .like-button svg.icon-heart { + width: 20px; + height: 20px; + } + + .like-button .numero { + font-size: 0.9rem; + } +} + +@media screen and (min-width: 768px) and (max-width: 991px) { + .co-blog-component .blog-container { + width: 80%; + } + + .co-blog-component .blog-cover { + height: 14rem; + } + + .co-blog-component .blog-title h1 a { + font-size: 1.8rem; + } +} + diff --git a/src/app/blogs/blogs.component.spec.ts b/src/app/blogs/blogs.component.spec.ts new file mode 100644 index 000000000..4803d0178 --- /dev/null +++ b/src/app/blogs/blogs.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BlogsComponent } from './blogs.component'; + +describe('BlogsComponent', () => { + let component: BlogsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ BlogsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(BlogsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/blogs/blogs.component.ts b/src/app/blogs/blogs.component.ts new file mode 100644 index 000000000..826d230f8 --- /dev/null +++ b/src/app/blogs/blogs.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit } from '@angular/core'; +import { Blog } from 'app/blogslist/blogslist.component'; +import { BlogService } from 'app/Services/BlogService'; +import { environment } from 'environments/environment'; + +@Component({ + selector: 'app-blogs', + templateUrl: './blogs.component.html', + styleUrls: ['./blogs.component.scss'] +}) +export class BlogsComponent implements OnInit { + blogPosts = [ + ]; + apiBaseUrl = environment.baseUrl; + constructor(private blogService: BlogService) { + } + loadBlogs() { + this.blogService.getAllBlogs().subscribe({ + next: blogs => { + this.blogPosts = blogs.map(blog => ({ + ...blog, + liked: false + })); + }, + error: err => console.error(err) + }); +} + ngOnInit() { + this.loadBlogs() + } + getImageUrl(imagePath: string): string { + return this.apiBaseUrl + imagePath; + } + onLike(blog: Blog, index: number): void { + this.blogService.incrementLike(blog.id).subscribe({ + next: (res) => { + this.blogPosts[index].likes = res.likes; + this.blogPosts[index].liked = true; + }, + error: (err) => console.error('Erreur lors du like :', err) + }); + } + transformNewlines(text: string): string { + if (!text) return ''; + return text.replace(/\n/g, '
'); +} +} diff --git a/src/app/blogslist/blogslist.component.html b/src/app/blogslist/blogslist.component.html new file mode 100644 index 000000000..2a0fbf494 --- /dev/null +++ b/src/app/blogslist/blogslist.component.html @@ -0,0 +1,30 @@ +
+
+

Liste des Blogs

+ + + +
+
+
+
+ image blog +
+
+
{{ blog.titre }}
+

{{ blog.contenu }}

+
+
+ {{ tag.nom }} +
+
+ {{ blog.likes }} + + +
+
+
+
+
+
+
diff --git a/src/app/blogslist/blogslist.component.scss b/src/app/blogslist/blogslist.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/blogslist/blogslist.component.spec.ts b/src/app/blogslist/blogslist.component.spec.ts new file mode 100644 index 000000000..4a90aa9ab --- /dev/null +++ b/src/app/blogslist/blogslist.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BlogslistComponent } from './blogslist.component'; + +describe('BlogslistComponent', () => { + let component: BlogslistComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ BlogslistComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(BlogslistComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/blogslist/blogslist.component.ts b/src/app/blogslist/blogslist.component.ts new file mode 100644 index 000000000..8d48a352e --- /dev/null +++ b/src/app/blogslist/blogslist.component.ts @@ -0,0 +1,69 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import Swal from 'sweetalert2'; +import { environment } from '../../environments/environment'; +import { BlogService } from 'app/Services/BlogService'; + +export interface Blog { + id: number; + titre: string; + contenu: string; + imagePath: string; + tags: string[]; + likes: number; +} + +@Component({ + selector: 'app-blogslist', + templateUrl: './blogslist.component.html', + styleUrls: ['./blogslist.component.scss'] +}) +export class BlogslistComponent implements OnInit { + blogs: Blog[] = []; +apiBaseUrl = environment.baseUrl; + + constructor(private router: Router, private blogService: BlogService) { } + + ngOnInit() { + this.loadBlogs(); + } + + loadBlogs() { + this.blogService.getAllBlogs().subscribe({ + next: (data) => this.blogs = data, + error: (err) => console.error('Erreur lors du chargement des blogs:', err) + }); + } + + editBlog(id: number) { + this.router.navigate(['/update-blog', id]); + } + + confirmDelete(id: number) { + Swal.fire({ + title: 'Supprimer ce blog ?', + text: 'Cette action est irréversible.', + icon: 'warning', + showCancelButton: true, + confirmButtonText: 'Oui, supprimer', + cancelButtonText: 'Annuler' + }).then((result) => { + if (result.isConfirmed) { + this.blogService.deleteBlog(id).subscribe({ + next: () => { + this.blogs = this.blogs.filter(b => b.id !== id); + Swal.fire('Supprimé !', 'Le blog a été supprimé.', 'success'); + }, + error: (err) => { + console.error(err); + Swal.fire('Erreur', 'Une erreur est survenue lors de la suppression.', 'error'); + } + }); + } + }); +} + +getImageUrl(imagePath: string): string { + return this.apiBaseUrl + imagePath; + } +} diff --git a/src/app/carousel-home-page/carousel-home-page.component.html b/src/app/carousel-home-page/carousel-home-page.component.html new file mode 100644 index 000000000..24ff9f8c2 --- /dev/null +++ b/src/app/carousel-home-page/carousel-home-page.component.html @@ -0,0 +1,42 @@ + + +
+ + + +
diff --git a/src/app/carousel-home-page/carousel-home-page.component.scss b/src/app/carousel-home-page/carousel-home-page.component.scss new file mode 100644 index 000000000..07a1774af --- /dev/null +++ b/src/app/carousel-home-page/carousel-home-page.component.scss @@ -0,0 +1,227 @@ + +.carousel-container { + height: 100%; + width: 100%; + +background-image: url(/assets/img/HomeCarousselBackground.png); + background-repeat: no-repeat; + background-position: center center; + background-size: cover; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + position: relative; + overflow: hidden; +} + +.carousel-slide { + display: flex; + justify-content: space-between; + align-items: center; + width: 90%; + max-width: 1400px; + padding: 40px; + box-sizing: border-box; + position: relative; + transition: opacity 1s ease-in-out; + opacity: 1; +} + +.fade { + animation: fadeEffect 1s; +} + +@keyframes fadeEffect { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.left-content { + color: white; + width: 50%; +} + +.title { + font-size: 2.8rem; + font-weight: bold; + margin: 0 0 15px; +} + +.subtitle { + font-size: 1.5rem; + color: #369; + margin-bottom: 10px; +} + +.description { + font-size: 1.1rem; + margin-bottom: 20px; + line-height: 1.5; +} + +.button-wrapper { + margin-top: 10px; +} + +.button-wrapper button { + padding: 10px 20px; + font-size: 1rem; + background-color: #007bff; + border: none; + border-radius: 6px; + color: white; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.button-wrapper button:hover { + background-color: #0056b3; +} + +.right-image { + width: 45%; + display: flex; + justify-content: center; + align-items: center; +} + +.right-image img { + width: 300px; + height: 300px; + object-fit: cover; + border-radius: 50%; + border: 5px solid #ffffff96; +} + +.dots { + position: absolute; + bottom: 40px; + display: flex; + gap: 10px; +} + +.dots span { + width: 10px; + height: 10px; + background-color: gray; + border-radius: 50%; + cursor: pointer; + transition: background-color 0.3s; +} + +.dots span.active { + background-color: white; + transform: scale(1.4); +} + +.scroll-arrow { + position: absolute; + right: 80px; + bottom: 20px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 50%; + padding: 10px; + cursor: pointer; + animation: saut 2s infinite; +} + +@keyframes saut{ + 0%,20%,50%,80%,100% { + transform:translateY(0); + } + 40%{ + transform: translateY(15px); + + } + 60%{ + transform: translateY(15px); + } +} + +.scroll-arrow img { + width: 40px; + height: 40px; +} +@media screen and (max-width: 767px) { + .carousel-slide { + flex-direction: column; + padding: 20px; + text-align: center; + } + + .left-content { + width: 100%; + margin-bottom: 20px; + } + + .title { + font-size: 1.8rem; + } + + .subtitle { + font-size: 1.2rem; + } + + .description { + font-size: 1rem; + } + + .right-image { + width: 100%; + } + + .right-image img { + width: 200px; + height: 200px; + } + + .dots { + bottom: 20px; + } + + .scroll-arrow { + right: 20px; + bottom: 10px; + padding: 8px; + } + + .scroll-arrow img { + width: 30px; + height: 30px; + } +} + +@media screen and (min-width: 768px) and (max-width: 991px) { + .carousel-slide { + padding: 30px; + } + + .title { + font-size: 2.2rem; + } + + .subtitle { + font-size: 1.4rem; + } + + .description { + font-size: 1.05rem; + } + + .right-image img { + width: 250px; + height: 250px; + } + + .scroll-arrow { + right: 50px; + bottom: 15px; + } +} diff --git a/src/app/carousel-home-page/carousel-home-page.component.spec.ts b/src/app/carousel-home-page/carousel-home-page.component.spec.ts new file mode 100644 index 000000000..afccc81d9 --- /dev/null +++ b/src/app/carousel-home-page/carousel-home-page.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CarouselHomePageComponent } from './carousel-home-page.component'; + +describe('CarouselHomePageComponent', () => { + let component: CarouselHomePageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CarouselHomePageComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CarouselHomePageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/carousel-home-page/carousel-home-page.component.ts b/src/app/carousel-home-page/carousel-home-page.component.ts new file mode 100644 index 000000000..1bf1358aa --- /dev/null +++ b/src/app/carousel-home-page/carousel-home-page.component.ts @@ -0,0 +1,62 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-carousel-home-page', + templateUrl: './carousel-home-page.component.html', + styleUrls: ['./carousel-home-page.component.scss'] +}) +export class CarouselHomePageComponent implements OnInit { + + slides = [ + { + titleKey: 'CAROUSEL.SLIDE1.TITLE', + subtitleKey: 'CAROUSEL.SLIDE1.SUBTITLE', + descriptionKey: 'CAROUSEL.SLIDE1.DESCRIPTION', + image: '/assets/img/route.png' + }, + { + titleKey: 'CAROUSEL.SLIDE2.TITLE', + subtitleKey: 'CAROUSEL.SLIDE2.SUBTITLE', + descriptionKey: 'CAROUSEL.SLIDE2.DESCRIPTION', + image: '/assets/img/iot.png' + }, + { + titleKey: 'CAROUSEL.SLIDE3.TITLE', + subtitleKey: 'CAROUSEL.SLIDE3.SUBTITLE', + descriptionKey: 'CAROUSEL.SLIDE3.DESCRIPTION', + image: '/assets/img/service.png' + } + ]; + + currentSlide = 0; + intervalId: any; + + constructor(private router: Router) {} + + ngOnInit() { + this.intervalId = setInterval(() => this.nextSlide(), 4000); + } + + ngOnDestroy() { + clearInterval(this.intervalId); + } + + nextSlide() { + this.currentSlide = (this.currentSlide + 1) % this.slides.length; + } + + goToSlide(index: number) { + this.currentSlide = index; + } + + goToVitrine() { + this.router.navigate(['/vitrine']); + } + + goToHomeAndScroll(section: string, event: Event) { + event.preventDefault(); + this.router.navigate([''], { queryParams: { section } }); +} + +} diff --git a/src/app/carousel2/carousel2.component.css b/src/app/carousel2/carousel2.component.css new file mode 100644 index 000000000..5fe9327c7 --- /dev/null +++ b/src/app/carousel2/carousel2.component.css @@ -0,0 +1,283 @@ +.carousel-container { + width: 100%; + height: 400px; + /* background-image: url('assets/img/infinite-loop-01.jpg'); */ + background-repeat: no-repeat; + background-position: center center; + background-size: cover; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + position: relative; + overflow: hidden; +} + +.carousel-slide { + display: flex; + justify-content: space-between; + align-items: center; + width: 90%; + max-width: 1400px; + padding: 40px; + box-sizing: border-box; + position: relative; + transition: opacity 1s ease-in-out; + opacity: 1; +} + +.fade { + animation: fadeEffect 1s; +} + +@keyframes fadeEffect { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.left-content { + color: white; + width: 50%; +} + +.title { + font-size: 2.8rem; + font-weight: bold; + margin: 0 0 15px; +} + +.subtitle { + font-size: 1.5rem; + color: #00aaff; + margin-bottom: 10px; +} + +.description { + font-size: 1.1rem; + margin-bottom: 20px; + line-height: 1.5; +} + +.button-wrapper { + margin-top: 10px; +} + +.button-wrapper button { + padding: 10px 20px; + font-size: 1rem; + background-color: #007bff; + border: none; + border-radius: 6px; + color: white; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.button-wrapper button:hover { + background-color: #0056b3; +} + +.right-image { + width: 45%; + display: flex; + justify-content: center; + align-items: center; +} + +.right-image img { + width: 300px; + height: 300px; + object-fit: cover; + border-radius: 50%; + border: 5px solid #ffffff96; +} + +.dots { + position: absolute; + bottom: 40px; + display: flex; + gap: 10px; +} + +.dots span { + width: 10px; + height: 10px; + background-color: gray; + border-radius: 50%; + cursor: pointer; + transition: background-color 0.3s; +} + +.dots span.active { + background-color: white; + transform: scale(1.4); +} + +.scroll-arrow { + position: absolute; + right: 20px; + bottom: 20px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 50%; + padding: 10px; + cursor: pointer; + animation: saut 2s infinite; +} + +@keyframes saut{ + 0%,20%,50%,80%,100% { + transform:translateY(0); + } + 40%{ + transform: translateY(15px); + + } + 60%{ + transform: translateY(15px); + } +} + +.scroll-arrow img { + width: 40px; + height: 40px; +} +/* ✅ Responsive sans changement de layout : on réduit juste les tailles */ + +@media (max-width: 1024px) { + .carousel-container { + height: 350px; + } + + .carousel-slide { + padding: 30px 20px; + } + + .title { + font-size: 2rem; + } + + .subtitle { + font-size: 1.3rem; + } + + .description { + font-size: 1rem; + } + + .right-image img { + width: 220px; + height: 220px; + } + + .scroll-arrow { + right: 15px; + bottom: 15px; + } +} + +@media (max-width: 768px) { + .carousel-container { + height: 300px; + } + + .title { + font-size: 1.7rem; + } + + .subtitle { + font-size: 1.1rem; + } + + .description { + font-size: 0.95rem; + } + + .right-image img { + width: 180px; + height: 180px; + } + + .scroll-arrow img { + width: 30px; + height: 30px; + } + + .dots span { + width: 8px; + height: 8px; + } +} + +@media (max-width: 480px) { + .carousel-container { + height: 260px; + } + + .carousel-slide { + padding: 20px 10px; + } + + .title { + font-size: 1.4rem; + } + + .subtitle { + font-size: 1rem; + } + + .description { + font-size: 0.85rem; + } + + .right-image img { + width: 140px; + height: 140px; + } + + .scroll-arrow { + right: 10px; + bottom: 10px; + } + + .scroll-arrow img { + width: 25px; + height: 25px; + } +} + +/* hethy un peu plus petite lektiba */ +@media (max-width: 380px) { + .title { + font-size: 1.2rem; + } + + .subtitle { + font-size: 0.9rem; + } + + .description { + font-size: 0.75rem; + } + + .right-image img { + width: 120px; + height: 120px; + } + + .scroll-arrow img { + width: 22px; + height: 22px; + } + + .carousel-slide { + padding: 15px 8px; + } +} + + diff --git a/src/app/carousel2/carousel2.component.html b/src/app/carousel2/carousel2.component.html new file mode 100644 index 000000000..f02e398fa --- /dev/null +++ b/src/app/carousel2/carousel2.component.html @@ -0,0 +1,28 @@ + diff --git a/src/app/carousel2/carousel2.component.spec.ts b/src/app/carousel2/carousel2.component.spec.ts new file mode 100644 index 000000000..b04d98e68 --- /dev/null +++ b/src/app/carousel2/carousel2.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Carousel2Component } from './carousel2.component'; + +describe('Carousel2Component', () => { + let component: Carousel2Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ Carousel2Component ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Carousel2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/carousel2/carousel2.component.ts b/src/app/carousel2/carousel2.component.ts new file mode 100644 index 000000000..aca6bc82b --- /dev/null +++ b/src/app/carousel2/carousel2.component.ts @@ -0,0 +1,62 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-carousel2', + templateUrl: './carousel2.component.html', + styleUrls: ['./carousel2.component.css'] +}) +export class Carousel2Component implements OnInit, OnDestroy { + slides = [ + { + titleKey: 'CAROUSEL.SLIDE1.TITLE', + subtitleKey: 'CAROUSEL.SLIDE1.SUBTITLE', + descriptionKey: 'CAROUSEL.SLIDE1.DESCRIPTION', + image: 'assets/img/route.png' + }, + { + titleKey: 'CAROUSEL.SLIDE2.TITLE', + subtitleKey: 'CAROUSEL.SLIDE2.SUBTITLE', + descriptionKey: 'CAROUSEL.SLIDE2.DESCRIPTION', + image: 'assets/img/iot.png' + }, + { + titleKey: 'CAROUSEL.SLIDE3.TITLE', + subtitleKey: 'CAROUSEL.SLIDE3.SUBTITLE', + descriptionKey: 'CAROUSEL.SLIDE3.DESCRIPTION', + image: 'assets/img/service.png' + } + ]; + + currentSlide = 0; + intervalId: any; + + constructor(private router: Router) {} + + ngOnInit() { + this.intervalId = setInterval(() => this.nextSlide(), 4000); + } + + ngOnDestroy() { + clearInterval(this.intervalId); + } + + nextSlide() { + this.currentSlide = (this.currentSlide + 1) % this.slides.length; + } + + goToSlide(index: number) { + this.currentSlide = index; + } + + goToVitrine() { + this.router.navigate(['/vitrine']); + } + + scrollToGps() { + const titreSection = document.getElementById('partietitre'); + if (titreSection) { + titreSection.scrollIntoView({ behavior: 'smooth' }); + } + } +} diff --git a/src/app/carte-produit-nodevis/carte-produit-nodevis.component.css b/src/app/carte-produit-nodevis/carte-produit-nodevis.component.css new file mode 100644 index 000000000..cf3fbfe47 --- /dev/null +++ b/src/app/carte-produit-nodevis/carte-produit-nodevis.component.css @@ -0,0 +1,297 @@ +.carte-produit { + background-color: #e9ecef; + position: relative; + width: 300px; + height: 360px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0px 10px 1px #38B; + cursor: pointer; + transition: transform 0.25s ease-in-out 0s; +} + +.carte-produit:hover { + transform: scale(1.02); + transform: translateY(-3px); +} + +.image-fond { + width: 100%; + height: 90%; + object-fit: contain; + position: relative; + margin-top: 10px; + +} + +.infos-base { + position: absolute; + /* bottom: 0px; */ + left: 20px; + color: white; +} + +.categorie { + color: #01b4ff; + font-weight: bold; + font-size: 14px; + display: block; + top: 50px; +} + +.titre { + color: #01b4ff; + font-size: 14px; + font-weight: bold; + display: block; + margin-bottom: 20px; +} + +.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.50); + color: white; + padding: 20px; + display: flex; + align-items: flex-end; + box-sizing: border-box; +} + +.infos-hover h3 { + margin: 0 0 10px; + font-size: 18px; +} + +.infos-hover p { + font-size: 14px; + margin-bottom: 15px; +} + + + +.liens { + display: flex; + gap: 12px; +} + +.voir-details { + color: white; + text-decoration: none; + font-weight: bold; + border-bottom: 3px solid white; + font-size: 14px; +} +/* //////////////// */ +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(3px); + display: flex; + justify-content: center; + align-items: center; + z-index: 1001; +} + +.modal-container { + border-radius: 20px; + background-color: white; + width: 55%; + overflow-y: auto; + position: relative; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25); + animation: fadeIn 0.3s ease-in-out; +} + +.modal-top { + background-image: url('assets/img/infinite-loop-01.jpg'); + background-repeat: no-repeat; + background-size: cover; + /* background-position: center ; */ + /* background-color: #38B; */ + padding: 30px; + color: #ffffff; + height: 160px; +} + +.categorie h5 { + margin: 0; + font-size: 16px; + background: #fff; + color: #38B; + display: inline-block; + padding: 4px 10px; + border-radius: 10px; + font-weight: bold; + +} + + +.titre-et-description h2 { + font-size: 30px; + font-weight: bold; + /* margin: 10px 0 8px; */ + color: #fff; +} + +.titre-et-description p { + margin: 0; + font-size: 10px; + color: #fff; +} + +.modal-image { + width: 100%; + height: auto; + margin-top: 20px; + max-height: 300px; + object-fit: contain; + border-radius: 10px; + margin-bottom: 20px; +} + +.modal-content { + padding: 10px; + max-height: 90vh; +} + +.modal-content h2 { + font-size: 24px; + margin-bottom: 10px; + color: #041c44; +} + +.modal-content p { + font-size: 16px; + margin-bottom: 20px; + color: #333; +} + +.modal-bottom { + display: flex; + justify-content: space-between; + gap: 20px; +} +.imagee{ + /* background-color: #f2f2f2; */ + padding: 12px 16px; + border-radius: 10px; + width: 65%; +} +.caracteristiques-et-tarif{ + display: flex; + flex-direction: column; + gap:20px; + max-width: 50%; + min-width: 40%; +} +.caracteristiques{ + height: 60%; + background-color: #f2f2f2; + padding: 12px 16px; + border-radius: 10px; + word-wrap: break-word; + overflow-wrap: break-word; + text-align: left; +} +.tarif{ + height: auto; + background-color: #f2f2f2; + padding: 12px 16px; + border-radius: 10px; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.caracteristiques h4, +.tarif h4 { + margin-top: 0; + margin-bottom: 8px; + color: #38B; + font-size: 18px; + text-decoration: underline; + font-weight: bold; + text-align: left; +} +.tarif h2 { + font-weight: bold; + color: #000000; + display: block; + text-align: left; +} + +.caracteristiques ul { + padding-left: 20px; + margin: 0; +} +.caracteristiques li{ + /* font-weight: bold; */ + color: black; +} + +.tarif p { + font-weight: bold; + font-size: 20px; + margin: 10px 0; + color: orange; +} + +.btn-devis { + background-color: #01b4ff; + color: white; + padding: 8px 16px; + border-radius: 6px; + border: none; + cursor: pointer; + font-weight: bold; +} + +.modal-close { + color: #ffffff; + position: absolute; + top: 0px; + right: 16px; + font-size: 40px; + background: none; + border: none; + cursor: pointer; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} +.btn-panier { + margin-top: 20px; + background-color: #01b4ff; + width: 100%; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-weight: bold; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s ease-in-out; + text-align: center; +} + +.btn-panier:hover { + background-color: #008fcc; +} + + + diff --git a/src/app/carte-produit-nodevis/carte-produit-nodevis.component.html b/src/app/carte-produit-nodevis/carte-produit-nodevis.component.html new file mode 100644 index 000000000..5d81aaab0 --- /dev/null +++ b/src/app/carte-produit-nodevis/carte-produit-nodevis.component.html @@ -0,0 +1,64 @@ + +
+ + +
+ +

{{ titre }}

+
+ +
+
+

{{ titre }}

+

{{ description }}

+ +
+
+
+ + + + + diff --git a/src/app/carte-produit-nodevis/carte-produit-nodevis.component.spec.ts b/src/app/carte-produit-nodevis/carte-produit-nodevis.component.spec.ts new file mode 100644 index 000000000..f5acb117d --- /dev/null +++ b/src/app/carte-produit-nodevis/carte-produit-nodevis.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CarteProduitNodevisComponent } from './carte-produit-nodevis.component'; + +describe('CarteProduitNodevisComponent', () => { + let component: CarteProduitNodevisComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CarteProduitNodevisComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CarteProduitNodevisComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/carte-produit-nodevis/carte-produit-nodevis.component.ts b/src/app/carte-produit-nodevis/carte-produit-nodevis.component.ts new file mode 100644 index 000000000..737bcc90f --- /dev/null +++ b/src/app/carte-produit-nodevis/carte-produit-nodevis.component.ts @@ -0,0 +1,37 @@ +import { Component, Input } from '@angular/core'; +import { environment } from 'environments/environment'; + +@Component({ + selector: 'app-carte-produit-nodevis', + templateUrl: './carte-produit-nodevis.component.html', + styleUrls: ['./carte-produit-nodevis.component.css'] +}) +export class CarteProduitNodevisComponent { + @Input() titre: string = ''; + @Input() description: string = ''; + @Input() image: string = ''; + @Input() categorie: string = ''; + @Input() prix: string = ''; + @Input() caracteristiques: { texte: string }[] = []; + + apiBaseUrl = environment.baseUrl + + + hover: boolean = false; + showModal: boolean = false; + + openModal() { + this.showModal = true; + } + + closeModal() { + this.showModal = false; + } + getImageUrl(imagePath: string): string { + if (imagePath.startsWith('/assets')) { + return imagePath; + } + return this.apiBaseUrl + imagePath; +} + +} diff --git a/src/app/carte-produit/carte-produit.component.css b/src/app/carte-produit/carte-produit.component.css new file mode 100644 index 000000000..527adabeb --- /dev/null +++ b/src/app/carte-produit/carte-produit.component.css @@ -0,0 +1,293 @@ +.carte-produit { + background-color: #e9ecef; + position: relative; + width: 300px; + height: 360px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0px 10px 1px #38B; + cursor: pointer; + transition: transform 0.25s ease-in-out 0s; +} + +.carte-produit:hover { + transform: scale(1.02); + transform: translateY(-3px); +} + +.image-fond { + width: 100%; + height: 90%; + object-fit: contain; + position: relative; + margin-top: 10px; +} + +.infos-base { + position: absolute; + left: 20px; + color: white; +} + +.categorie { + display: inline-block; + background-color: white; + color: #01b4ff; + font-weight: bold; + font-size: 10px; + padding: 5px 10px; + border-radius: 8px; + margin-bottom: 10px; +} + +.titre { + color: #01b4ff; + font-size: 14px; + font-weight: bold; + display: block; + margin-bottom: 20px; +} + +.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.50); + color: white; + padding: 20px; + display: flex; + align-items: flex-end; + box-sizing: border-box; +} + + +.infos-hover h3 { + margin: 0 0 10px; + font-size: 18px; +} + +.infos-hover p { + font-size: 14px; + margin-bottom: 15px; +} + +.btn-devis { + background-color: #01b4ff; + color: white; + border: none; + padding: 8px 14px; + border-radius: 4px; + font-size: 12px; + margin-bottom: 12px; + cursor: pointer; +} + +.liens { + display: flex; + gap: 12px; +} + +.voir-details { + color: white; + text-decoration: none; + font-weight: bold; + border-bottom: 3px solid white; + font-size: 14px; +} + +.demander-devis { + color: orange; + text-decoration: none; + font-weight: bold; + border-bottom: 3px solid orange; + font-size: 14px; +} +/* /////////////////////////// */ +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(3px); + display: flex; + justify-content: center; + align-items: center; + z-index: 1001; +} + +.modal-container { + border-radius: 20px; + background-color: white; + width: 55%; + overflow-y: auto; + position: relative; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25); + animation: fadeIn 0.3s ease-in-out; +} + +.modal-top { + background-image: url('assets/img/infinite-loop-01.jpg'); + background-repeat: no-repeat; + background-size: cover; + /* background-position: center ; */ + /* background-color: #38B; */ + padding: 30px; + color: #ffffff; + height: 160px; +} + +.categorie h5 { + margin: 0; + font-size: 16px; + background: #fff; + color: #38B; + display: inline-block; + padding: 4px 10px; + border-radius: 10px; + font-weight: bold; +} + +.titre-et-description h2 { + font-size: 30px; + font-weight: bold; + /* margin: 10px 0 8px; */ + color: #fff; +} + +.titre-et-description p { + margin: 0; + font-size: 10px; + color: #fff; +} + +.modal-image { + width: 100%; + height: auto; + margin-top: 20px; + max-height: 300px; + object-fit: contain; + border-radius: 10px; + margin-bottom: 20px; +} + +.modal-content { + padding: 10px; + max-height: 90vh; +} + +.modal-content h2 { + font-size: 24px; + margin-bottom: 10px; + color: #041c44; +} + +.modal-content p { + font-size: 16px; + margin-bottom: 20px; + color: #333; +} + +.modal-bottom { + display: flex; + justify-content: space-between; + gap: 20px; +} +.imagee{ + /* background-color: #f2f2f2; */ + padding: 12px 16px; + border-radius: 10px; + width: 65%; +} +.caracteristiques-et-tarif{ + display: flex; + flex-direction: column; + gap:20px; + max-width: 50%; +} + +.caracteristiques { + height: 60%; + background-color: #f2f2f2; + padding: 12px 16px; + border-radius: 10px; + word-wrap: break-word; + overflow-wrap: break-word; + text-align: left; +} +.tarif{ + height: auto; + background-color: #f2f2f2; + padding: 12px 16px; + border-radius: 10px; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.caracteristiques h4, +.tarif h4 { + margin-top: 0; + margin-bottom: 8px; + color: #38B; + font-size: 18px; + text-decoration: underline; + font-weight: bold; + text-align: left; +} +.tarif h2 { + font-weight: bold; + color: #000000; + display: block; + text-align: left; +} + +.caracteristiques ul { + padding-left: 20px; + margin: 0; +} +.caracteristiques li{ + /* font-weight: bold; */ + color: black; +} + +.tarif p { + font-weight: bold; + font-size: 20px; + margin: 10px 0; + color: orange; +} + +.btn-devis { + background-color: #01b4ff; + color: white; + padding: 8px 16px; + border-radius: 6px; + border: none; + cursor: pointer; + font-weight: bold; +} + +.modal-close { + color: #ffffff; + position: absolute; + top: 0px; + right: 16px; + font-size: 40px; + background: none; + border: none; + cursor: pointer; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} diff --git a/src/app/carte-produit/carte-produit.component.html b/src/app/carte-produit/carte-produit.component.html new file mode 100644 index 000000000..160188fb2 --- /dev/null +++ b/src/app/carte-produit/carte-produit.component.html @@ -0,0 +1,64 @@ + +
+ + +
+

{{ titre }}

+
+ +
+
+

{{ titre }}

+

{{ description }}

+ + +
+
+
+ + + + + diff --git a/src/app/carte-produit/carte-produit.component.spec.ts b/src/app/carte-produit/carte-produit.component.spec.ts new file mode 100644 index 000000000..c7cb1edae --- /dev/null +++ b/src/app/carte-produit/carte-produit.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CarteProduitComponent } from './carte-produit.component'; + +describe('CarteProduitComponent', () => { + let component: CarteProduitComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CarteProduitComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CarteProduitComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/carte-produit/carte-produit.component.ts b/src/app/carte-produit/carte-produit.component.ts new file mode 100644 index 000000000..1ace9328c --- /dev/null +++ b/src/app/carte-produit/carte-produit.component.ts @@ -0,0 +1,45 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { environment } from 'environments/environment'; + +@Component({ + selector: 'app-carte-produit', + templateUrl: './carte-produit.component.html', + styleUrls: ['./carte-produit.component.css'] +}) +export class CarteProduitComponent { + @Input() titre: string = ''; + @Input() description: string = ''; + @Input() image: string = ''; + @Input() categorie: string = ''; + @Input() id: number; + @Input() caracteristiques: { texte: string }[] = []; + + apiBaseUrl = environment.baseUrl + + constructor(private router: Router) {} + + hover: boolean = false; + showModal: boolean = false; + + openModal() { + this.showModal = true; + } + + closeModal() { + this.showModal = false; + } + allerAuFormulaire() { + this.closeModal(); + this.router.navigate(['/formulaireiotit'], { queryParams: { produitId: this.id ,titre:this.titre} }); + + } + getImageUrl(imagePath: string): string { + if (imagePath.startsWith('/assets')) { + return imagePath; + } + return this.apiBaseUrl + imagePath; +} + +} + diff --git a/src/app/chat-bot/chat-bot.component.html b/src/app/chat-bot/chat-bot.component.html new file mode 100644 index 000000000..a0b7771cf --- /dev/null +++ b/src/app/chat-bot/chat-bot.component.html @@ -0,0 +1,2 @@ +
+
\ No newline at end of file diff --git a/src/app/chat-bot/chat-bot.component.scss b/src/app/chat-bot/chat-bot.component.scss new file mode 100644 index 000000000..093e5e26b --- /dev/null +++ b/src/app/chat-bot/chat-bot.component.scss @@ -0,0 +1,36 @@ +.chatbot-fab { + position: fixed; + bottom: 30px; + right: 30px; + display: flex; + align-items: center; + gap: 10px; + z-index: 1000; +} + +.chatbot-label { + background-color: #007bff; + color: white; + padding: 8px 12px; + border-radius: 20px; + font-size: 14px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + animation: fadeIn 0.5s ease; +} + +.chatbot-button { + background-color:blue; + color: white; + border: none; + border-radius: 50%; + width: 55px; + height: 55px; + font-size: 20px; + cursor: pointer; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} diff --git a/src/app/chat-bot/chat-bot.component.spec.ts b/src/app/chat-bot/chat-bot.component.spec.ts new file mode 100644 index 000000000..2b9bd93d1 --- /dev/null +++ b/src/app/chat-bot/chat-bot.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChatBotComponent } from './chat-bot.component'; + +describe('ChatBotComponent', () => { + let component: ChatBotComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ChatBotComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ChatBotComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/chat-bot/chat-bot.component.ts b/src/app/chat-bot/chat-bot.component.ts new file mode 100644 index 000000000..6d9e88e39 --- /dev/null +++ b/src/app/chat-bot/chat-bot.component.ts @@ -0,0 +1,58 @@ +import { Component, OnInit ,Renderer2} from '@angular/core'; + +@Component({ + selector: 'app-chat-bot', + templateUrl: './chat-bot.component.html', + styleUrls: ['./chat-bot.component.scss'] +}) +export class ChatBotComponent implements OnInit { + showChat = false; + + constructor(private renderer: Renderer2) {} + + ngOnInit(): void { + const script = this.renderer.createElement('script'); + script.innerHTML = ` + (function(){ + if(!window.chatbase || window.chatbase("getState") !== "initialized") { + window.chatbase=(...arguments)=>{ + if(!window.chatbase.q){ + window.chatbase.q=[] + } + window.chatbase.q.push(arguments) + }; + window.chatbase=new Proxy(window.chatbase,{ + get(target,prop){ + if(prop==="q"){ return target.q } + return (...args)=>target(prop,...args) + } + }) + } + + const onLoad=function(){ + const script=document.createElement("script"); + script.src="https://www.chatbase.co/embed.min.js"; + script.id="f0d3oyMrl3n7digplpnOD"; + script.setAttribute("chatbotId", "f0d3oyMrl3n7digplpnOD"); + document.body.appendChild(script); + }; + + if(document.readyState==="complete"){ + onLoad() + }else{ + window.addEventListener("load",onLoad) + } + })(); + `; + this.renderer.appendChild(document.body, script); + } + + toggleChat(): void { + const iframe = document.querySelector('iframe[src*="chatbase"]') as HTMLElement; + if (iframe) { + this.showChat = !this.showChat; + iframe.style.display = this.showChat ? 'block' : 'none'; + } + } + +} diff --git a/src/app/clients-carousel/clients-carousel.component.html b/src/app/clients-carousel/clients-carousel.component.html new file mode 100644 index 000000000..15da3f632 --- /dev/null +++ b/src/app/clients-carousel/clients-carousel.component.html @@ -0,0 +1,23 @@ +
+
+

{{ 'CLIENTS.TITLE' | translate }}

+

+ {{ 'CLIENTS.DESCRIPTION' | translate }} +

+
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/src/app/clients-carousel/clients-carousel.component.scss b/src/app/clients-carousel/clients-carousel.component.scss new file mode 100644 index 000000000..03e4a4e34 --- /dev/null +++ b/src/app/clients-carousel/clients-carousel.component.scss @@ -0,0 +1,20 @@ +.carousel-container { + width: 100%; + margin: 0 auto; +} + +.carousel-content { + display: flex; + transition: transform 0.01s linear; /* Transition très courte pour fluidité */ + will-change: transform; /* Optimisation performance */ +} + +/* Masquer la scrollbar */ +.carousel-container::-webkit-scrollbar { + display: none; +} + +.carousel-container { + -ms-overflow-style: none; + scrollbar-width: none; +} \ No newline at end of file diff --git a/src/app/clients-carousel/clients-carousel.component.spec.ts b/src/app/clients-carousel/clients-carousel.component.spec.ts new file mode 100644 index 000000000..cb45a9bb0 --- /dev/null +++ b/src/app/clients-carousel/clients-carousel.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ClientsCarouselComponent } from './clients-carousel.component'; + +describe('ClientsCarouselComponent', () => { + let component: ClientsCarouselComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ClientsCarouselComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ClientsCarouselComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/clients-carousel/clients-carousel.component.ts b/src/app/clients-carousel/clients-carousel.component.ts new file mode 100644 index 000000000..d041117bb --- /dev/null +++ b/src/app/clients-carousel/clients-carousel.component.ts @@ -0,0 +1,58 @@ +import { Component, OnInit,OnDestroy } from '@angular/core'; + +@Component({ + selector: 'app-clients-carousel', + templateUrl: './clients-carousel.component.html', + styleUrls: ['./clients-carousel.component.scss'] +}) +export class ClientsCarouselComponent implements OnInit , OnDestroy { + clients = [ + { name: 'Client 1', logo: '/assets/img/logos/pgs.jpeg' }, + { name: 'Client 2', logo: '/assets/img/logos/pharmaderm.jpeg' }, + { name: 'Client 3', logo: '/assets/img/logos/sagemcom.jpeg' }, + { name: 'Client 4', logo: '/assets/img/logos/TT.jpeg' }, + { name: 'Client 5', logo: '/assets/img/logos/pharmaservice.jpeg' }, + { name: 'Client 6', logo: '/assets/img/logos/cftp.jpeg' }, + { name: 'Client 7', logo: '/assets/img/logos/cnam.jpeg' }, + { name: 'Client 8', logo: '/assets/img/logos/etap.jpeg' }, + { name: 'Client 9', logo: '/assets/img/logos/smtf.jpeg' }, + { name: 'Client 10', logo: '/assets/img/logos/TLS.jpeg' }, + + + ]; + + duplicatedClients = [...this.clients, ...this.clients]; // Pour l'effet infini + private animationFrameId: number; + private scrollSpeed = 1; // Ajustez la vitesse ici + private carouselElement: HTMLElement; + + ngOnInit(): void { + this.carouselElement = document.querySelector('.carousel-content'); + this.startAnimation(); + } + + ngOnDestroy(): void { + this.stopAnimation(); + } + + startAnimation(): void { + let position = 0; + const animate = () => { + position += this.scrollSpeed; + + // Réinitialiser la position quand on arrive à la moitié (grâce aux éléments dupliqués) + if (position >= this.carouselElement.scrollWidth / 2) { + position = 0; + } + + this.carouselElement.style.transform = `translateX(-${position}px)`; + this.animationFrameId = requestAnimationFrame(animate); + }; + + this.animationFrameId = requestAnimationFrame(animate); + } + + stopAnimation(): void { + cancelAnimationFrame(this.animationFrameId); + } +} \ No newline at end of file diff --git a/src/app/contact-us/contact-us.component.html b/src/app/contact-us/contact-us.component.html new file mode 100644 index 000000000..884776303 --- /dev/null +++ b/src/app/contact-us/contact-us.component.html @@ -0,0 +1,98 @@ + + + + +
+
+
+
+

{{ 'CONTACT.TITLE' | translate }}

+

{{ 'CONTACT.INTRO' | translate }}

+
+
+ +
+
+ + + + + +
+ {{ 'CONTACT.FORM.EMAIL_ERROR' | translate }} +
+ + + + +
+ +
+ {{ messageText }} +
+
+ + +
+
+
+ + diff --git a/src/app/contact-us/contact-us.component.scss b/src/app/contact-us/contact-us.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/contact-us/contact-us.component.spec.ts b/src/app/contact-us/contact-us.component.spec.ts new file mode 100644 index 000000000..ecc463a0d --- /dev/null +++ b/src/app/contact-us/contact-us.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContactUsComponent } from './contact-us.component'; + +describe('ContactUsComponent', () => { + let component: ContactUsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ContactUsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ContactUsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/contact-us/contact-us.component.ts b/src/app/contact-us/contact-us.component.ts new file mode 100644 index 000000000..004fbf8d6 --- /dev/null +++ b/src/app/contact-us/contact-us.component.ts @@ -0,0 +1,75 @@ +import { Component, OnInit } from '@angular/core'; +import emailjs from 'emailjs-com'; +import { EmailjsService } from 'emailJs/email.service'; +import Swal from 'sweetalert2'; + +declare var Email: any; +@Component({ + selector: 'app-contact-us', + templateUrl: './contact-us.component.html', + styleUrls: ['./contact-us.component.scss'] +}) +export class ContactUsComponent implements OnInit { + + constructor(private emailjsService: EmailjsService) { } + form = { + name: '', + email: '', + message: '' + }; + + ngOnInit(): void { + const script = document.createElement('script'); + script.src = 'https://smtpjs.com/v3/smtp.js'; + script.type = 'text/javascript'; + document.body.appendChild(script); + } + messageBox = document.getElementById('messageBox'); + + showMessage(text, isError = false) { + this.messageBox.textContent = text; + this.messageBox.style.color = isError ? 'red' : 'green'; + this.messageBox.style.display = 'block'; +} +emailInvalid = false; +messageText = ''; +messageError = false; + +isValidEmail(email: string): boolean { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); +} + + sendEmail() { + if (!this.isValidEmail(this.form.email)) { + Swal.fire({ + icon: 'error', + title: 'Invalid Email', + text: 'Please enter a valid email address.', + }); + return; + } + + this.emailjsService.sendContactEmail(this.form) + .then(() => { + Swal.fire({ + icon: 'success', + title: 'Message envoyé ✅', + html: ` +

+ An automatic confirmation email will be sent to you immediately.

+ ⚠️ If you do not receive it within a few minutes, it means you entered a non-existent or incorrect email address. + `, + confirmButtonColor: '#3085d6' + }); + this.form = { name: '', email: '', message: '' }; + }) + .catch((err) => { + Swal.fire({ + icon: 'error', + title: 'Error', + text: 'An error occurred while sending.', + }); + }); + } +} diff --git a/src/app/devis-detail/devis-detail.component.html b/src/app/devis-detail/devis-detail.component.html new file mode 100644 index 000000000..fcd5efd34 --- /dev/null +++ b/src/app/devis-detail/devis-detail.component.html @@ -0,0 +1,56 @@ +
+
+
+
+

📄 Devis {{ devis.produitDevis ? 'IOT' : 'IT' }}

+
+ +
+
+
+
👤 Informations personnelles
+

Nom : {{ devis.nom }}

+

Prénom : {{ devis.prenom }}

+

Email : {{ devis.email }}

+

Entreprise : {{ devis.entreprise }}

+
+ +
+
📋 Détails du devis
+

Message :
{{ devis.message }}

+

Quantité : {{ devis.quantite }}

+
+
+ +
+ +
+
+
🔧 Produit concerné
+

Titre : {{ devis.produitDevis.titre }}

+

Description : {{ devis.produitDevis.description }}

+

Catégorie : {{ devis.produitDevis.categorie }}

+
+
+
🖼️ Image
+ Produit +
+
+ +
+ + ⬅ Retour à la liste + + + + + +
+
+
+
+
diff --git a/src/app/devis-detail/devis-detail.component.scss b/src/app/devis-detail/devis-detail.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/devis-detail/devis-detail.component.spec.ts b/src/app/devis-detail/devis-detail.component.spec.ts new file mode 100644 index 000000000..373c4db3e --- /dev/null +++ b/src/app/devis-detail/devis-detail.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DevisDetailComponent } from './devis-detail.component'; + +describe('DevisDetailComponent', () => { + let component: DevisDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DevisDetailComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DevisDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/devis-detail/devis-detail.component.ts b/src/app/devis-detail/devis-detail.component.ts new file mode 100644 index 000000000..0a86f4b43 --- /dev/null +++ b/src/app/devis-detail/devis-detail.component.ts @@ -0,0 +1,123 @@ +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { DevisService } from 'app/Services/devis.service'; +import * as html2pdf from 'html2pdf.js'; + +@Component({ + selector: 'app-devis-detail', + templateUrl: './devis-detail.component.html', + styleUrls: ['./devis-detail.component.scss'] +}) +export class DevisDetailComponent implements OnInit { + @ViewChild('pdfContent') pdfContent!: ElementRef; + devis: any; + + constructor(private route: ActivatedRoute, private devisService: DevisService) {} + + ngOnInit(): void { + const id = this.route.snapshot.paramMap.get('id'); + if (id) { + this.devisService.getDevisById(+id).subscribe({ + next: (data) => { + this.devis = data; + }, + error: (err) => console.error('Erreur lors du chargement du devis', err) + }); + } + } + today = new Date(); +genererPdf() { + const logoPath = '/assets/img/logoTunav.png'; + let quantiteSection = ''; + let produitSection = ''; + + if (this.devis.produitDevis) { + quantiteSection = ` +

Quantité : ${this.devis.quantite}

+ `; + + produitSection = ` +
+

Produit concerné

+ + + + + + + + + + + + + + + + + +
TitreDescriptionCatégorieImage
${this.devis.produitDevis.titre}${this.devis.produitDevis.description}${this.devis.produitDevis.categorie} + Produit +
+
+ `; + } + + const htmlContent = ` +
+ +
+ Logo +
+

Tunav It Group

+

Adresse complète

+

Téléphone : +216 71807667

+

Email : Tunav@tunav.com

+
+
+ +

Devis N°: ${this.devis.id}

+ +
+

Informations Client

+

Nom : ${this.devis.nom} ${this.devis.prenom}

+

Entreprise : ${this.devis.entreprise}

+

Email : ${this.devis.email}

+
+ +
+

Détails du devis

+

Message :

+

${this.devis.message}

+ ${quantiteSection} +
+ + ${produitSection} + +
+
+

Date : ${this.today.toLocaleDateString()}

+
+
+

Signature

+

+

+
+
+ +
+ `; + + const options = { + margin: 0.5, + filename: `Devis_${this.devis.nom}_${this.devis.prenom}.pdf`, + image: { type: 'jpeg', quality: 0.98 }, + html2canvas: { scale: 2, dpi: 192, letterRendering: true }, + jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' } + }; + + html2pdf().set(options).from(htmlContent).save(); +} + + +} diff --git a/src/app/footer-home-page/footer-home-page.component.html b/src/app/footer-home-page/footer-home-page.component.html new file mode 100644 index 000000000..1e239d8b7 --- /dev/null +++ b/src/app/footer-home-page/footer-home-page.component.html @@ -0,0 +1,32 @@ + + + diff --git a/src/app/footer-home-page/footer-home-page.component.scss b/src/app/footer-home-page/footer-home-page.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/footer-home-page/footer-home-page.component.spec.ts b/src/app/footer-home-page/footer-home-page.component.spec.ts new file mode 100644 index 000000000..8eb44750a --- /dev/null +++ b/src/app/footer-home-page/footer-home-page.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FooterHomePageComponent } from './footer-home-page.component'; + +describe('FooterHomePageComponent', () => { + let component: FooterHomePageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FooterHomePageComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FooterHomePageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/footer-home-page/footer-home-page.component.ts b/src/app/footer-home-page/footer-home-page.component.ts new file mode 100644 index 000000000..df8b1b75f --- /dev/null +++ b/src/app/footer-home-page/footer-home-page.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-footer-home-page', + templateUrl: './footer-home-page.component.html', + styleUrls: ['./footer-home-page.component.scss'] +}) +export class FooterHomePageComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/app/form-franchise/form-franchise.component.css b/src/app/form-franchise/form-franchise.component.css new file mode 100644 index 000000000..857cd2de5 --- /dev/null +++ b/src/app/form-franchise/form-franchise.component.css @@ -0,0 +1,223 @@ +.body { + background-image: url('assets/img/infinite-loop-01.jpg'); + background-size: cover; + background-repeat: no-repeat; + background-position: center; +} + +.container { + display: flex; + flex-wrap: wrap; + gap: 40px; + padding: 2rem; + max-width: 1200px; + margin: auto; + color: white; +} + +.partie-informations { + margin-top: 50px; + flex-grow: 1; + flex-shrink: 1; + flex-basis: 40%; +} + +.partie-formulaire { + flex-grow: 1; + flex-shrink: 1; + flex-basis: 60%; +} + +h1 { + text-align: center; + color: #01b4ff; +} + +.subtitle { + text-align: center; + color: #ffffff; + margin-bottom: 1.5rem; +} + +.form-container { + background: white; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + color: rgba(0, 0, 0, 0.766); +} + +.form-container h2 { + font-size: 1.5rem; + margin-bottom: 0.8rem; + color: #01b4ff; + font-weight: 600; +} + +.form-container p { + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.form-container form { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.form-container label { + font-size: 0.9rem; +} + +.form-container input:not([type="radio"]), +.form-container textarea { + padding: 0.6rem; + border-radius: 6px; + border: 1px solid #ccc; + font-size: 0.9rem; +} + +textarea { + resize: vertical; + height: 80px; +} + +.form-container button { + margin-top: 1rem; + background-color: #01b4ff; + border: none; + color: white; + padding: 0.75rem; + font-size: 0.95rem; + font-weight: bold; + border-radius: 6px; + cursor: pointer; + transition: background 0.3s ease; +} + +.form-container button:hover { + background: linear-gradient(to right, #1e88e5, #1565c0); +} + +@media (max-width: 768px) { + .container { + flex-direction: column; + padding: 1rem; + } + + .form-container { + padding: 1.2rem; + } +} +input[type="radio"]{ + border: #01b4ff; + gap: 30px; +} + +input::placeholder, +textarea::placeholder { + color:black; +} +.iti { + display: flex; + border: 1px solid #ccc; + border-radius: 6px; + overflow: hidden; +} + +.iti__flag-container { + background-color: #f3f3f3; + border-right: 1px solid #ccc; + padding: 0 10px; + display: flex; + align-items: center; +} + +input.form-control { + border: none; + flex: 1; + padding: 10px; + font-size: 1rem; +} +@media (max-width: 992px) { + .container { + gap: 20px; + padding: 1.5rem; + } + + .partie-informations, + .partie-formulaire { + flex-basis: 100%; + } + + .form-container { + padding: 1.5rem; + } + + h1 { + font-size: 2rem; + } + + .subtitle { + font-size: 1rem; + } + + .form-container h2 { + font-size: 1.3rem; + } + + .form-container button { + padding: 0.6rem; + font-size: 0.9rem; + } +} + +/* Responsive mobile */ +@media (max-width: 576px) { + .container { + padding: 1rem; + } + + .partie-informations, + .partie-formulaire { + flex-basis: 100%; + } + + h1 { + font-size: 1.6rem; + } + + .subtitle { + font-size: 0.9rem; + } + + .form-container { + padding: 1rem; + } + + .form-container h2 { + font-size: 1.1rem; + } + + .form-container p { + font-size: 0.8rem; + } + + .form-container input:not([type="radio"]), + .form-container textarea { + font-size: 0.85rem; + } + + .form-container button { + font-size: 0.85rem; + padding: 0.6rem; + } + + .iti__flag-container { + padding: 0 6px; + } + + input.form-control { + font-size: 0.9rem; + } +} diff --git a/src/app/form-franchise/form-franchise.component.html b/src/app/form-franchise/form-franchise.component.html new file mode 100644 index 000000000..771b47075 --- /dev/null +++ b/src/app/form-franchise/form-franchise.component.html @@ -0,0 +1,80 @@ +
+ + +
+
+
+

{{ 'FRANCHISE.TITLE' | translate }}

+

+ {{ 'FRANCHISE.SUBTITLE' | translate }} +

+
+ +
+
+
+

{{ 'FRANCHISE.FORM.TITLE' | translate }}

+

{{ 'FRANCHISE.FORM.DESCRIPTION' | translate }}

+ + + + + + + + + + + + + + + + + +
+ + +
+ +
+ + +
+ + +
+ + +
+ +
+ + +
+ + + + + +
+
+
+
+


+ +
diff --git a/src/app/form-franchise/form-franchise.component.spec.ts b/src/app/form-franchise/form-franchise.component.spec.ts new file mode 100644 index 000000000..60243b7bd --- /dev/null +++ b/src/app/form-franchise/form-franchise.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FormFranchiseComponent } from './form-franchise.component'; + +describe('FormFranchiseComponent', () => { + let component: FormFranchiseComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FormFranchiseComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FormFranchiseComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/form-franchise/form-franchise.component.ts b/src/app/form-franchise/form-franchise.component.ts new file mode 100644 index 000000000..e4ecfd9c0 --- /dev/null +++ b/src/app/form-franchise/form-franchise.component.ts @@ -0,0 +1,145 @@ +import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { FranchiseService } from 'app/Services/franchise.service'; +import { CookieService } from 'ngx-cookie-service'; +import Swal from 'sweetalert2'; + +declare var intlTelInput: any; + +@Component({ + selector: 'app-form-franchise', + templateUrl: './form-franchise.component.html', + styleUrls: ['./form-franchise.component.css'] +}) +export class FormFranchiseComponent implements OnInit, AfterViewInit { + @ViewChild('phoneInput', { static: false }) phoneInputRef!: ElementRef; + formFranchise!: FormGroup; + iti: any; + storedUser = this.cookieService.get('userId'); + userId = this.storedUser ? Number(this.storedUser) : null; + constructor(private fb: FormBuilder,private franchiseService: FranchiseService,private router: Router,private cookieService: CookieService) {} + + ngOnInit(): void { + this.formFranchise = this.fb.group({ + nom: ['', Validators.required], + prenom: ['', Validators.required], + telephone: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + profession: [''], + experienceIT: ['', Validators.required], + precisionsExp: [''], + dirigeEntreprise: ['', Validators.required], + secteurDuree: [''], + motivation: ['', Validators.required], + }); + + } + + ngAfterViewInit(): void { + this.iti = intlTelInput(this.phoneInputRef.nativeElement, { + initialCountry: 'tn', + utilsScript:'https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js' + }); + } + +onSubmit(): void { + const localNumber = this.phoneInputRef.nativeElement.value.trim(); + const countryData = this.iti.getSelectedCountryData(); + const dialCode = countryData?.dialCode || ''; + + if (!localNumber || !dialCode) { + Swal.fire('Error', 'Phone number or country not selected.', 'error'); + return; + } + + const phoneRegex = /^[0-9]{6,15}$/; + + if (!phoneRegex.test(localNumber)) { + Swal.fire('Invalid number', 'The number must contain only digits (no letters or symbols).', 'error'); + return; + } + + const internationalNumber = `+${dialCode}${localNumber.replace(/^0+/, '')}`; + this.formFranchise.patchValue({ telephone: internationalNumber }); + + // 🔐 Validation conditionnelle + if (this.formFranchise.get('experienceIT')?.value === 'oui' && + !this.formFranchise.get('precisionsExp')?.value.trim()) { + Swal.fire('Required field', 'Please specify your experience in IT/IoT/GPS.', 'warning'); + return; + } + + if (this.formFranchise.get('dirigeEntreprise')?.value === 'oui' && + !this.formFranchise.get('secteurDuree')?.value.trim()) { + Swal.fire('Required field', 'Please specify the sector and duration of the company you managed.', 'warning'); + return; + } + + // 🔍 Validation générale + if (!this.formFranchise.valid) { + const controls = this.formFranchise.controls; + + if (controls['nom'].invalid) { + Swal.fire('Required field', 'Please enter your last name.', 'warning'); + return; + } + + if (controls['prenom'].invalid) { + Swal.fire('Required field', 'Please enter your first name.', 'warning'); + return; + } + + if (controls['email'].invalid) { + Swal.fire('Invalid email', 'Please enter a valid email address.', 'error'); + return; + } + + if (controls['experienceIT'].invalid) { + Swal.fire('Required field', 'Please indicate your experience.', 'warning'); + return; + } + + if (controls['dirigeEntreprise'].invalid) { + Swal.fire('Required field', 'Please specify if you have managed a company.', 'warning'); + return; + } + + if (controls['motivation'].invalid) { + Swal.fire('Required field', 'Please state your motivations.', 'warning'); + return; + } + + this.formFranchise.markAllAsTouched(); + return; + } + + const formData = this.formFranchise.value; + +const payload = { + nom: formData.nom, + prenom: formData.prenom, + email: formData.email, + telephone: internationalNumber, + professionActuelle: formData.profession || '', + experienceIotGps: formData.experienceIT === 'oui' ? formData.precisionsExp : '', + entrepriseDirige: formData.dirigeEntreprise === 'oui' ? formData.secteurDuree : '', + motivation: formData.motivation, + userId: this.userId +}; + +this.franchiseService.envoyerDemandeFranchise(payload).subscribe({ + next: (res) => { + Swal.fire('Success', 'Your request has been sent successfully.', 'success'); + this.formFranchise.reset(); + this.router.navigate(['/home']); + + }, + error: (err) => { + Swal.fire('Error', 'An error occurred while sending', 'error'); + } +}); + +} + +} diff --git a/src/app/form-products/form-products.component.html b/src/app/form-products/form-products.component.html new file mode 100644 index 000000000..cebdc3999 --- /dev/null +++ b/src/app/form-products/form-products.component.html @@ -0,0 +1,69 @@ +
+

{{ isEditMode ? 'Modifier un Produit' : 'Ajouter un Produit ' + (type === 'iot' ? 'IoT' : 'GPS') }}

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + {{ carac }} + + +
+ +
+ +
+ +
+

Glissez-déposez une image ici ou cliquez

+ Image + +
+ +
+ + + + +
+
diff --git a/src/app/form-products/form-products.component.scss b/src/app/form-products/form-products.component.scss new file mode 100644 index 000000000..cba7ff152 --- /dev/null +++ b/src/app/form-products/form-products.component.scss @@ -0,0 +1,103 @@ +.drag-drop-area { + border: 2px dashed #aaa; + padding: 20px; + text-align: center; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.drag-drop-area:hover { + background-color: #f1f1f1; +} +/* Responsive tablette */ +@media (max-width: 992px) { + .container { + gap: 20px; + padding: 1.5rem; + } + + .partie-informations, + .partie-formulaire { + flex-basis: 100%; + } + + .form-container { + padding: 1.5rem; + } + + h1 { + font-size: 2rem; + } + + .subtitle { + font-size: 1rem; + } + + .form-container h2 { + font-size: 1.3rem; + } + + .form-container button { + padding: 0.6rem; + font-size: 0.9rem; + } + + .drag-drop-area { + padding: 15px; + } +} + +/* Responsive mobile */ +@media (max-width: 576px) { + .container { + padding: 1rem; + } + + .partie-informations, + .partie-formulaire { + flex-basis: 100%; + } + + h1 { + font-size: 1.6rem; + } + + .subtitle { + font-size: 0.9rem; + } + + .form-container { + padding: 1rem; + } + + .form-container h2 { + font-size: 1.1rem; + } + + .form-container p { + font-size: 0.8rem; + } + + .form-container input:not([type="radio"]), + .form-container textarea { + font-size: 0.85rem; + } + + .form-container button { + font-size: 0.85rem; + padding: 0.6rem; + } + + .iti__flag-container { + padding: 0 6px; + } + + input.form-control { + font-size: 0.9rem; + } + + .drag-drop-area { + padding: 12px; + font-size: 0.9rem; + } +} diff --git a/src/app/form-products/form-products.component.spec.ts b/src/app/form-products/form-products.component.spec.ts new file mode 100644 index 000000000..250095eaa --- /dev/null +++ b/src/app/form-products/form-products.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FormProductsComponent } from './form-products.component'; + +describe('FormProductsComponent', () => { + let component: FormProductsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FormProductsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FormProductsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/form-products/form-products.component.ts b/src/app/form-products/form-products.component.ts new file mode 100644 index 000000000..ec50ed762 --- /dev/null +++ b/src/app/form-products/form-products.component.ts @@ -0,0 +1,224 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { + ProduitAvecDevisService, + ProduitAvecDevis, + ProduitAvecDevisCreateRequest, + ProduitAvecDevisUpdateRequest +} from '../Services/ProduitAvecDevisService'; +import { + ProduitSansDevisService, + ProduitSansDevis, + ProduitSansDevisCreateRequest, + ProduitSansDevisUpdateRequest +} from '../Services/ProduitSansDevisService'; +import { environment } from 'environments/environment'; +import { CookieService } from 'ngx-cookie-service'; + +@Component({ + selector: 'app-form-products', + templateUrl: './form-products.component.html', + styleUrls: ['./form-products.component.scss'] +}) +export class FormProductsComponent implements OnInit { + + productForm!: FormGroup; + isEditMode = false; + productId: number | null = null; + previewUrl: string | ArrayBuffer | null = null; + selectedFile: File | null = null; + apiBaseUrl = environment.baseUrl + + type: 'iot' | 'gps' = 'gps'; + caracteristiques: string[] = []; + newCaracteristique: string = ''; + + constructor( + private fb: FormBuilder, + private route: ActivatedRoute, + private router: Router, + private produitSansDevisService: ProduitSansDevisService, + private produitAvecDevisService: ProduitAvecDevisService, + private cookieService: CookieService + ) {} + storedUser = this.cookieService.get('userId');; + userId = this.storedUser ? Number(this.storedUser) : null; + ngOnInit(): void { + const routeType = this.route.snapshot.queryParamMap.get('type'); + if (routeType === 'iot' || routeType === 'gps') { + this.type = routeType; + } + + this.productForm = this.fb.group({ + nom: [''], + description: [''], + categorie: [this.type], + prix: this.type === 'gps' ? [''] : null, + image: [null], + userId: [''] + }); + + this.productId = Number(this.route.snapshot.paramMap.get('id')); + if (this.productId) { + this.isEditMode = true; + + if (this.type === 'gps') { + this.produitSansDevisService.getById(this.productId).subscribe({ + next: (product: ProduitSansDevis) => { + this.productForm.patchValue({ + nom: product.titre, + description: product.description, + prix: product.prix, + categorie: product.categorie + }); + this.caracteristiques = product.caracteristiques.map(c => c.texte); + this.previewUrl = product.imagePath.startsWith('/assets') + ? product.imagePath + : this.apiBaseUrl + product.imagePath; + + }, + error: err => console.error('Erreur chargement produit GPS :', err) + }); + } else { + this.produitAvecDevisService.getById(this.productId).subscribe({ + next: (product: ProduitAvecDevis) => { + this.productForm.patchValue({ + nom: product.titre, + description: product.description, + categorie: product.categorie + }); + this.caracteristiques = product.caracteristiques.map(c => c.texte); + this.previewUrl = product.imagePath.startsWith('/assets') + ? product.imagePath + : this.apiBaseUrl + product.imagePath; + + }, + error: err => console.error('Erreur chargement produit IoT :', err) + }); + } + } + } + + onSubmit(): void { + const formValue = this.productForm.value; + + if (this.type === 'iot') { + if (this.isEditMode && this.productId) { + const updateRequest: ProduitAvecDevisUpdateRequest = { + titre: formValue.nom, + description: formValue.description, + categorie: 'iot', + caracteristiques: this.caracteristiques, + newImage: this.selectedFile || undefined + }; + + this.produitAvecDevisService.update(this.productId, updateRequest).subscribe({ + next: () => this.router.navigate(['/listProducts']), + error: err => console.error('Erreur update IoT :', err) + }); + + } else { + if (!this.selectedFile) { + console.error('Aucune image sélectionnée.'); + return; + } + + const createRequest: ProduitAvecDevisCreateRequest = { + titre: formValue.nom, + description: formValue.description, + categorie: 'iot', + caracteristiques: this.caracteristiques, + image: this.selectedFile, + }; + + this.produitAvecDevisService.create(createRequest).subscribe({ + next: () => this.router.navigate(['/listProducts']), + error: err => console.error('Erreur ajout IoT :', err) + }); + } + } else { + if (this.isEditMode && this.productId) { + const updateRequest: ProduitSansDevisUpdateRequest = { + titre: formValue.nom, + description: formValue.description, + categorie: 'gps', + prix: formValue.prix, + caracteristiques: this.caracteristiques, + userId:this.userId, + newImage: this.selectedFile || undefined + }; + + this.produitSansDevisService.update(this.productId, updateRequest).subscribe({ + next: () => this.router.navigate(['/listProducts']), + error: err => console.error('Erreur update GPS :', err) + }); + + } else { + if (!this.selectedFile) { + console.error('Aucune image sélectionnée.'); + return; + } + + const createRequest: ProduitSansDevisCreateRequest = { + titre: formValue.nom, + description: formValue.description, + categorie: 'gps', + prix: formValue.prix, + caracteristiques: this.caracteristiques, + image: this.selectedFile, + userId: this.userId + }; + + this.produitSansDevisService.create(createRequest).subscribe({ + next: () => this.router.navigate(['/listProducts']), + error: err => console.error('Erreur ajout GPS :', err) + }); + } + } + } + + onDragOver(event: DragEvent) { + event.preventDefault(); + } + + onDragLeave(event: DragEvent) { + event.preventDefault(); + } + + onFileDrop(event: DragEvent) { + event.preventDefault(); + if (event.dataTransfer?.files.length) { + this.handleFile(event.dataTransfer.files[0]); + } + } + + onFileSelect(event: any) { + if (event.target.files.length) { + this.handleFile(event.target.files[0]); + } + } + + handleFile(file: File) { + this.selectedFile = file; + this.productForm.patchValue({ image: file }); + const reader = new FileReader(); + reader.onload = () => { + this.previewUrl = reader.result; + }; + reader.readAsDataURL(file); + } + + addCaracteristique(event: KeyboardEvent) { + event.preventDefault(); + const valeur = this.newCaracteristique.trim(); + if (valeur && !this.caracteristiques.includes(valeur)) { + this.caracteristiques.push(valeur); + this.newCaracteristique = ''; + } + } + + removeCaracteristique(index: number) { + this.caracteristiques.splice(index, 1); + } +} diff --git a/src/app/formblog/formblog.component.html b/src/app/formblog/formblog.component.html new file mode 100644 index 000000000..11a6e2414 --- /dev/null +++ b/src/app/formblog/formblog.component.html @@ -0,0 +1,53 @@ +
+
+

{{ isEditMode ? 'Modifier le blog' : 'Ajouter un blog' }}

+
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ +
+ + {{ tag}} + + +
+ +
+ + +
+ +
+

Glissez-déposez une image ici ou sélectionnez un fichier.

+ Aperçu de l'image + +
+
+ + + +
+
diff --git a/src/app/formblog/formblog.component.scss b/src/app/formblog/formblog.component.scss new file mode 100644 index 000000000..f5272583a --- /dev/null +++ b/src/app/formblog/formblog.component.scss @@ -0,0 +1,20 @@ +.image-upload-container { + border: 2px dashed #3377AA; + padding: 20px; + text-align: center; + border-radius: 10px; + background-color: #f8f9fa; + transition: background-color 0.2s ease-in-out; + + &.drag-over { + background-color: #e0f3ff; + } + + .image-preview { + max-width: 200px; + max-height: 200px; + margin-top: 10px; + border-radius: 10px; + object-fit: cover; + } +} diff --git a/src/app/formblog/formblog.component.spec.ts b/src/app/formblog/formblog.component.spec.ts new file mode 100644 index 000000000..edfef247f --- /dev/null +++ b/src/app/formblog/formblog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FormblogComponent } from './formblog.component'; + +describe('FormblogComponent', () => { + let component: FormblogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FormblogComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FormblogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/formblog/formblog.component.ts b/src/app/formblog/formblog.component.ts new file mode 100644 index 000000000..2762a2051 --- /dev/null +++ b/src/app/formblog/formblog.component.ts @@ -0,0 +1,147 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BlogCreateRequest, BlogService, BlogUpdateRequest } from 'app/Services/BlogService'; +import { Blog } from 'app/blogslist/blogslist.component'; +import { environment } from 'environments/environment'; +import { CookieService } from 'ngx-cookie-service'; + +@Component({ + selector: 'app-formblog', + templateUrl: './formblog.component.html', + styleUrls: ['./formblog.component.scss'] +}) +export class FormblogComponent implements OnInit { + + blog: Blog = { + id: 0, + titre: '', + contenu: '', + imagePath: '', + tags: [], + likes: 0 + }; + storedUser = this.cookieService.get('userId'); + userId = this.storedUser ? Number(this.storedUser) : null; + selectedFile?: File; + newTag: string = ''; + isEditMode = false; + isDragging = false; + apiBaseUrl = environment.baseUrl; + + constructor( + private route: ActivatedRoute, + private router: Router, + private blogService: BlogService,private cookieService: CookieService + ) {} + + ngOnInit() { + + const blogId = this.route.snapshot.paramMap.get('id'); + if (blogId) { + this.isEditMode = true; + this.blogService.getBlogById(+blogId).subscribe({ + next: data => { + this.blog = {...data, + tags: data.tags.map((t: any) => t.nom || t) + + } + }, + error: err => console.error('Erreur chargement blog') + }); + } + } + + onFileSelected(event: any) { + this.selectedFile = event.target.files[0]; + if (this.selectedFile && this.selectedFile.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onload = e => this.blog.imagePath = (e.target as FileReader).result as string; + reader.readAsDataURL(this.selectedFile); + } + } + + onDragOver(event: DragEvent) { + event.preventDefault(); + this.isDragging = true; + } + + onDragLeave(event: DragEvent) { + event.preventDefault(); + this.isDragging = false; + } + + onDrop(event: DragEvent) { + event.preventDefault(); + this.isDragging = false; + const file = event.dataTransfer?.files[0]; + if (file && file.type.startsWith('image/')) { + this.selectedFile = file; + const reader = new FileReader(); + reader.onload = () => this.blog.imagePath = reader.result as string; + reader.readAsDataURL(file); + } + } + + addTag(event: KeyboardEvent) { + event.preventDefault(); + const tag = this.newTag.trim(); + const tagExists = this.blog.tags.includes(tag); + if (tag && !tagExists) { + this.blog.tags.push(tag); + this.newTag = ''; + } + } + + removeTag(index: number) { + this.blog.tags.splice(index, 1); + } + + onSubmit() { + const CreateRequest: BlogCreateRequest = { + titre: this.blog.titre, + contenu: this.blog.contenu, + userId: this.userId, + tags: this.blog.tags, + image: this.selectedFile || undefined + }; + const UpdateRequest: BlogUpdateRequest = { + titre: this.blog.titre, + contenu: this.blog.contenu, + tags: this.blog.tags, + newImage: this.selectedFile || undefined + }; + + if (this.isEditMode) { + this.blogService.updateBlog(this.blog.id, UpdateRequest).subscribe({ + next: () => this.router.navigate(['/listblogs']), + error: (err) => { + console.error('Erreur update blog:', err); + if (err.error instanceof Blob) { + const reader = new FileReader(); + reader.onload = () => console.error('Erreur backend JSON:', reader.result); + reader.readAsText(err.error); + } else { + console.error('Erreur directe:', err.error); + } + } + }); + } else { + this.blogService.createBlog(CreateRequest).subscribe({ + next: () => this.router.navigate(['/listblogs']), + error: (err) => { + console.error('Erreur création blog:', err); + if (err.error instanceof Blob) { + const reader = new FileReader(); + reader.onload = () => console.error('Erreur backend:', reader.result); + reader.readAsText(err.error); + } + } + }); + } + } + + getImageUrl(imagePath: string): string { + return this.apiBaseUrl + imagePath; + } + +} diff --git a/src/app/formulaire-iot-it/formulaire-iot-it.component.css b/src/app/formulaire-iot-it/formulaire-iot-it.component.css new file mode 100644 index 000000000..af5f1f8b3 --- /dev/null +++ b/src/app/formulaire-iot-it/formulaire-iot-it.component.css @@ -0,0 +1,242 @@ +.body{ + background-image: url('assets/img/infinite-loop-01.jpg'); +} +.container { + /* margin-top: 20px; */ + display: flex; + /* padding: 2rem; */ + min-width: 100%; + background: transparent; + font-family: 'Segoe UI', sans-serif; + box-shadow: 0 5px 12px rgba(0, 0, 0, 0.1); + gap: 30px; + justify-content: center; + align-items: center; + height: 900px; +} + +.partie-informations { + width: auto; +} + +.partie-formulaire { + width: 65%; + margin-right: 30px; +} +h1 { + text-align: center; + color: #01b4ff; +} + +.subtitle { + text-align: center; + color: #ffffff; + margin-bottom: 1.5rem; +} +.tabs { + display: flex; + justify-content: center; + margin-bottom: 2rem; + gap: 1rem; +} +.tabs .tab { + padding: 0.6rem 1.5rem; + border-radius: 30px; + border: none; + background: #e0e0e0; + cursor: pointer; + transition: all 0.3s ease; + font-weight: 500; +} + +.tabs .tab.active { + background-color:#01b4ff; + color: white; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); +} + +.form-container { + margin-top: 20px; + margin-bottom: 20px; + background: white; + padding: 1rem; + border-radius: 12px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + height: 750px; + max-height: 100%; + margin: auto 0; + display: flex; + flex-direction: column; + justify-content: space-between; + overflow: hidden; +} + +.form-container .description { + font-size: 0.85rem; + margin-bottom: 0.6rem; +} + +.form-container h2 { + font-size: 1.3rem; + margin-bottom: 0.3rem; +} + +.form-container strong { + font-size: 0.9rem; +} + +.form-container form { + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: space-evenly; +} + +.form-container form label { + font-size: 0.85rem; + margin-bottom: 0.2rem; +} + +.form-container form input, +.form-container form textarea { + margin: 0.3rem 0; + padding: 0.6rem; + border-radius: 6px; + border: 1px solid #ccc; + font-size: 0.85rem; +} + +.form-container form textarea { + resize: vertical; + height: 70px; +} + +.form-container form button { + margin-top: 0.6rem; + background-color: #01b4ff; + border: none; + color: white; + padding: 0.7rem; + font-size: 0.85rem; + font-weight: bold; + border-radius: 6px; + cursor: pointer; + transition: background 0.3s; +} + +.form-container form button:hover { + background: linear-gradient(to right, #1e88e5, #1565c0); +} +/* Responsive tablette */ +@media (max-width: 992px) { + .container { + flex-direction: column; + height: auto; + padding: 1.5rem; + gap: 20px; + } + + .partie-informations, + .partie-formulaire { + width: 100%; + margin-right: 0; + } + + .form-container { + height: auto; + padding: 1.2rem; + } + + h1 { + font-size: 2rem; + } + + .subtitle { + font-size: 1rem; + } + + .form-container h2 { + font-size: 1.2rem; + } + + .form-container .description, + .form-container strong, + .form-container form label { + font-size: 0.8rem; + } + + .form-container form input, + .form-container form textarea { + font-size: 0.85rem; + } + + .form-container form button { + font-size: 0.85rem; + padding: 0.6rem; + } + + .tabs .tab { + padding: 0.5rem 1rem; + font-size: 0.9rem; + } +} + +/* Responsive mobile */ +@media (max-width: 576px) { + .container { + flex-direction: column; + height: auto; + padding: 1rem; + gap: 15px; + } + + .partie-informations, + .partie-formulaire { + width: 100%; + margin-right: 0; + } + + h1 { + font-size: 1.6rem; + } + + .subtitle { + font-size: 0.9rem; + } + + .form-container { + padding: 1rem; + height: auto; + } + + .form-container h2 { + font-size: 1.1rem; + } + + .form-container .description, + .form-container strong, + .form-container form label { + font-size: 0.75rem; + } + + .form-container form input, + .form-container form textarea { + font-size: 0.8rem; + } + + .form-container form button { + font-size: 0.8rem; + padding: 0.5rem; + } + + .tabs { + flex-direction: column; + gap: 0.5rem; + } + + .tabs .tab { + width: 100%; + padding: 0.5rem; + font-size: 0.85rem; + } +} diff --git a/src/app/formulaire-iot-it/formulaire-iot-it.component.html b/src/app/formulaire-iot-it/formulaire-iot-it.component.html new file mode 100644 index 000000000..f8200e743 --- /dev/null +++ b/src/app/formulaire-iot-it/formulaire-iot-it.component.html @@ -0,0 +1,92 @@ +
+ +
+
+
+

{{ 'SOLUTIONS.TITLE' | translate }}

+

{{ 'SOLUTIONS.SUBTITLE' | translate }}

+ +
+ + +
+
+ +
+ +
+

🌐{{ 'SOLUTIONS.IOT.TITLE' | translate }} + - {{ produitTitre }} +

+

+ +
+ {{ 'SOLUTIONS.IOT.DESCRIPTION' | translate }} +

+ +
+ + + + + + + + + + + + + + + + + +
+
+ + +
+

💻{{ 'SOLUTIONS.IT.TITLE' | translate }}

+

+ +
+ {{ 'SOLUTIONS.IT.DESCRIPTION' | translate }} +

+ +
+ + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+ +
diff --git a/src/app/formulaire-iot-it/formulaire-iot-it.component.spec.ts b/src/app/formulaire-iot-it/formulaire-iot-it.component.spec.ts new file mode 100644 index 000000000..1fb027ef9 --- /dev/null +++ b/src/app/formulaire-iot-it/formulaire-iot-it.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FormulaireIotItComponent } from './formulaire-iot-it.component'; + +describe('FormulaireIotItComponent', () => { + let component: FormulaireIotItComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FormulaireIotItComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FormulaireIotItComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/formulaire-iot-it/formulaire-iot-it.component.ts b/src/app/formulaire-iot-it/formulaire-iot-it.component.ts new file mode 100644 index 000000000..2e0033324 --- /dev/null +++ b/src/app/formulaire-iot-it/formulaire-iot-it.component.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { DevisService } from 'app/Services/devis.service'; +import { CookieService } from 'ngx-cookie-service'; +import Swal from 'sweetalert2'; + +@Component({ + selector: 'app-formulaire-iot-it', + templateUrl: './formulaire-iot-it.component.html', + styleUrls: ['./formulaire-iot-it.component.css'] +}) +export class FormulaireIotItComponent implements OnInit { + selectedTab: 'iot' | 'it' = 'iot'; + formIot!: FormGroup; + formIt!: FormGroup; + produitAvecDevisId: number = 0; + produitTitre: string = ''; + constructor(private fb: FormBuilder,private cookieService: CookieService,private devisService: DevisService, + private route: ActivatedRoute + ) {} + + ngOnInit(): void { + this.route.queryParams.subscribe(params => { + this.produitAvecDevisId = +params['produitId'] || 0; + this.produitTitre = params['titre'] || ''; + }); + this.formIot = this.fb.group({ + nom: ['', Validators.required], + prenom: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + entreprise: [''], + message: ['', Validators.required], + quantite: [1] + }); + + this.formIt = this.fb.group({ + nom: ['', Validators.required], + prenom: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + entreprise: [''], + message: ['', Validators.required], + quantite: [0] + }); + } + + selectTab(tab: 'iot' | 'it') { + this.selectedTab = tab; + } + + submitForm(type: 'iot' | 'it') { + const form = type === 'iot' ? this.formIot : this.formIt; + + if (form.invalid) return; + const userId = this.cookieService.get('userId');; + + const data = { + ...form.value, + userId, + produitAvecDevisId: type === 'iot' ? this.produitAvecDevisId : null, + etat:0 + }; + this.devisService.addDevis(data).subscribe({ + next: () => { + Swal.fire({ + icon: 'success', + title: 'Quote request sent!', + text: 'Your request has been successfully submitted.', + confirmButtonText: 'OK' + }); + form.reset(); + }, + error: err => { + Swal.fire({ + icon: 'error', + title: 'An error occurred', + text: 'There was a problem submitting your request. Please try again later.', + confirmButtonText: 'Close' + }); + } + }); +} + + +} diff --git a/src/app/franchise-detail/franchise-detail.component.html b/src/app/franchise-detail/franchise-detail.component.html new file mode 100644 index 000000000..70106a79e --- /dev/null +++ b/src/app/franchise-detail/franchise-detail.component.html @@ -0,0 +1,52 @@ +
+
+
+
+

📋 Détails de la demande de franchise

+
+ +
+
+
+
👤 Informations personnelles
+

Nom : {{ franchise.nom }}

+

Prénom : {{ franchise.prenom }}

+

Email : {{ franchise.email }}

+

Téléphone : {{ franchise.telephone }}

+
+
+
💼 Expérience & Motivation
+

Profession actuelle : {{ franchise.professionActuelle }}

+

Expérience IoT/GPS : + + {{ franchise.experienceIotGps || 'Non' }} + +

+

A dirigé une entreprise : + + {{ franchise.entrepriseDirige || 'Non' }} + +

+

Motivation :
+ {{ franchise.motivation }} +

+
+
+ +
+ + ⬅ Retour à la liste + + + + + +
+
+
+
+
diff --git a/src/app/franchise-detail/franchise-detail.component.scss b/src/app/franchise-detail/franchise-detail.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/franchise-detail/franchise-detail.component.spec.ts b/src/app/franchise-detail/franchise-detail.component.spec.ts new file mode 100644 index 000000000..ef9f93885 --- /dev/null +++ b/src/app/franchise-detail/franchise-detail.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FranchiseDetailComponent } from './franchise-detail.component'; + +describe('FranchiseDetailComponent', () => { + let component: FranchiseDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FranchiseDetailComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FranchiseDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/franchise-detail/franchise-detail.component.ts b/src/app/franchise-detail/franchise-detail.component.ts new file mode 100644 index 000000000..f0097e6d8 --- /dev/null +++ b/src/app/franchise-detail/franchise-detail.component.ts @@ -0,0 +1,101 @@ +import { Component, ElementRef, OnInit,ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Franchise } from 'app/liste-franchises/liste-franchises.component'; +import { FranchiseService } from 'app/Services/franchise.service'; +import * as html2pdf from 'html2pdf.js'; +import Swal from 'sweetalert2'; +import emailjs, { EmailJSResponseStatus } from 'emailjs-com'; +import { EmailjsService } from 'emailJs/email.service'; +@Component({ + selector: 'app-franchise-detail', + templateUrl: './franchise-detail.component.html', + styleUrls: ['./franchise-detail.component.scss'] +}) +export class FranchiseDetailComponent implements OnInit { + + franchise?: Franchise; + @ViewChild('pdfContent') pdfContent!: ElementRef; + constructor( + private route: ActivatedRoute, + private franchiseService: FranchiseService, + private emailjsService: EmailjsService + ) {} + + ngOnInit(): void { + const id = Number(this.route.snapshot.paramMap.get('id')); + this.franchiseService.getFranchiseById(id).subscribe(data => { + this.franchise = data; + }); + } + + envoyerMailConfirmation() { + Swal.fire({ + title: 'Confirmer l\'envoi ?', + text: 'Voulez-vous vraiment envoyer un mail de confirmation à ' + this.franchise.nom + ' ?', + icon: 'question', + showCancelButton: true, + confirmButtonText: 'Oui, envoyer', + cancelButtonText: 'Annuler' + }).then((result) => { + if (result.isConfirmed) { + + const params = { + nom: this.franchise.nom, + prenom: this.franchise.prenom, + to_email: this.franchise.email, + }; + + this.emailjsService.sendFranchiseConfirmation(params) + .then(() => { + Swal.fire('Succès', 'L\'email a été envoyé avec succès.', 'success'); + }) + .catch(() => { + Swal.fire('Erreur', 'Une erreur est survenue lors de l\'envoi.', 'error'); + }); + + } else { + Swal.fire('Annulé', 'L\'email n\'a pas été envoyé.', 'info'); + } + }); + } + + + genererPdf() { + const logoUrl = '/assets/img/logoTunav.png'; + const franchise = this.franchise; + + const content = ` +
+
+ Logo +

Fiche Franchise - TUNAV

+
+
+

Nom : ${franchise?.nom || ''}

+

Prénom : ${franchise?.prenom || ''}

+

Email : ${franchise?.email || ''}

+

Téléphone : ${franchise?.telephone || ''}

+

Profession actuelle : ${franchise?.professionActuelle || 'Non spécifiée'}

+

Expérience IT/IoT/GPS : ${franchise?.experienceIotGps || 'Non spécifiée'}

+

Entreprise dirigée : ${franchise?.entrepriseDirige || 'Non spécifiée'}

+

Motivation :
${franchise?.motivation || ''}

+
+
+ `; + + const opt = { + margin: 0.5, + filename: `Fiche_Franchise_${franchise?.nom}_${franchise?.prenom}.pdf`, + image: { type: 'jpeg', quality: 0.98 }, + html2canvas: { scale: 2 }, + jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' } + }; + + const element = document.createElement('div'); + element.innerHTML = content; + +html2pdf().set(opt).from(element).save(); +} + + +} diff --git a/src/app/header-home-page/header-home-page.component.html b/src/app/header-home-page/header-home-page.component.html new file mode 100644 index 000000000..47ccecb3d --- /dev/null +++ b/src/app/header-home-page/header-home-page.component.html @@ -0,0 +1,48 @@ + diff --git a/src/app/header-home-page/header-home-page.component.scss b/src/app/header-home-page/header-home-page.component.scss new file mode 100644 index 000000000..d8adb5742 --- /dev/null +++ b/src/app/header-home-page/header-home-page.component.scss @@ -0,0 +1,87 @@ +.language-switcher { + position: relative; + z-index: 2; + + .current-language { + font-size: 1.5rem; + background: none; + border: none; + cursor: pointer; + } + + .language-menu { + display: block; // Par défaut block, mais masqué par *ngIf + position: absolute; + top: 2rem; + left: 0; + list-style: none; + background: #ffffff; + border: 1px solid #ddd; + padding: 0.5rem 0; + margin: 0; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + + li { + padding: 0.5rem 1rem; + cursor: pointer; + + &:hover { + background-color: #f5f5f5; + } + + &.active { + font-weight: bold; + } + } + } +} + +/* Responsive tablette */ +/* Tablette en mode portrait (768px) à paysage (1024px) */ +@media (min-width: 577px) and (max-width: 1024px) { + .language-switcher .current-language { + font-size: 1.3rem; + } + + .language-switcher .language-menu { + top: 1.8rem; + min-width: 100px; + } + + .language-switcher .language-menu li { + padding: 0.4rem 0.8rem; + font-size: 0.95rem; + } +} +@media (min-width: 600px) and (max-width: 900px) { + .language-switcher .current-language { + font-size: 1.3rem; + } + + .language-switcher .language-menu { + top: 1.8rem; + min-width: 100px; + } + + .language-switcher .language-menu li { + padding: 0.4rem 0.8rem; + font-size: 0.95rem; + } +} + +/* Responsive mobile */ +@media (max-width: 576px) { + .language-switcher .current-language { + font-size: 1.1rem; + } + + .language-switcher .language-menu { + top: 1.6rem; + min-width: 90px; + } + + .language-switcher .language-menu li { + padding: 0.35rem 0.7rem; + font-size: 0.9rem; + } +} diff --git a/src/app/header-home-page/header-home-page.component.spec.ts b/src/app/header-home-page/header-home-page.component.spec.ts new file mode 100644 index 000000000..67de4addc --- /dev/null +++ b/src/app/header-home-page/header-home-page.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeaderHomePageComponent } from './header-home-page.component'; + +describe('HeaderHomePageComponent', () => { + let component: HeaderHomePageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HeaderHomePageComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HeaderHomePageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/header-home-page/header-home-page.component.ts b/src/app/header-home-page/header-home-page.component.ts new file mode 100644 index 000000000..6a0c823f1 --- /dev/null +++ b/src/app/header-home-page/header-home-page.component.ts @@ -0,0 +1,132 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { AuthService } from 'app/Services/auth.service'; +import { LanguageService } from 'app/Services/language.service'; +import Swal from 'sweetalert2'; + +@Component({ + selector: 'app-header-home-page', + templateUrl: './header-home-page.component.html', + styleUrls: ['./header-home-page.component.scss'] +}) +export class HeaderHomePageComponent implements OnInit { + isDropdownOpen = false; + isNavbarCollapsed: boolean = true; + isLoggedIn = false; + pendingSection: string | null = null; + languages = [ + { code: 'en', name: 'English', flag: '/assets/img/flags/united-kingdom-flag.png' }, + { code: 'fr', name: 'Francais', flag: '/assets/img/flags/france-flag.png' }, + { code: 'ar', name: 'العربية', flag: '/assets/img/flags/tn-flag.gif' } +]; + ngOnInit(): void { + this.checkLoginStatus(); + window.addEventListener('scroll', this.onScroll); + + } + currentLanguage = 'en'; + constructor(private router: Router,private translate: TranslateService, + private authService: AuthService, + private languageService: LanguageService,private cdr: ChangeDetectorRef) { + this.currentLanguage = this.languageService.getCurrentLanguage(); + this.router.events.subscribe(event => { + if (event instanceof NavigationEnd && this.pendingSection) { + setTimeout(() => { + const el = document.getElementById(this.pendingSection!); + if (el) { + el.scrollIntoView({ behavior: 'smooth' }); + } + this.pendingSection = null; + }, 100); + } + }); + } + + + switchLanguage(languageCode: string): void { + this.currentLanguage = languageCode; + this.translate.use(languageCode); + localStorage.setItem('language', languageCode); + this.isDropdownOpen = false; + } + changeDropDown() + { + this.isDropdownOpen=!this.isDropdownOpen; + this.cdr.detectChanges(); + } + getFlag(languageCode: string): string { + const language = this.languages.find((lang) => lang.code === languageCode); + return language ? language.flag : ''; + } + navigateToSection(sectionId: string, event: Event) { + event.preventDefault(); + + const currentUrl = this.router.url.split('#')[0]; + if (currentUrl !== '/' && currentUrl !== '/home') { + this.pendingSection = sectionId; + this.router.navigate(['/']); + } else { + const el = document.getElementById(sectionId); + if (el) { + el.scrollIntoView({ behavior: 'smooth' }); + } + } + } + +checkLoginStatus(): void { + const token = this.authService.getToken(); + this.isLoggedIn = !!token; + } +onScroll = () => { + const scrollY = window.scrollY; + const logoBlanc = document.getElementById('logoBlanc'); + const logoCouleur = document.getElementById('logoCouleur'); + + if (scrollY <= 50) { + logoBlanc!.style.display = 'block'; + logoCouleur!.style.display = 'none'; + } else { + logoBlanc!.style.display = 'none'; + logoCouleur!.style.display = 'block'; + } +}; + + + Login(event: Event){ + event.preventDefault(); + this.router.navigate(['auth']); + } + About(event: Event){ + event.preventDefault(); + this.router.navigate(['about']); + } + Blogs(event: Event){ + event.preventDefault(); + this.router.navigate(['blogs']); + } + Franchise(event: Event){ + event.preventDefault(); + this.router.navigate(['formulairefranchise']); + } + logout() + { + this.authService.logout(); + Swal.fire({ + title: this.translate.instant('ALERT.LOGOUT_TITLE'), + text: this.translate.instant('ALERT.LOGOUT_SUCCESS'), + icon: 'success', + showConfirmButton: true, + confirmButtonText: this.translate.instant('ALERT.OK'), + background: '#f0f8ff', + color: '#333', + confirmButtonColor: '#3085d6', + backdrop: ` + rgba(0,0,123,0.4) + left top + no-repeat + ` + }); + this.checkLoginStatus() + } +} diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 53fc2fd3a..f74227511 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -1,33 +1,41 @@
-
- - -
-
- - + +
+ +
+ + +
+ + +
+ +
diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 9aba51e04..6de635b73 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; -import { LocationStrategy, PlatformLocation, Location } from '@angular/common'; -import { LegendItem, ChartType } from '../lbd/lbd-chart/lbd-chart.component'; -import * as Chartist from 'chartist'; +import { ChartType, LegendItem } from '../lbd/lbd-chart/lbd-chart.component'; +import { UserStatistics, UserStatisticsService } from 'app/Services/user-statistics.service'; +import { DevisService } from 'app/Services/devis.service'; @Component({ selector: 'app-home', @@ -9,104 +9,130 @@ import * as Chartist from 'chartist'; styleUrls: ['./home.component.css'] }) export class HomeComponent implements OnInit { - public emailChartType: ChartType; - public emailChartData: any; - public emailChartLegendItems: LegendItem[]; + // Propriétés pour le graphique des statistiques utilisateur + public hoursChartType: ChartType; + public hoursChartData: any; + public hoursChartOptions: any; + public hoursChartResponsive: any[]; + public hoursChartLegendItems: LegendItem[]; - public hoursChartType: ChartType; - public hoursChartData: any; - public hoursChartOptions: any; - public hoursChartResponsive: any[]; - public hoursChartLegendItems: LegendItem[]; + // Propriétés pour le graphique des devis + public devisChartType: ChartType; + public devisChartData: any; + public devisChartOptions: any; + public devisChartResponsive: any[]; + public devisChartLegendItems: LegendItem[]; - public activityChartType: ChartType; - public activityChartData: any; - public activityChartOptions: any; - public activityChartResponsive: any[]; - public activityChartLegendItems: LegendItem[]; - constructor() { } + public footerText: string = ''; + + constructor( + private statsService: UserStatisticsService, + private devisService: DevisService + ) {} ngOnInit() { - this.emailChartType = ChartType.Pie; - this.emailChartData = { - labels: ['62%', '32%', '6%'], - series: [62, 32, 6] - }; - this.emailChartLegendItems = [ - { title: 'Open', imageClass: 'fa fa-circle text-info' }, - { title: 'Bounce', imageClass: 'fa fa-circle text-danger' }, - { title: 'Unsubscribe', imageClass: 'fa fa-circle text-warning' } - ]; + this.hoursChartType = ChartType.Bar; + this.hoursChartResponsive = []; + this.hoursChartLegendItems = [ + { title: 'Visitors', imageClass: 'fa fa-eye text-info' }, + { title: 'Sign-ups', imageClass: 'fa fa-user-plus text-success' }, + { title: 'Logins', imageClass: 'fa fa-sign-in-alt text-warning' } + ]; - this.hoursChartType = ChartType.Line; - this.hoursChartData = { - labels: ['9:00AM', '12:00AM', '3:00PM', '6:00PM', '9:00PM', '12:00PM', '3:00AM', '6:00AM'], - series: [ - [287, 385, 490, 492, 554, 586, 698, 695, 752, 788, 846, 944], - [67, 152, 143, 240, 287, 335, 435, 437, 539, 542, 544, 647], - [23, 113, 67, 108, 190, 239, 307, 308, 439, 410, 410, 509] - ] - }; - this.hoursChartOptions = { - low: 0, - high: 800, - showArea: true, - height: '245px', - axisX: { - showGrid: false, - }, - lineSmooth: Chartist.Interpolation.simple({ - divisor: 3 - }), - showLine: false, - showPoint: false, - }; - this.hoursChartResponsive = [ - ['screen and (max-width: 640px)', { - axisX: { - labelInterpolationFnc: function (value) { - return value[0]; - } - } - }] - ]; - this.hoursChartLegendItems = [ - { title: 'Open', imageClass: 'fa fa-circle text-info' }, - { title: 'Click', imageClass: 'fa fa-circle text-danger' }, - { title: 'Click Second Time', imageClass: 'fa fa-circle text-warning' } - ]; + this.devisChartType = ChartType.Pie; + this.devisChartResponsive = []; + this.devisChartLegendItems = [ + { title: 'Devis IT', imageClass: 'fa fa-circle text-info' }, + { title: 'Devis IOT', imageClass: 'fa fa-circle text-danger' } + ]; + + this.loadStats(); + } + + loadStats() { + this.statsService.getStats().subscribe({ + next: (stats: UserStatistics) => { + const maxValue = Math.max(stats.visitors, stats.signUps, stats.logins); + this.hoursChartOptions = { + high: maxValue, + low: 0, + axisY: { + onlyInteger: true + }, + seriesBarDistance: 20 + }; + this.hoursChartData = { + labels: ['Visitors', 'Sign-ups', 'Logins'], + series: [ + [stats.visitors, stats.signUps, stats.logins] + ] + }; + + this.footerText = this.getTimeSinceLastUpdate(stats.lastUpdated); + }, + error: (error) => console.error('Error fetching user stats') + }); - this.activityChartType = ChartType.Bar; - this.activityChartData = { - labels: ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - series: [ - [542, 443, 320, 780, 553, 453, 326, 434, 568, 610, 756, 895], - [412, 243, 280, 580, 453, 353, 300, 364, 368, 410, 636, 695] - ] - }; - this.activityChartOptions = { - seriesBarDistance: 10, - axisX: { - showGrid: false - }, - height: '245px' - }; - this.activityChartResponsive = [ - ['screen and (max-width: 640px)', { - seriesBarDistance: 5, - axisX: { - labelInterpolationFnc: function (value) { - return value[0]; + this.devisService.getNumberDevisWithoutProduit().subscribe({ + next: (withoutProduitData) => { + this.devisService.getNumberDevisWithProduit().subscribe({ + next: (withProduitData) => { + const devisWithoutProduit = withoutProduitData.devisWithoutProduit; + const devisWithProduit = withProduitData.devisWithProduit; + const totalDevis = devisWithProduit + devisWithoutProduit; + + const withProduitPercentage = totalDevis > 0 ? ((devisWithProduit / totalDevis) * 100).toFixed(1) : 0; + const withoutProduitPercentage = totalDevis > 0 ? ((devisWithoutProduit / totalDevis) * 100).toFixed(1) : 0; + /*this.devisChartData = { + labels: ['Devis IT', 'Devis IOT'], + datasets: [{ + data: [devisWithoutProduit, devisWithProduit], + backgroundColor: ['#36A2EB', '#FF6384'], + hoverBackgroundColor: ['#36A2EB', '#FF6384'] + }] + };*/ + + this.devisChartData = { + labels: [`${withoutProduitPercentage}%`, `${withProduitPercentage}%`], + series: [devisWithoutProduit, devisWithProduit] + }; + + this.devisChartOptions = { + donut: true, + donutWidth: 50, + showLabel: true, + labelInterpolationFnc: function (value: string, index: number) { + return value; } - } - }] - ]; - this.activityChartLegendItems = [ - { title: 'Tesla Model S', imageClass: 'fa fa-circle text-info' }, - { title: 'BMW 5 Series', imageClass: 'fa fa-circle text-danger' } - ]; + }; + }, + error: (error) => console.error('Error fetching devis with produit') + }); + }, + error: (error) => console.error('Error fetching devis without produit') + }); + } - } + getTimeSinceLastUpdate(lastUpdated: string): string { + const lastUpdateDate = new Date(lastUpdated); + const now = new Date(); + const diffInMs = now.getTime() - lastUpdateDate.getTime(); + const diffInMinutes = Math.floor(diffInMs / (1000 * 60)); + const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); + const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); + const diffInWeeks = Math.floor(diffInMs / (1000 * 60 * 60 * 24 * 7)); -} + if (diffInMinutes < 1) { + return 'Updated just now'; + } else if (diffInMinutes <= 59) { + return `Updated ${diffInMinutes} minute${diffInMinutes === 1 ? '' : 's'} ago`; + } else if (diffInHours <= 23) { + return `Updated ${diffInHours} hour${diffInHours === 1 ? '' : 's'} ago`; + } else if (diffInDays <= 6) { + return `Updated ${diffInDays} day${diffInDays === 1 ? '' : 's'} ago`; + } else { + return `Updated ${diffInWeeks} week${diffInWeeks === 1 ? '' : 's'} ago`; + } + } +} \ No newline at end of file diff --git a/src/app/iotcarousel/iotcarousel.component.html b/src/app/iotcarousel/iotcarousel.component.html new file mode 100644 index 000000000..42191aa88 --- /dev/null +++ b/src/app/iotcarousel/iotcarousel.component.html @@ -0,0 +1,26 @@ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/src/app/iotcarousel/iotcarousel.component.scss b/src/app/iotcarousel/iotcarousel.component.scss new file mode 100644 index 000000000..b4592278a --- /dev/null +++ b/src/app/iotcarousel/iotcarousel.component.scss @@ -0,0 +1,89 @@ +$poussin: url("http://images.metmuseum.org/CRDImages/ep/original/46_160.jpg"); +$picasso: url("http://uploads6.wikiart.org/images/pablo-picasso/the-abduction-of-sabines-1962-1.jpg"); +$rubens: url("https://upload.wikimedia.org/wikipedia/commons/7/72/Peter_Paul_Rubens_(taller)_-_Rapto_de_las_Sabinas.jpg"); + +html, body { + margin: 0; + height: 100%; + background: #F4F1E9; +} + +.arrow { + font-size: 2em; + color: #363f85; + position: absolute; + top: 50%; + left: 50%; + transition: 0.2s; + + &.left-arrow { + transform: translate(-11em, -50%); + } + + &.right-arrow { + transform: translate(10em, -50%); + } + + &:hover { + color: #B3B2AD; + } +} + +.slider { + position: relative; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 600px; + height: 200px; +} + +.slide { + float: left; + position: relative; + width: 33.3333%; + height: 100%; + overflow-x: hidden; + border-radius: 50%; + box-shadow: 0px 0px 15px rgba(0,0,0,0.5); + + &.slide-center { + z-index: 1; + box-shadow: 0px 0px 15px rgba(0,0,0,0.75); + transform: scale(1.3); + } +} + +.slide-holder { + width: 300%; + height: 100%; + position: relative; + top: 0; + transform: translateX(-33.3333%); + display: flex; +} + +.slide-bg { + width: 33.3333%; + height: 100%; + background-size: cover; + background-position: center center; + background-repeat: no-repeat; + display: inline-block; +} + +#slide-left { + .bg-previous { background-image: $rubens; } + .bg-current { background-image: $poussin; } + .bg-next { background-image: $picasso; } +} +#slide-center { + .bg-previous { background-image: $poussin; } + .bg-current { background-image: $picasso; } + .bg-next { background-image: $rubens; } +} +#slide-right { + .bg-previous { background-image: $picasso; } + .bg-current { background-image: $rubens; } + .bg-next { background-image: $poussin; } +} diff --git a/src/app/iotcarousel/iotcarousel.component.spec.ts b/src/app/iotcarousel/iotcarousel.component.spec.ts new file mode 100644 index 000000000..cd2555c1e --- /dev/null +++ b/src/app/iotcarousel/iotcarousel.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IOTCarouselComponent } from './iotcarousel.component'; + +describe('IOTCarouselComponent', () => { + let component: IOTCarouselComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ IOTCarouselComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(IOTCarouselComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/iotcarousel/iotcarousel.component.ts b/src/app/iotcarousel/iotcarousel.component.ts new file mode 100644 index 000000000..9d542fe75 --- /dev/null +++ b/src/app/iotcarousel/iotcarousel.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-iotcarousel', + templateUrl: './iotcarousel.component.html', + styleUrls: ['./iotcarousel.component.scss'] +}) +export class IOTCarouselComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/app/layouts/admin-layout/admin-layout.component.ts b/src/app/layouts/admin-layout/admin-layout.component.ts index 34c9814e0..e2ef2a894 100644 --- a/src/app/layouts/admin-layout/admin-layout.component.ts +++ b/src/app/layouts/admin-layout/admin-layout.component.ts @@ -17,11 +17,9 @@ export class AdminLayoutComponent implements OnInit { constructor( public location: Location, private router: Router) {} ngOnInit() { - console.log(this.router) const isWindows = navigator.platform.indexOf('Win') > -1 ? true : false; if (isWindows) { - // if we are on windows OS we activate the perfectScrollbar function document.getElementsByTagName('body')[0].classList.add('perfect-scrollbar-on'); } else { diff --git a/src/app/layouts/admin-layout/admin-layout.routing.ts b/src/app/layouts/admin-layout/admin-layout.routing.ts index e09417d82..54555076b 100644 --- a/src/app/layouts/admin-layout/admin-layout.routing.ts +++ b/src/app/layouts/admin-layout/admin-layout.routing.ts @@ -5,8 +5,6 @@ import { UserComponent } from '../../user/user.component'; import { TablesComponent } from '../../tables/tables.component'; import { TypographyComponent } from '../../typography/typography.component'; import { IconsComponent } from '../../icons/icons.component'; -import { MapsComponent } from '../../maps/maps.component'; -import { NotificationsComponent } from '../../notifications/notifications.component'; import { UpgradeComponent } from '../../upgrade/upgrade.component'; export const AdminLayoutRoutes: Routes = [ @@ -15,7 +13,5 @@ export const AdminLayoutRoutes: Routes = [ { path: 'table', component: TablesComponent }, { path: 'typography', component: TypographyComponent }, { path: 'icons', component: IconsComponent }, - { path: 'maps', component: MapsComponent }, - { path: 'notifications', component: NotificationsComponent }, { path: 'upgrade', component: UpgradeComponent }, ]; diff --git a/src/app/lbd/lbd-chart/lbd-chart.component.ts b/src/app/lbd/lbd-chart/lbd-chart.component.ts index 059800328..57a00f5ac 100644 --- a/src/app/lbd/lbd-chart/lbd-chart.component.ts +++ b/src/app/lbd/lbd-chart/lbd-chart.component.ts @@ -1,4 +1,11 @@ -import {Component, Input, OnInit, AfterViewInit, ChangeDetectionStrategy} from '@angular/core'; +import { + Component, + Input, + OnInit, + ChangeDetectionStrategy, + SimpleChanges, + OnChanges +} from '@angular/core'; import * as Chartist from 'chartist'; export interface LegendItem { @@ -9,7 +16,8 @@ export interface LegendItem { export enum ChartType { Pie, Line, - Bar + Bar, + Doughnut } @Component({ @@ -17,54 +25,44 @@ export enum ChartType { templateUrl: './lbd-chart.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class LbdChartComponent implements OnInit, AfterViewInit { +export class LbdChartComponent implements OnInit, OnChanges { static currentId = 1; - @Input() - public title: string; - - @Input() - public subtitle: string; - - @Input() - public chartClass: string; - - @Input() - public chartType: ChartType; - - @Input() - public chartData: any; - - @Input() - public chartOptions: any; - - @Input() - public chartResponsive: any[]; - - @Input() - public footerIconClass: string; - - @Input() - public footerText: string; - - @Input() - public legendItems: LegendItem[]; - - @Input() - public withHr: boolean; + @Input() public title: string; + @Input() public subtitle: string; + @Input() public chartClass: string; + @Input() public chartType: ChartType; + @Input() public chartData: any; + @Input() public chartOptions: any; + @Input() public chartResponsive: any[]; + @Input() public footerIconClass: string; + @Input() public footerText: string; + @Input() public legendItems: LegendItem[]; + @Input() public withHr: boolean; public chartId: string; - constructor() { - } + constructor() {} - public ngOnInit(): void { + ngOnInit(): void { this.chartId = `lbd-chart-${LbdChartComponent.currentId++}`; } - public ngAfterViewInit(): void { + ngOnChanges(changes: SimpleChanges): void { + if (changes['chartData'] && this.chartData && this.chartId) { + this.renderChart(); + } + } + private renderChart(): void { switch (this.chartType) { + case ChartType.Doughnut: + new Chartist.Pie(`#${this.chartId}`, this.chartData, { + ...this.chartOptions, + donut: true, + donutWidth: 40 + }, this.chartResponsive); + break; case ChartType.Pie: new Chartist.Pie(`#${this.chartId}`, this.chartData, this.chartOptions, this.chartResponsive); break; diff --git a/src/app/list-devis/list-devis.component.html b/src/app/list-devis/list-devis.component.html new file mode 100644 index 000000000..4a8663d41 --- /dev/null +++ b/src/app/list-devis/list-devis.component.html @@ -0,0 +1,46 @@ +
+
+

📄 Liste des Devis

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
NomPrénomEmailEntrepriseÉtatAction
{{ devis.nom }}{{ devis.prenom }}{{ devis.email }}{{ devis.entreprise }} +
+ +
+
+ + 🔍 Détails + +
+
+
diff --git a/src/app/list-devis/list-devis.component.scss b/src/app/list-devis/list-devis.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/list-devis/list-devis.component.spec.ts b/src/app/list-devis/list-devis.component.spec.ts new file mode 100644 index 000000000..87e27c16b --- /dev/null +++ b/src/app/list-devis/list-devis.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ListDevisComponent } from './list-devis.component'; + +describe('ListDevisComponent', () => { + let component: ListDevisComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ListDevisComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ListDevisComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/list-devis/list-devis.component.ts b/src/app/list-devis/list-devis.component.ts new file mode 100644 index 000000000..110caeaf3 --- /dev/null +++ b/src/app/list-devis/list-devis.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit } from '@angular/core'; +import { DevisService } from 'app/Services/devis.service'; +import Swal from 'sweetalert2'; + +@Component({ + selector: 'app-list-devis', + templateUrl: './list-devis.component.html', + styleUrls: ['./list-devis.component.scss'] +}) +export class ListDevisComponent implements OnInit { + etatsDisponibles: string[] = ['EnAttente', 'EnCours', 'Validé','Annulé']; + + devisList: any[] = []; + + constructor(private devisService: DevisService) {} + + ngOnInit(): void { + this.getAllDevis(); + } + getAllDevis() { + this.devisService.getAllDevis().subscribe({ + next: (data) => { + this.devisList = data; + }, + error: (error) => { + console.error('Erreur lors du chargement des devis', error); + } + }); + } + onEtatSelected(nouvelEtat: string, devis: any) { + if (nouvelEtat === devis.etat) return; // Pas de changement + + Swal.fire({ + title: 'Confirmation', + text: `Voulez-vous vraiment changer l'état du devis en '${nouvelEtat}' ?`, + icon: 'question', + showCancelButton: true, + confirmButtonText: 'Oui', + cancelButtonText: 'Non' + }).then(result => { + if (result.isConfirmed) { + const nouvelEtatIndex = this.etatsDisponibles.indexOf(nouvelEtat); + + this.devisService.updateEtatDevis(devis.id, nouvelEtatIndex.toString()).subscribe({ + next: (updatedDevis) => { + devis.etat = updatedDevis.etat; + Swal.fire('Succès', "L'état a été mis à jour.", 'success'); + }, + error: (err) => { + Swal.fire('Erreur', "Impossible de mettre à jour l'état.", 'error'); + } + }); + } else { + + devis.etat = devis.etat; + } + }); + } +} diff --git a/src/app/liste-franchises/liste-franchises.component.html b/src/app/liste-franchises/liste-franchises.component.html new file mode 100644 index 000000000..6d483950f --- /dev/null +++ b/src/app/liste-franchises/liste-franchises.component.html @@ -0,0 +1,37 @@ +
+
+

📋 Liste des demandes de franchises

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
NomPrénomEmailTéléphoneProfessionExp. IT/IoT/GPSDéjà dirigé ?Action
{{ f.nom }}{{ f.prenom }}{{ f.email }}{{ f.telephone }}{{ f.professionActuelle }}{{ f.experienceIotGps ? 'Oui' : 'Non' }}{{ f.entrepriseDirige ? 'Oui' : 'Non' }} + + 🔍 Détails + +
+
+
diff --git a/src/app/liste-franchises/liste-franchises.component.scss b/src/app/liste-franchises/liste-franchises.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/liste-franchises/liste-franchises.component.spec.ts b/src/app/liste-franchises/liste-franchises.component.spec.ts new file mode 100644 index 000000000..ee9bc848f --- /dev/null +++ b/src/app/liste-franchises/liste-franchises.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ListeFranchisesComponent } from './liste-franchises.component'; + +describe('ListeFranchisesComponent', () => { + let component: ListeFranchisesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ListeFranchisesComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ListeFranchisesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/liste-franchises/liste-franchises.component.ts b/src/app/liste-franchises/liste-franchises.component.ts new file mode 100644 index 000000000..33a03f665 --- /dev/null +++ b/src/app/liste-franchises/liste-franchises.component.ts @@ -0,0 +1,38 @@ +import { Component, OnInit } from '@angular/core'; +import { FranchiseService } from 'app/Services/franchise.service'; +export interface Franchise { + id: number; + nom: string; + prenom: string; + email: string; + telephone: string; + professionActuelle: string; + experienceIotGps: string; + entrepriseDirige: string; + motivation: string; +} + +@Component({ + selector: 'app-liste-franchises', + templateUrl: './liste-franchises.component.html', + styleUrls: ['./liste-franchises.component.scss'] +}) + +export class ListeFranchisesComponent implements OnInit { + + franchises: Franchise[] = []; + + constructor(private franchiseService: FranchiseService) {} + + ngOnInit(): void { + this.loadFranchises(); + } + + loadFranchises(): void { + this.franchiseService.getFranchises().subscribe({ + next: (data) => this.franchises = data, + error: (err) => console.error('Erreur lors du chargement des franchises', err) + }); + } + +} diff --git a/src/app/navbar/navbar.component.css b/src/app/navbar/navbar.component.css new file mode 100644 index 000000000..250ad7968 --- /dev/null +++ b/src/app/navbar/navbar.component.css @@ -0,0 +1,63 @@ + nav { + position: fixed; + width: 100%; + top: 0; + left: 0; + transition: background-color 0.3s ease, transform 0.5s ease; + z-index: 2; + height: 80px; + display: flex; + align-items: center; +} + +.navbar-container { + width: 100%; + max-width: 1300px; + margin: 0 auto; + padding: 0 30px; + display: flex; + justify-content: space-between; + /* align-items: center; */ +} + +.logo img { + height: 60px; +} + +.nav-links { + list-style: none; + display: flex; + gap: 25px; + justify-content: center; + align-items: center; + text-align: center; + +} + +.nav-links li a { + text-decoration: none; + color: white; + font-weight: 500; + text-align: center; + cursor: pointer; + +} + +.navbar-transparent { + background-color: transparent; +} + +.navbar-dark { + background-color: white; +} +.navbar-dark .nav-links a{ + color: #38B; +} +.nav-links a { + transition: color 0.3s ease; +} + +.navbar-hidden { + transform: translateY(-100%); + transition: transform 0.4s ease-in-out; +} \ No newline at end of file diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html new file mode 100644 index 000000000..7245f54c5 --- /dev/null +++ b/src/app/navbar/navbar.component.html @@ -0,0 +1,20 @@ + diff --git a/src/app/navbar/navbar.component.spec.ts b/src/app/navbar/navbar.component.spec.ts new file mode 100644 index 000000000..505cc2ffb --- /dev/null +++ b/src/app/navbar/navbar.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NavbarComponent } from './navbar.component'; + +describe('NavbarComponent', () => { + let component: NavbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NavbarComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(NavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts new file mode 100644 index 000000000..0e40215e8 --- /dev/null +++ b/src/app/navbar/navbar.component.ts @@ -0,0 +1,42 @@ +import { Component, HostListener, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.css'] +}) +export class NavbarComponent implements OnInit { + isScrolled = false; + + constructor(private router: Router) {} + + ngOnInit(): void { + this.onScroll(); + } + + @HostListener('window:scroll', []) + onScroll(): void { + const scrollY = window.scrollY || document.documentElement.scrollTop; + this.isScrolled = scrollY > 10; + } + + navigateToSection(sectionId: string): void { + const element = document.getElementById(sectionId); + if (element) { + element.scrollIntoView({ behavior: 'smooth' }); + } else { + this.router.navigate(['/home']).then(() => { + setTimeout(() => { + const el = document.getElementById(sectionId); + if (el) el.scrollIntoView({ behavior: 'smooth' }); + }, 50); + }); + } + } + + login(event: Event): void { + event.preventDefault(); + this.router.navigate(['/auth']); + } +} diff --git a/src/app/petit-cadre/petit-cadre.component.css b/src/app/petit-cadre/petit-cadre.component.css new file mode 100644 index 000000000..7fa7c1271 --- /dev/null +++ b/src/app/petit-cadre/petit-cadre.component.css @@ -0,0 +1,21 @@ +button { + border: 2px solid #01b4ff; + background-color: transparent; + color: #01b4ff; + font-weight: 600; + padding: 10px 25px; + border-radius: 25px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s ease, color 0.3s ease; +} + +button.active { + background-color: #01b4ff; + color: white; +} + +button:hover { + background-color: #01b4ff; + color: white; +} diff --git a/src/app/petit-cadre/petit-cadre.component.html b/src/app/petit-cadre/petit-cadre.component.html new file mode 100644 index 000000000..8f89ba4cd --- /dev/null +++ b/src/app/petit-cadre/petit-cadre.component.html @@ -0,0 +1,5 @@ + diff --git a/src/app/petit-cadre/petit-cadre.component.spec.ts b/src/app/petit-cadre/petit-cadre.component.spec.ts new file mode 100644 index 000000000..e272db61b --- /dev/null +++ b/src/app/petit-cadre/petit-cadre.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PetitCadreComponent } from './petit-cadre.component'; + +describe('PetitCadreComponent', () => { + let component: PetitCadreComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PetitCadreComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PetitCadreComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/petit-cadre/petit-cadre.component.ts b/src/app/petit-cadre/petit-cadre.component.ts new file mode 100644 index 000000000..2d79dbe42 --- /dev/null +++ b/src/app/petit-cadre/petit-cadre.component.ts @@ -0,0 +1,17 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'app-petit-cadre', + templateUrl: './petit-cadre.component.html', + styleUrls: ['./petit-cadre.component.css'] +}) +export class PetitCadreComponent { + @Input() label: string = ''; + @Input() active: boolean = false; + @Output() clicked = new EventEmitter(); + + onClick() { + this.clicked.emit(); + } +} + diff --git a/src/app/products/products.component.css b/src/app/products/products.component.css new file mode 100644 index 000000000..f0f4b15bf --- /dev/null +++ b/src/app/products/products.component.css @@ -0,0 +1,175 @@ +.nav{ + width: 100%; + margin-bottom: 50px; +} +.products-grid { + /* width: 85%; jai rajoute hethy pour le truc de 3 containers */ + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 20px; + padding: 20px; + + /* margin-left: 100px; jai rajoute hethy pour le truc de 3 containers */ +} +.carousel-et-nav{ + background-image: url('assets/img/infinite-loop-03.jpg'); +} + +.body { + margin-bottom: 100px; + padding: 0; + background-color: #ffffff; + background-repeat: no-repeat; + background-size: cover; + background-position:center center; + height: 100%; +} +.filter-buttons { + display: flex; + gap: 15px; + justify-content: center; + margin-top: 20px; + margin-bottom: 40px; +} +.description-it { + max-width: 800px; + margin: 0 auto 40px auto; + padding: 20px; + text-align: center; + font-size: 1.1rem; + color: #333; + background-color: #f4f9ff; + border-radius: 12px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} + +.btn-formulaire-it { + display: inline-block; + margin-top: 20px; + padding: 12px 24px; + background: linear-gradient(to right, #42a5f5, #1e88e5); + color: white; + text-decoration: none; + font-weight: 600; + border-radius: 30px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transition: background 0.3s ease; +} + +.btn-formulaire-it:hover { + transform: translateY(-5px); + transition: transform 0.25s ease-in-out; + background: linear-gradient(to right, #1e88e5, #1565c0); +} + +.anchor-offset { + scroll-margin-top: 100px; +} +@media (max-width: 1200px) { + .products-grid { + padding: 10px; + gap: 16px; + } + + .description-it { + max-width: 90%; + font-size: 1rem; + } + + .btn-formulaire-it { + padding: 10px 20px; + font-size: 0.95rem; + } +} + +@media (max-width: 992px) { + .carousel { + width: 90%; + margin: 0 auto; + } + + .titre { + text-align: center; + font-size: 1.7rem; /* Titre plus petit */ + } + + .filter-buttons { + gap: 10px; + flex-wrap: nowrap; + justify-content: center; + } + + app-petit-cadre { + transform: scale(0.9); + } + + .products-grid { + padding: 10px; + gap: 15px; + } +} + +@media (max-width: 768px) { + .carousel { + width: 100%; + } + + .titre { + font-size: 1.4rem; + } + + .filter-buttons { + gap: 8px; + } + + app-petit-cadre { + transform: scale(0.85); + } + + .description-it { + font-size: 0.95rem; + padding: 15px; + } + + .btn-formulaire-it { + padding: 10px 18px; + font-size: 0.9rem; + } +} + +@media (max-width: 576px) { + .carousel { + width: 100%; + } + + .titre { + font-size: 1.2rem; + } + + .filter-buttons { + flex-wrap: wrap; + gap: 6px; + } + + app-petit-cadre { + transform: scale(0.8); + } + + .products-grid { + gap: 12px; + padding: 10px; + } + + .description-it { + font-size: 0.9rem; + } + + .btn-formulaire-it { + padding: 8px 16px; + font-size: 0.85rem; + } +} + + + diff --git a/src/app/products/products.component.html b/src/app/products/products.component.html new file mode 100644 index 000000000..b06cb1ac6 --- /dev/null +++ b/src/app/products/products.component.html @@ -0,0 +1,73 @@ + +
+
+ + +
+ +
+ +
+ + +
+ +
+
+

{{ 'SOLUTIONS_PRODUCTS.IT.TITLE' | translate }}

+

{{ 'SOLUTIONS_PRODUCTS.IT.DESCRIPTION' | translate }}

+ + {{ 'SOLUTIONS_PRODUCTS.IT.LINK' | translate }} + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ + + diff --git a/src/app/products/products.component.spec.ts b/src/app/products/products.component.spec.ts new file mode 100644 index 000000000..623e5feff --- /dev/null +++ b/src/app/products/products.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductsComponent } from './products.component'; + +describe('ProductsComponent', () => { + let component: ProductsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ProductsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ProductsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/products/products.component.ts b/src/app/products/products.component.ts new file mode 100644 index 000000000..e33cee494 --- /dev/null +++ b/src/app/products/products.component.ts @@ -0,0 +1,112 @@ +import { Component, HostListener, OnInit, AfterViewChecked, NgZone } from '@angular/core'; +import { ProduitAvecDevisService, ProduitAvecDevis } from '../Services/ProduitAvecDevisService'; +import { ProduitSansDevisService, ProduitSansDevis } from '../Services/ProduitSansDevisService'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'app-products', + templateUrl: './products.component.html', + styleUrls: ['./products.component.css'] +}) +export class ProductsComponent implements OnInit, AfterViewChecked { + isScrolled = false; + + // @HostListener('window:scroll', []) + // onWindowScroll() { + // const offset = window.pageYOffset || document.documentElement.scrollTop; + + allProduitsAvecDevis: ProduitAvecDevis[] = []; + allProduitsNodevis: ProduitSansDevis[] = []; + + produits_nodevis: ProduitSansDevis[] = []; + produits_avecdevis: ProduitAvecDevis[] = []; + + petitsCadres = [ + { label: 'Tous', active: true }, + { label: 'GPS Trackers', active: false }, + { label: 'Solutions IoT', active: false }, + { label: 'Solutions IT', active: false } + ]; + + fragmentToScroll: string | null = null; + hasScrolled = false; + + constructor( + private produitAvecDevisService: ProduitAvecDevisService, + private produitSansDevisService: ProduitSansDevisService, + private route: ActivatedRoute, + private zone: NgZone + ) {} + + ngOnInit(): void { + this.route.fragment.subscribe((fragment) => { + this.fragmentToScroll = fragment; + this.hasScrolled = false; + + this.loadProduits().then(() => { + if (fragment === 'section-it') { + this.setActive('Solutions IT'); + } else if (fragment === 'section-iot') { + this.setActive('Solutions IoT'); + } else if (fragment === 'section-gps') { + this.setActive('GPS Trackers'); + } else { + this.setActive('Tous'); + } + }); + }); + } + + ngAfterViewChecked(): void { + if (this.fragmentToScroll && !this.hasScrolled) { + const element = document.getElementById(this.fragmentToScroll); + if (element) { + this.zone.runOutsideAngular(() => { + setTimeout(() => { + element.scrollIntoView({ behavior: 'smooth' }); + this.hasScrolled = true; + }, 200); + }); + } + } + } + + async loadProduits(): Promise { + const produitsSansDevis = this.produitSansDevisService.getAllProduits().toPromise(); + const produitsAvecDevis = this.produitAvecDevisService.getAllProduits().toPromise(); + + const [nodevis, avecdevis] = await Promise.all([produitsSansDevis, produitsAvecDevis]); + + this.allProduitsNodevis = nodevis; + this.allProduitsAvecDevis = avecdevis; + + this.produits_nodevis = [...nodevis]; + this.produits_avecdevis = [...avecdevis]; + } + + setActive(label: string) { + this.petitsCadres.forEach((c) => (c.active = c.label === label)); + + if (label === 'Tous') { + this.produits_nodevis = [...this.allProduitsNodevis]; + this.produits_avecdevis = [...this.allProduitsAvecDevis]; + } else if (label === 'GPS Trackers') { + this.produits_nodevis = this.allProduitsNodevis.filter((p) => + p.categorie.toUpperCase().includes('GPS') + ); + this.produits_avecdevis = []; + } else if (label === 'Solutions IoT') { + this.produits_avecdevis = this.allProduitsAvecDevis.filter((p) => + p.categorie.toUpperCase().includes('IOT') + ); + this.produits_nodevis = []; + } else if (label === 'Solutions IT') { + this.produits_avecdevis = []; + this.produits_nodevis = []; + } + } + + isSolutionsITSelected(): boolean { + return this.petitsCadres.find((c) => c.label === 'Solutions IT')?.active || false; + } +} diff --git a/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.html b/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.html new file mode 100644 index 000000000..4e803ec04 --- /dev/null +++ b/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.html @@ -0,0 +1,25 @@ +
+
+
+

Nom :

+

{{ produit.titre }}

+ +

Description :

+

{{ produit.description }}

+
+ +
+

Catégorie :

+ {{ produit.categorie }} + +

Caractéristiques :

+
+ {{ c.texte }} +
+
+
+ +
+ Produit +
+
diff --git a/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.scss b/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.scss new file mode 100644 index 000000000..e0c4cf94a --- /dev/null +++ b/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.scss @@ -0,0 +1,75 @@ +.detail-container { + background: #fff; + border-radius: 12px; + box-shadow: 0 0 15px rgba(0,0,0,0.1); + padding: 30px; + margin: 20px auto; + max-width: 1000px; +} + +.row { + display: flex; + justify-content: space-between; + gap: 40px; + flex-wrap: wrap; +} + +.col-left, .col-right { + flex: 1; + min-width: 300px; +} + +h4 { + margin: 15px 0 5px; + font-weight: bold; + color: #345; +} + +p { + margin: 0; +} + +.badge { + display: inline-block; + padding: 4px 10px; + border-radius: 12px; + font-size: 13px; + font-weight: bold; + color: #fff; +} + +.iot-badge { + background-color: #00a8e8; +} + +.gps-badge { + background-color: #38B000; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 5px; +} + +.tag { + background-color: #e0f0ff; + border-radius: 15px; + padding: 5px 12px; + font-size: 13px; + color: #0074cc; + font-weight: 500; +} + +.image-section { + margin-top: 30px; + text-align: center; +} + +.image-section img { + max-width: 100%; + height: auto; + border-radius: 8px; + box-shadow: 0 0 8px rgba(0,0,0,0.2); +} diff --git a/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.spec.ts b/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.spec.ts new file mode 100644 index 000000000..de36da395 --- /dev/null +++ b/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProduitAvecDevisDetailComponent } from './produit-avec-devis-detail.component'; + +describe('ProduitAvecDevisDetailComponent', () => { + let component: ProduitAvecDevisDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ProduitAvecDevisDetailComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ProduitAvecDevisDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.ts b/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.ts new file mode 100644 index 000000000..1ea0ddf67 --- /dev/null +++ b/src/app/produit-avec-devis-detail/produit-avec-devis-detail.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ProduitAvecDevisService, ProduitAvecDevis } from '../Services/ProduitAvecDevisService'; +import { environment } from 'environments/environment'; + +@Component({ + selector: 'app-produit-avec-devis-detail', + templateUrl: './produit-avec-devis-detail.component.html', + styleUrls: ['./produit-avec-devis-detail.component.scss'] +}) +export class ProduitAvecDevisDetailComponent implements OnInit { + produit!: ProduitAvecDevis; + apiBaseUrl = environment.baseUrl; + + constructor(private route: ActivatedRoute, private produitService: ProduitAvecDevisService) {} + + ngOnInit(): void { + const id = Number(this.route.snapshot.paramMap.get('id')); + this.produitService.getById(id).subscribe({ + next: (data) => this.produit = data, + error: (err) => console.error('Erreur chargement produit IoT :', err) + }); + } + + getImageUrl(imagePath: string): string { + return imagePath.startsWith('/assets') ? imagePath : this.apiBaseUrl + imagePath; + } +} diff --git a/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.html b/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.html new file mode 100644 index 000000000..e1c1534ac --- /dev/null +++ b/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.html @@ -0,0 +1,29 @@ +
+
+
+

Nom :

+

{{ produit.titre }}

+ +

Description :

+

{{ produit.description }}

+ +

Prix :

+

{{ produit.prix }}DT

+
+ +
+

Catégorie :

+ {{ produit.categorie }} + + +

Caractéristiques :

+
+ {{ c.texte }} +
+
+
+ +
+ Produit +
+
diff --git a/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.scss b/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.scss new file mode 100644 index 000000000..8154af581 --- /dev/null +++ b/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.scss @@ -0,0 +1,81 @@ +.detail-container { + background: #fff; + border-radius: 12px; + box-shadow: 0 0 15px rgba(0,0,0,0.1); + padding: 30px; + margin: 20px auto; + max-width: 1000px; +} + +.row { + display: flex; + justify-content: space-between; + gap: 40px; + flex-wrap: wrap; +} + +.col-left, .col-right { + flex: 1; + min-width: 300px; +} + +h4 { + margin: 15px 0 5px; + font-weight: bold; + font-size: x-large; + color: #345; +} +.titre{ + font-weight:bold ; + color: #00a8e8; +} + +p { + margin: 0; +} + +.badge { + display: inline-block; + padding: 4px 10px; + border-radius: 12px; + font-size: 13px; + font-weight: bold; + color: #fff; +} + +.iot-badge { + background-color: #00a8e8; +} + +.gps-badge { + background-color: #00a8e8; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 5px; +} + +.tag { + background-color: #e0f0ff; + border-radius: 15px; + padding: 5px 12px; + font-size: 13px; + color: #0074cc; + font-weight: 500; +} + +.image-section { + margin-top: 30px; + text-align: center; +} + +.image-section img { + max-width: 100%; + height: auto; + border-radius: 8px; + box-shadow: 0 0 8px rgba(0,0,0,0.2); +} + diff --git a/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.spec.ts b/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.spec.ts new file mode 100644 index 000000000..2f3109868 --- /dev/null +++ b/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProduitSansDevisDetailComponent } from './produit-sans-devis-detail.component'; + +describe('ProduitSansDevisDetailComponent', () => { + let component: ProduitSansDevisDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ProduitSansDevisDetailComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ProduitSansDevisDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.ts b/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.ts new file mode 100644 index 000000000..31864c3f2 --- /dev/null +++ b/src/app/produit-sans-devis-detail/produit-sans-devis-detail.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ProduitSansDevisService, ProduitSansDevis } from '../Services/ProduitSansDevisService'; +import { environment } from 'environments/environment'; + +@Component({ + selector: 'app-produit-sans-devis-detail', + templateUrl: './produit-sans-devis-detail.component.html', + styleUrls: ['./produit-sans-devis-detail.component.scss'] +}) +export class ProduitSansDevisDetailComponent implements OnInit { + produit!: ProduitSansDevis; + apiBaseUrl = environment.baseUrl; + + constructor(private route: ActivatedRoute, private produitService: ProduitSansDevisService) {} + + ngOnInit(): void { + const id = Number(this.route.snapshot.paramMap.get('id')); + this.produitService.getById(id).subscribe({ + next: (data) => this.produit = data, + error: (err) => console.error('Erreur chargement produit GPS :', err) + }); + } + + getImageUrl(imagePath: string): string { + return imagePath.startsWith('/assets') ? imagePath : this.apiBaseUrl + imagePath; + } +} diff --git a/src/app/register/register.component.html b/src/app/register/register.component.html new file mode 100644 index 000000000..2cfd8c51b --- /dev/null +++ b/src/app/register/register.component.html @@ -0,0 +1,76 @@ + + + + + + Auth Component + + + + + + + + + +
+
+ + +
+
+
+

{{ 'AUTH.OVERLAY.LEFT.TITLE' | translate }}

+

{{ 'AUTH.OVERLAY.LEFT.TEXT' | translate }}

+ +
+
+

{{ 'AUTH.OVERLAY.RIGHT.TITLE' | translate }}

+

{{ 'AUTH.OVERLAY.RIGHT.TEXT' | translate }}

+ +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/app/register/register.component.scss b/src/app/register/register.component.scss new file mode 100644 index 000000000..075d955f8 --- /dev/null +++ b/src/app/register/register.component.scss @@ -0,0 +1,347 @@ +@import url('https://fonts.googleapis.com/css?family=Montserrat:400,800'); + +.auth-component * { + box-sizing: border-box; + +} +.auth-component { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-color: white; +} + +.auth-box { + width: 1000px; + max-width: 95%; + min-height: 600px; + background-color: #fff; + border-radius: 20px; + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), + 0 10px 10px rgba(0, 0, 0, 0.22); + display: flex; + position: relative; + overflow: hidden; +} + +.auth-component body { + background:white; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + font-family: 'Montserrat', sans-serif; + height: 200vh; + margin: -20px 0 50px; + +} + +.auth-component h1 { + font-weight: bold; + margin: 0; +} + +.auth-component h2 { + text-align: center; +} + +.auth-component p { + font-size: 14px; + font-weight: 100; + line-height: 20px; + letter-spacing: 0.5px; + margin: 20px 0 30px; +} + +.auth-component span { + font-size: 12px; +} + +.auth-component a { + color: #333; + font-size: 14px; + text-decoration: none; + margin: 15px 0; +} + +.auth-component button { + border-radius: 20px; + border: 1px solid #369; + background-color: #369; + color: #FFFFFF; + font-size: 12px; + font-weight: bold; + padding: 12px 45px; + letter-spacing: 1px; + text-transform: uppercase; + transition: transform 80ms ease-in; +} + +.auth-component .overlay { + background: #38B; + background: -webkit-linear-gradient(to right, #357, #37A); + background: linear-gradient(to right, #357, #37A); + background-repeat: no-repeat; + background-size: cover; + background-position: 0 0; + color: #FFFFFF; + position: relative; + left: -100%; + height: 100%; + width: 200%; + transform: translateX(0); + transition: transform 0.6s ease-in-out; +} + + +.auth-component button:active { + transform: scale(0.95); +} + +.auth-component button:focus { + outline: none; +} + +.auth-component button.ghost { + background-color: transparent; + border-color: #FFFFFF; +} + +.auth-component form { + background-color: #FFFFFF; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 0 50px; + height: 100%; + text-align: center; +} + +.auth-component input { + background-color: #eee; + border: none; + padding: 12px 15px; + margin: 8px 0; + width: 100%; +} + +.auth-component .auth-box { + background-color: #fff; + border-radius: 10px; + box-shadow: 0 14px 28px rgba(0,0,0,0.25), + 0 10px 10px rgba(0,0,0,0.22); + position: relative; + overflow: hidden; + width: 768px; + max-width: 100%; + min-height: 480px; +} + +.auth-component .form-auth-box { + position: absolute; + top: 0; + height: 100%; + transition: all 0.6s ease-in-out; +} + +.auth-component .sign-in-auth-box { + left: 0; + width: 50%; + z-index: 2; +} + +.auth-component .auth-box.right-panel-active .sign-in-auth-box { + transform: translateX(100%); +} + +.auth-component .sign-up-auth-box { + left: 0; + width: 50%; + opacity: 1; + z-index: 1; +} + +.auth-component .auth-box.right-panel-active .sign-up-auth-box { + transform: translateX(100%); + opacity: 1; + z-index: 5; + animation: show 0.6s; +} + +@keyframes show { + 0%, 49.99% { + opacity: 0; + z-index: 1; + } + + 50%, 100% { + opacity: 1; + z-index: 5; + } +} + +.auth-component .overlay-auth-box { + position: absolute; + top: 0; + left: 50%; + width: 50%; + height: 100%; + overflow: hidden; + transition: transform 0.6s ease-in-out; + z-index: 100; +} + +.auth-component .auth-box.right-panel-active .overlay-auth-box{ + transform: translateX(-100%); +} + + +.auth-component .auth-box.right-panel-active .overlay { + transform: translateX(50%); +} + +.auth-component .overlay-panel { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 0 40px; + text-align: center; + top: 0; + height: 100%; + width: 50%; + transform: translateX(0); + transition: transform 0.6s ease-in-out; +} + +.auth-component .overlay-left { + transform: translateX(-20%); +} + +.auth-component .auth-box.right-panel-active .overlay-left { + transform: translateX(0); +} + +.auth-component .overlay-right { + right: 0; + transform: translateX(0); +} + +.auth-component .auth-box.right-panel-active .overlay-right { + transform: translateX(20%); +} + +.auth-component .social-auth-box { + margin: 20px 0; +} + +.auth-component .social-auth-box a { + border: 1px solid #DDDDDD; + border-radius: 50%; + display: inline-flex; + justify-content: center; + align-items: center; + margin: 0 5px; + height: 40px; + width: 40px; +} +.back-arrow { + position: fixed; + top: 60px; + left: 20px; + font-size: 24px; + color: #007bff; + text-decoration: none; + z-index: 1000; + background: rgba(255, 255, 255, 0.8); + padding: 10px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + + @media (max-width: 768px) { + top: 40px; + left: 10px; + font-size: 20px; + width: 35px; + height: 35px; + } + + &:hover { + color: #0056b3; + background: rgba(255, 255, 255, 1); + } +} +.auth-component input::placeholder { + color: #369; + opacity: 1; +} + +input:-ms-input-placeholder { color: #369; } +input::-ms-input-placeholder { color: #369; } + +@media (max-width: 768px) { + .auth-box { + max-width: 90%; + padding: 15px; + + } + + .auth-component h1 { + font-size: 20px; + } + + .auth-component p, + .auth-component span { + font-size: 13px; + } + + .auth-component button { + padding: 10px 25px; + font-size: 13px; + } + + .auth-component input { + padding: 10px; + font-size: 13px; + } + + .back-arrow { + top: 15px; + left: 15px; + font-size: 18px; + width: 35px; + height: 35px; + } +} + +@media (max-width: 576px) { + .auth-box { + padding: 10px; + } + + .auth-component h1 { + font-size: 18px; + } + + .auth-component p, + .auth-component span { + font-size: 12px; + } + + .auth-component button { + padding: 8px 20px; + font-size: 12px; + } + + .auth-component input { + padding: 8px; + font-size: 12px; + } +} \ No newline at end of file diff --git a/src/app/register/register.component.spec.ts b/src/app/register/register.component.spec.ts new file mode 100644 index 000000000..f97553378 --- /dev/null +++ b/src/app/register/register.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegisterComponent } from './register.component'; + +describe('RegisterComponent', () => { + let component: RegisterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ RegisterComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/register/register.component.ts b/src/app/register/register.component.ts new file mode 100644 index 000000000..b7730ac51 --- /dev/null +++ b/src/app/register/register.component.ts @@ -0,0 +1,174 @@ +import { Component, OnInit, AfterViewInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from 'app/Services/auth.service'; +import { EmailjsService } from 'emailJs/email.service'; +import { environment } from 'environments/environment'; +import Swal from 'sweetalert2'; +import { CookieService } from 'ngx-cookie-service'; + + +@Component({ + selector: 'app-register', + templateUrl: './register.component.html', + styleUrls: ['./register.component.scss'] +}) +export class RegisterComponent implements OnInit, AfterViewInit { + signUpName: string = ''; + signUpEmail: string = ''; + frontUrl=environment.frontUrl; + signInEmail: string = ''; + signInPassword: string = ''; + constructor(private authService: AuthService,private router: Router,private emailjsService: EmailjsService,private cookieService: CookieService) { } + + ngOnInit(): void { + } + + ngAfterViewInit(): void { + const signUpButton = document.getElementById('signUp'); + const signInButton = document.getElementById('signIn'); + const container = document.getElementById('auth-box'); + + if (signUpButton && signInButton && container) { + signUpButton.addEventListener('click', () => { + container.classList.add("right-panel-active"); + }); + + signInButton.addEventListener('click', () => { + container.classList.remove("right-panel-active"); + }); + } + } + + onSignUp(): void { + const data = { + nom: this.signUpName, + email: this.signUpEmail, + role:0 + }; + + this.authService.signUp(data).subscribe({ + next: (response) => { + const params = { + email: data.email, + name: data.nom, + password: response.mdp, + loginLink: this.frontUrl+'/auth' + }; + + this.emailjsService.sendPasswordEmail(params); + + Swal.fire({ + icon: 'success', + title: 'Registration successful', + text: 'An email containing your password will be sent.' + }); + }, + error: (err) => { + Swal.fire({ + icon: 'error', + title: 'Error', + text: 'An account with this email already exists.' + }); + } + }); + } + +onSignIn() { + const data = { + email: this.signInEmail, + password: this.signInPassword + }; + + this.authService.signIn(data).subscribe({ + next: (res) => { + this.cookieService.set('token', res.token); + this.cookieService.set('role', res.role); + this.cookieService.set('name', res.nom); + this.cookieService.set('userId', res.userId); + const redirectUrl = this.cookieService.get('redirectAfterLogin'); + this.cookieService.delete('redirectAfterLogin'); + Swal.fire({ + icon: 'success', + title: 'Login successful', + text: `Welcome ${res.nom} !`, + confirmButtonText: 'Continue' + }).then((result) => { + if (result.isConfirmed) { + if (res.role === 'Client') { + if (redirectUrl && (redirectUrl.includes('formulaireiotit') || redirectUrl.includes('formulairefranchise'))) { + this.router.navigateByUrl(redirectUrl); + } else { + this.router.navigate(['/home']); + } + } else { + this.router.navigate(['/admin']); + } + } + }); + }, + error: (err) => { + Swal.fire({ + icon: 'error', + title: 'Login failed', + text: 'Incorrect email or password.' + }); + } + }); +} + +showForgetForm = false; +forgetEmail: string = ''; +handleForgetPassword(email: string): void { + if (!email) { + Swal.fire({ + icon: 'warning', + title: 'Email required', + text: 'Please enter your email address.', + }); + return; + } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + Swal.fire({ + icon: 'warning', + title: 'Invalid email', + text: 'Please enter a valid email address.', + }); + return; + } + this.authService.forgetPassword({ email }).subscribe({ + next: (res) => { + const params = { + email: email, + password: res.mdp, + loginLink: this.frontUrl + '/auth' + }; + + this.emailjsService.sendPasswordEmail(params).then(() => { + Swal.fire({ + icon: 'success', + title: 'Password reset email sent', + text: 'Please check your inbox for your new password.', + }); + this.showForgetForm = false; + this.forgetEmail = ''; + }).catch((err) => { + Swal.fire({ + icon: 'error', + title: 'Email error', + text: err?.message || 'Failed to send email. Try again later.', + }); + }); + }, + error: (err) => { + Swal.fire({ + icon: 'error', + title: 'Reset error', + text: err.error?.message || 'Failed to reset password.', + }); + } + }); +} + + +} diff --git a/src/app/shared/navbar/navbar.component.ts b/src/app/shared/navbar/navbar.component.ts index 068ed7ee8..1eaf4638a 100644 --- a/src/app/shared/navbar/navbar.component.ts +++ b/src/app/shared/navbar/navbar.component.ts @@ -51,16 +51,15 @@ export class NavbarComponent implements OnInit{ }; getTitle(){ - var titlee = this.location.prepareExternalUrl(this.location.path()); + /*var titlee = this.location.prepareExternalUrl(this.location.path()); if(titlee.charAt(0) === '#'){ titlee = titlee.slice( 1 ); } for(var item = 0; item < this.listTitles.length; item++){ - if(this.listTitles[item].path === titlee){ return this.listTitles[item].title; - } + } - return 'Dashboard'; + return 'Dashboard';*/ } } diff --git a/src/app/shared/navbar/navbar.module.ts b/src/app/shared/navbar/navbar.module.ts index 0a76b80ed..375b2849c 100644 --- a/src/app/shared/navbar/navbar.module.ts +++ b/src/app/shared/navbar/navbar.module.ts @@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { NavbarComponent } from './navbar.component'; - @NgModule({ imports: [ RouterModule, CommonModule ], declarations: [ NavbarComponent ], diff --git a/src/app/sidebar/sidebar.component.html b/src/app/sidebar/sidebar.component.html index 817b194c6..9a7566a59 100644 --- a/src/app/sidebar/sidebar.component.html +++ b/src/app/sidebar/sidebar.component.html @@ -1,78 +1,20 @@ +