Browse Source

final commit

devBranch
Benjamin Arhen 3 years ago
parent
commit
54d3b97922
  1. 33988
      package-lock.json
  2. 8
      package.json
  3. 4
      src/app/app.component.ts
  4. 37
      src/app/app.module.ts
  5. 6
      src/app/app.resolvers.ts
  6. 4
      src/app/app.routing.ts
  7. 58
      src/app/core/auth/auth.service.ts
  8. 49
      src/app/core/user/user.service.ts
  9. 10
      src/app/layout/common/goldcoin/goldcoin.component.ts
  10. 164
      src/app/layout/common/notifications/notifications.component.html
  11. 156
      src/app/layout/common/notifications/notifications.component.ts
  12. 6
      src/app/layout/common/user/user.component.html
  13. 14
      src/app/layout/common/user/user.component.ts
  14. 2
      src/app/layout/layouts/vertical/compact/compact.component.html
  15. 5
      src/app/models/businessModel.ts
  16. 52
      src/app/models/dashSummary.ts
  17. 5
      src/app/models/generalModel.ts
  18. 6
      src/app/pages/admin/Coupons/Active/active-coupons.component.ts
  19. 1
      src/app/pages/admin/Coupons/DetailsDialog/details-dialog.component.ts
  20. 4
      src/app/pages/admin/Coupons/Inactive/inactive-coupons.component.html
  21. 2
      src/app/pages/admin/Coupons/Inactive/inactive-coupons.component.ts
  22. 2
      src/app/pages/admin/Coupons/coupons.component.html
  23. 297
      src/app/pages/admin/Dashboard/dashboard.component.html
  24. 21
      src/app/pages/admin/Dashboard/dashboard.component.scss
  25. 3548
      src/app/pages/admin/Dashboard/dashboard.component.ts
  26. 8
      src/app/pages/admin/Dashboard/dashboard.module.ts
  27. 37
      src/app/pages/admin/Dashboard/dashboard.service.ts
  28. 47
      src/app/pages/admin/Products/EditProduct/edit-product.component.html
  29. 63
      src/app/pages/admin/Products/EditProduct/edit-product.component.ts
  30. 29
      src/app/pages/admin/Products/products.service.ts
  31. 2
      src/app/pages/admin/Profile/InfoDialog/information-dialog.component.html
  32. 2
      src/app/pages/admin/Profile/InfoDialog/information-dialog.component.ts
  33. 3
      src/app/pages/admin/Profile/Information/information.component.html
  34. 2
      src/app/pages/admin/Profile/Information/information.component.ts
  35. 4
      src/app/pages/admin/Profile/QRCode/qrcode-dialog.component.html
  36. 0
      src/app/pages/admin/Profile/QRCode/qrcode-dialog.component.scss
  37. 25
      src/app/pages/admin/Profile/QRCode/qrcode-dialog.component.spec.ts
  38. 20
      src/app/pages/admin/Profile/QRCode/qrcode-dialog.component.ts
  39. 22
      src/app/pages/admin/Profile/profile.component.html
  40. 37
      src/app/pages/admin/Profile/profile.component.ts
  41. 4
      src/app/pages/admin/Profile/profile.module.ts
  42. 23
      src/app/pages/admin/Profile/profile.service.ts
  43. 2
      src/app/pages/admin/TempShop/temp-shop.component.ts
  44. 62
      src/app/pages/auth/confirmation-required/confirmation-required.component.html
  45. 18
      src/app/pages/auth/confirmation-required/confirmation-required.component.ts
  46. 22
      src/app/pages/auth/confirmation-required/confirmation-required.module.ts
  47. 9
      src/app/pages/auth/confirmation-required/confirmation-required.routing.ts
  48. 93
      src/app/pages/auth/forgot-password/forgot-password.component.html
  49. 105
      src/app/pages/auth/forgot-password/forgot-password.component.ts
  50. 32
      src/app/pages/auth/forgot-password/forgot-password.module.ts
  51. 9
      src/app/pages/auth/forgot-password/forgot-password.routing.ts
  52. 122
      src/app/pages/auth/reset-password/reset-password.component.html
  53. 111
      src/app/pages/auth/reset-password/reset-password.component.ts
  54. 32
      src/app/pages/auth/reset-password/reset-password.module.ts
  55. 9
      src/app/pages/auth/reset-password/reset-password.routing.ts
  56. 94
      src/app/pages/auth/sign-in/sign-in.component.css
  57. 122
      src/app/pages/auth/sign-in/sign-in.component.html
  58. 211
      src/app/pages/auth/sign-in/sign-in.component.ts
  59. 8
      src/app/pages/auth/sign-in/sign-in.module.ts
  60. 106
      src/app/pages/auth/sign-in/signOld.txt
  61. 136
      src/app/pages/auth/sign-up/sign-up.component.html
  62. 104
      src/app/pages/auth/sign-up/sign-up.component.ts
  63. 34
      src/app/pages/auth/sign-up/sign-up.module.ts
  64. 9
      src/app/pages/auth/sign-up/sign-up.routing.ts
  65. 25
      src/app/pages/auth/verification/verification-dialog.component.html
  66. 82
      src/app/pages/auth/verification/verification-dialog.component.scss
  67. 90
      src/app/pages/auth/verification/verification-dialog.component.ts
  68. 13
      src/app/pipes/percentage-pipe.ts
  69. 11
      src/app/window.service.ts
  70. BIN
      src/assets/icons/notransaction.png
  71. 12
      src/environments/environment.prod.ts
  72. 14
      src/environments/environment.ts

33988
package-lock.json

File diff suppressed because it is too large

8
package.json

@ -18,6 +18,7 @@
"@angular/common": "13.0.2", "@angular/common": "13.0.2",
"@angular/compiler": "13.0.2", "@angular/compiler": "13.0.2",
"@angular/core": "13.0.2", "@angular/core": "13.0.2",
"@angular/fire": "^7.3.0",
"@angular/forms": "13.0.2", "@angular/forms": "13.0.2",
"@angular/material": "13.0.2", "@angular/material": "13.0.2",
"@angular/material-moment-adapter": "13.0.2", "@angular/material-moment-adapter": "13.0.2",
@ -35,20 +36,25 @@
"@fullcalendar/rrule": "4.4.2", "@fullcalendar/rrule": "4.4.2",
"@fullcalendar/timegrid": "4.4.2", "@fullcalendar/timegrid": "4.4.2",
"@ngneat/transloco": "^2.23.5", "@ngneat/transloco": "^2.23.5",
"@types/grecaptcha": "^3.0.4",
"apexcharts": "3.28.1", "apexcharts": "3.28.1",
"chart.js": "^3.7.1",
"crypto-js": "3.3.0", "crypto-js": "3.3.0",
"dayjs": "^1.10.7", "dayjs": "^1.11.0",
"highlight.js": "11.2.0", "highlight.js": "11.2.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.9.49", "libphonenumber-js": "^1.9.49",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"moment": "2.29.1", "moment": "2.29.1",
"ng-apexcharts": "1.5.12", "ng-apexcharts": "1.5.12",
"ng-otp-input": "^1.8.5",
"ngx-markdown": "^12.1.0", "ngx-markdown": "^12.1.0",
"ngx-mat-intl-tel-input": "^4.1.0", "ngx-mat-intl-tel-input": "^4.1.0",
"ngx-quill": "14.3.0", "ngx-quill": "14.3.0",
"node.js": "^0.0.1-security", "node.js": "^0.0.1-security",
"p-charts": "^2.0.1",
"perfect-scrollbar": "1.5.2", "perfect-scrollbar": "1.5.2",
"primeng": "^13.3.2",
"quill": "1.3.7", "quill": "1.3.7",
"rrule": "2.6.8", "rrule": "2.6.8",
"rxjs": "6.6.7", "rxjs": "6.6.7",

4
src/app/app.component.ts

@ -1,6 +1,8 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { SwUpdate } from '@angular/service-worker'; import { SwUpdate } from '@angular/service-worker';
import { interval } from 'rxjs'; import { interval } from 'rxjs';
import firebase from 'firebase/compat/app';
import { environment } from 'environments/environment';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -14,6 +16,8 @@ export class AppComponent implements OnInit {
constructor( constructor(
private swUpdate: SwUpdate, private swUpdate: SwUpdate,
) { ) {
firebase.initializeApp(environment.firebaseConfig);
} }
ngOnInit(): void { ngOnInit(): void {
// check for platform update // check for platform update

37
src/app/app.module.ts

@ -13,16 +13,21 @@ import { AppComponent } from 'app/app.component';
import { appRoutes } from 'app/app.routing'; import { appRoutes } from 'app/app.routing';
import { LocationStrategy, PathLocationStrategy } from '@angular/common'; import { LocationStrategy, PathLocationStrategy } from '@angular/common';
import { tesoModule } from '@teso/teso.module'; import { tesoModule } from '@teso/teso.module';
import { NgxAwesomePopupModule,ConfirmBoxConfigModule} from '@costlydeveloper/ngx-awesome-popup'; import { NgxAwesomePopupModule, ConfirmBoxConfigModule } from '@costlydeveloper/ngx-awesome-popup';
import { ProductDescriptionShort } from './pipes/productDescriptionShort.pipe';
import { CouponWorthPipe } from './pipes/coupon-worth.pipe';
import { GoldTransactionComponent } from './pages/admin/GoldPurchase/gold-transaction.component'; import { GoldTransactionComponent } from './pages/admin/GoldPurchase/gold-transaction.component';
import { ServiceWorkerModule } from '@angular/service-worker'; import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from 'environments/environment.prod'; import { environment } from '../environments/environment';
import { TempShopModule } from './pages/admin/TempShop/temp-shop.module'; import { TempShopModule } from './pages/admin/TempShop/temp-shop.module';
import { provideFirebaseApp, getApp, initializeApp } from '@angular/fire/app';
import { getFirestore, provideFirestore } from '@angular/fire/firestore';
import { provideAuth, getAuth } from '@angular/fire/auth';
import { NgOtpInputModule } from 'ng-otp-input';
import { VerificationDialogComponent } from './pages/auth/verification/verification-dialog.component';
import { AngularFireModule } from '@angular/fire/compat';
import { AngularFirestoreModule } from '@angular/fire/compat/firestore';
const routerConfig: ExtraOptions = { const routerConfig: ExtraOptions = {
preloadingStrategy : PreloadAllModules, preloadingStrategy: PreloadAllModules,
scrollPositionRestoration: 'enabled' scrollPositionRestoration: 'enabled'
}; };
@ -30,25 +35,33 @@ const routerConfig: ExtraOptions = {
declarations: [ declarations: [
AppComponent, AppComponent,
GoldTransactionComponent, GoldTransactionComponent,
VerificationDialogComponent,
], ],
imports : [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule, BrowserAnimationsModule,
RouterModule.forRoot(appRoutes, routerConfig), RouterModule.forRoot(appRoutes, routerConfig),
NgOtpInputModule,
NgxAwesomePopupModule.forRoot(), // Essential, mandatory main module. NgxAwesomePopupModule.forRoot(), // Essential, mandatory main module.
ConfirmBoxConfigModule.forRoot(), ConfirmBoxConfigModule.forRoot(),
// teso, tesoConfig & tesoMockAPI // teso, tesoConfig & tesoMockAPI
tesoModule, tesoModule,
tesoConfigModule.forRoot(appConfig), tesoConfigModule.forRoot(appConfig),
tesoMockApiModule.forRoot(mockApiServices), tesoMockApiModule.forRoot(mockApiServices),
TempShopModule, TempShopModule,
// Core module of your application // Core module of your application
CoreModule, CoreModule,
// Layout module of your application // Layout module of your application
LayoutModule, LayoutModule,
provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
provideAuth(() => getAuth()),
provideFirestore(() => getFirestore()),
AngularFireModule.initializeApp(environment.firebaseConfig),
AngularFirestoreModule,
// provideFirestore(() => getFirestorre()),
// 3rd party modules that require global configuration via forRoot // 3rd party modules that require global configuration via forRoot
MarkdownModule.forRoot({}), MarkdownModule.forRoot({}),
ServiceWorkerModule.register('ngsw-worker.js', { ServiceWorkerModule.register('ngsw-worker.js', {
@ -60,11 +73,13 @@ TempShopModule,
], ],
providers: [ providers: [
{ provide: LocationStrategy, useClass: PathLocationStrategy }, { provide: LocationStrategy, useClass: PathLocationStrategy },
], ],
bootstrap : [ bootstrap: [
AppComponent AppComponent
] ]
}) })
export class AppModule export class AppModule {
{
} }

6
src/app/app.resolvers.ts

@ -11,6 +11,7 @@ import { CouponsService } from './pages/admin/Coupons/coupons.service';
import { ProductsService } from './pages/admin/Products/products.service'; import { ProductsService } from './pages/admin/Products/products.service';
import { FollowersService } from './pages/admin/Followers/followers.service'; import { FollowersService } from './pages/admin/Followers/followers.service';
import { ProfileService } from './pages/admin/Profile/profile.service'; import { ProfileService } from './pages/admin/Profile/profile.service';
import { DashboardService } from './pages/admin/Dashboard/dashboard.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -31,6 +32,7 @@ export class InitialDataResolver implements Resolve<any>
private _goldCoinService: GoldCoinService, private _goldCoinService: GoldCoinService,
private _subscriberService:FollowersService, private _subscriberService:FollowersService,
private _profileService:ProfileService, private _profileService:ProfileService,
private _dashboardService:DashboardService,
) { ) {
} }
@ -57,7 +59,9 @@ export class InitialDataResolver implements Resolve<any>
this._productService.getCategories(), this._productService.getCategories(),
this._productService.getData(), this._productService.getData(),
this._subscriberService.getData(), this._subscriberService.getData(),
this._profileService.getCategories() this._profileService.getCategories(),
this._dashboardService.getData(),
this._dashboardService.getDataGraph()
]); ]);
} }
} }

4
src/app/app.routing.ts

@ -29,11 +29,7 @@ export const appRoutes: Route[] = [
layout: 'empty' layout: 'empty'
}, },
children: [ children: [
{path: 'confirmation-required', loadChildren: () => import('app/pages/auth/confirmation-required/confirmation-required.module').then(m => m.AuthConfirmationRequiredModule)},
{path: 'forgot-password', loadChildren: () => import('app/pages/auth/forgot-password/forgot-password.module').then(m => m.AuthForgotPasswordModule)},
{path: 'reset-password', loadChildren: () => import('app/pages/auth/reset-password/reset-password.module').then(m => m.AuthResetPasswordModule)},
{path: 'sign-in', loadChildren: () => import('app/pages/auth/sign-in/sign-in.module').then(m => m.AuthSignInModule)}, {path: 'sign-in', loadChildren: () => import('app/pages/auth/sign-in/sign-in.module').then(m => m.AuthSignInModule)},
{path: 'sign-up', loadChildren: () => import('app/pages/auth/sign-up/sign-up.module').then(m => m.AuthSignUpModule)}
] ]
}, },

58
src/app/core/auth/auth.service.ts

@ -4,6 +4,7 @@ import { Observable, of, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators'; import { catchError, switchMap } from 'rxjs/operators';
import { AuthUtils } from 'app/core/auth/auth.utils'; import { AuthUtils } from 'app/core/auth/auth.utils';
import { UserService } from 'app/core/user/user.service'; import { UserService } from 'app/core/user/user.service';
import { environment } from 'environments/environment';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
@ -76,10 +77,9 @@ export class AuthService {
return this._httpClient.post('api/auth/sign-in', credentials).pipe( return this._httpClient.post('api/auth/sign-in', credentials).pipe(
switchMap((response: any) => { switchMap((response: any) => {
// Store the access token in the local storage // Store the access token in the local storage
this.accessToken = response.accessToken; this.accessToken = response.accessToken;
this.relevantToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiItMTU5MDU0MDI0NCIsImp0aSI6IjRiYzI2MzdlLWY5OGEtNGZhOS04YmQ3LWNhOWYxYzQzYzAyNyIsImlzcyI6IlRFU08gQVVUSCBTRVJWRVIiLCJleHAiOjE2NDg4ODY1MDEsIm5iZiI6Ii0xNTkwNDgwMjQ0Iiwic3ViIjoiMVRFU0JVMDAwMDAwMDAiLCJidXNpbmVzc0lEIjoiMVRFU0JVMDAwMDAwMDAiLCJzdWJzY3JpcHRpb25QbGFuIjoiVFRTMDAxIiwiYnVzaW5lc3NOYW1lIjoiVGVzbyBHaGFuYSIsImF1ZCI6IlRFU08ifQ.mQg8CylBYrvA-L2570Rr-NOqIuuPeNhHAzorN6OW_Rs"; this.relevantToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiItMTk4NjA3NTYzIiwianRpIjoiODE5MTFjMzEtMzFjOC00MTVhLTk2Y2UtODg4ODBhZmE3Y2JkIiwiaXNzIjoiVEVTTyBBVVRIIFNFUlZFUiIsImV4cCI6MTY1MDI3ODQzNCwibmJmIjoiLTE5ODU0NzU2MyIsInN1YiI6IjFURVNCVTAwMDAwMDAwIiwiYnVzaW5lc3NJRCI6IjFURVNCVTAwMDAwMDAwIiwic3Vic2NyaXB0aW9uUGxhbiI6IlRUUzAwMSIsImJ1c2luZXNzTmFtZSI6IlRlc28gR2hhbmEiLCJhdWQiOiJURVNPIn0.x9Bj1F8DsLWStH30F44kff4W8Q8Wk6LrRmseBKLafs0";
// Set the authenticated flag to true // Set the authenticated flag to true
this._authenticated = true; this._authenticated = true;
@ -141,19 +141,32 @@ export class AuthService {
* *
* @param user * @param user
*/ */
signUp(user: { name: string; email: string; password: string; company: string }): Observable<any> { signUp(telephone: any, name: any, address: any,image:any): Observable<any> {
return this._httpClient.post('api/auth/sign-up', user); const formData = new FormData();
} formData.set("telephone", telephone);
formData.set("businessName", name);
formData.set("digitalAddress", address);
if (image != null) {
formData.set("file", image.file);
}
return this._httpClient.post(environment.apiURL + 'businessregistration', formData).pipe(
switchMap((response: any) => {
// Store the access token in the local storage
this.accessToken = response.tokenTeso;
this.relevantToken = response.tokenTeso;
// Set the authenticated flag to true
this._authenticated = true;
// Store the user on the user service
this._userService.user = response.business;
// Return a new observable with the response
return of(response);
}));
/**
* Unlock session
*
* @param credentials
*/
unlockSession(credentials: { email: string; password: string }): Observable<any> {
return this._httpClient.post('api/auth/unlock-session', credentials);
} }
/** /**
* Check the authentication status * Check the authentication status
*/ */
@ -178,4 +191,25 @@ export class AuthService {
// If the access token exists and it didn't expire, sign in using it // If the access token exists and it didn't expire, sign in using it
return this.signInUsingToken(); return this.signInUsingToken();
} }
authenticate(phonenumber): Observable<any> {
const formData = new FormData();
formData.set("phoneNumber", phonenumber);
return this._httpClient.post(environment.apiURL + "businessauth", formData).pipe(
switchMap((response: any) => {
// Store the access token in the local storage
this.accessToken = response.tokenTeso;
this.relevantToken = response.tokenTeso;
// Set the authenticated flag to true
this._authenticated = true;
// Store the user on the user service
this._userService.user = response.business;
// Return a new observable with the response
return of(response);
})
);
}
} }

49
src/app/core/user/user.service.ts

@ -5,19 +5,18 @@ import { map, tap } from 'rxjs/operators';
import { User } from 'app/core/user/user.types'; import { User } from 'app/core/user/user.types';
import { TesoBusinessDetail } from 'app/models/businessModel'; import { TesoBusinessDetail } from 'app/models/businessModel';
import { environment } from 'environments/environment'; import { environment } from 'environments/environment';
import { ProfileImage } from 'app/models/generalModel';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class UserService export class UserService {
{
private _user: ReplaySubject<TesoBusinessDetail> = new ReplaySubject<TesoBusinessDetail>(1); private _user: ReplaySubject<TesoBusinessDetail> = new ReplaySubject<TesoBusinessDetail>(1);
/** /**
* Constructor * Constructor
*/ */
constructor(private _httpClient: HttpClient) constructor(private _httpClient: HttpClient) {
{
} }
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
@ -29,14 +28,12 @@ export class UserService
* *
* @param value * @param value
*/ */
set user(value: TesoBusinessDetail) set user(value: TesoBusinessDetail) {
{
// Store the value // Store the value
this._user.next(value); this._user.next(value);
} }
get user$(): Observable<TesoBusinessDetail> get user$(): Observable<TesoBusinessDetail> {
{
return this._user.asObservable(); return this._user.asObservable();
} }
@ -47,9 +44,8 @@ export class UserService
/** /**
* Get the current logged in user data * Get the current logged in user data
*/ */
get(): Observable<TesoBusinessDetail> get(): Observable<TesoBusinessDetail> {
{ return this._httpClient.get<TesoBusinessDetail>(environment.apiURL + 'business/getProfile').pipe(
return this._httpClient.get<TesoBusinessDetail>(environment.apiURL +'business/getProfile').pipe(
tap((user) => { tap((user) => {
this._user.next(user); this._user.next(user);
}) })
@ -61,12 +57,31 @@ export class UserService
* *
* @param user * @param user
*/ */
update(user: TesoBusinessDetail): Observable<any> update(user: TesoBusinessDetail, image: ProfileImage): Observable<any> {
{ const formData = new FormData();
formData.set("name", user.businessName);
formData.set("description", user.businessDescription);
formData.set("category", user.businessCategory);
formData.set("address", user.businessAddress);
formData.set("digitalAddress", user.businessDigitalAddress);
formData.set("email", user.businessEmail);
formData.set("date", user.dateOfEst.toString());
formData.set("contact", user.businessContact);
if (image != null) {
formData.set("file", image.file);
user.businessLogo = image.imageSRC;
user.handle = true;
}else{
user.handle = false;
user.businessLogo = "";
}
this._user.next(user); this._user.next(user);
return this._httpClient.patch<TesoBusinessDetail>('api/common/user', {user}).pipe( return this._httpClient.post(environment.apiURL + 'business/updateprofile', formData);
map((response) => { }
})
); updateSub(user: TesoBusinessDetail) {
this._user.next(user);
} }
} }

10
src/app/layout/common/goldcoin/goldcoin.component.ts

@ -32,11 +32,11 @@ export class GoldCoinComponent implements OnInit, OnDestroy, AfterViewInit {
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
interval(1000) // interval(1000)
// .pipe(takeWhile(() => !stop)) // // .pipe(takeWhile(() => !stop))
.subscribe(() => { // .subscribe(() => {
this._goldcoinsService.getAll(); // this._goldcoinsService.getAll();
}); // });
} }
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------

164
src/app/layout/common/notifications/notifications.component.html

@ -1,11 +1,9 @@
<!-- Notifications toggle --> <!-- Notifications toggle -->
<button <button mat-icon-button (click)="openPanel()" #notificationsOrigin>
mat-icon-button
(click)="openPanel()"
#notificationsOrigin>
<ng-container *ngIf="unreadCount > 0"> <ng-container *ngIf="unreadCount > 0">
<span class="absolute top-0 right-0 left-0 flex items-center justify-center h-3"> <span class="absolute top-0 right-0 left-0 flex items-center justify-center h-3">
<span class="flex items-center justify-center flex-shrink-0 min-w-4 h-4 px-1 ml-4 mt-2.5 rounded-full bg-teal-600 text-indigo-50 text-xs font-medium"> <span
class="flex items-center justify-center flex-shrink-0 min-w-4 h-4 px-1 ml-4 mt-2.5 rounded-full bg-teal-600 text-indigo-50 text-xs font-medium">
{{unreadCount}} {{unreadCount}}
</span> </span>
</span> </span>
@ -16,29 +14,22 @@
<!-- Notifications panel --> <!-- Notifications panel -->
<ng-template #notificationsPanel> <ng-template #notificationsPanel>
<div class="fixed inset-0 sm:static sm:inset-auto flex flex-col sm:min-w-90 sm:w-90 sm:rounded-2xl overflow-hidden shadow-lg"> <div
class="fixed inset-0 sm:static sm:inset-auto flex flex-col sm:min-w-90 sm:w-90 sm:rounded-2xl overflow-hidden shadow-lg">
<!-- Header --> <!-- Header -->
<div class="flex flex-shrink-0 items-center py-4 pr-4 pl-6 bg-primary text-on-primary"> <div class="flex flex-shrink-0 items-center py-4 pr-4 pl-6 bg-primary text-on-primary"
style="background-color: #0f172a !important;">
<div class="sm:hidden -ml-1 mr-3"> <div class="sm:hidden -ml-1 mr-3">
<button <button mat-icon-button (click)="closePanel()">
mat-icon-button <mat-icon class="icon-size-5 text-current" [svgIcon]="'heroicons_solid:x'"></mat-icon>
(click)="closePanel()">
<mat-icon
class="icon-size-5 text-current"
[svgIcon]="'heroicons_solid:x'"></mat-icon>
</button> </button>
</div> </div>
<div class="text-lg font-medium leading-10">Notifications</div> <div class="text-lg font-medium leading-10">Notifications</div>
<div class="ml-auto"> <div class="ml-auto">
<button <button mat-icon-button [matTooltip]="'Mark all as read'" [disabled]="unreadCount === 0"
mat-icon-button
[matTooltip]="'Mark all as read'"
[disabled]="unreadCount === 0"
(click)="markAllAsRead()"> (click)="markAllAsRead()">
<mat-icon <mat-icon class="icon-size-5 text-current" [svgIcon]="'heroicons_solid:mail-open'"></mat-icon>
class="icon-size-5 text-current"
[svgIcon]="'heroicons_solid:mail-open'"></mat-icon>
</button> </button>
</div> </div>
</div> </div>
@ -47,96 +38,110 @@
<div class="relative flex flex-col flex-auto sm:max-h-120 divide-y overflow-y-auto bg-card"> <div class="relative flex flex-col flex-auto sm:max-h-120 divide-y overflow-y-auto bg-card">
<!-- Notifications --> <!-- Notifications -->
<ng-container *ngFor="let notification of notifications; trackBy: trackByFn"> <ng-container *ngFor="let notification of notifications; trackBy: trackByFn">
<div <div class="flex group hover:bg-gray-50 dark:hover:bg-black dark:hover:bg-opacity-5"
class="flex group hover:bg-gray-50 dark:hover:bg-black dark:hover:bg-opacity-5"
[ngClass]="{'unread': !notification.read}"> [ngClass]="{'unread': !notification.read}">
<!-- Notification with a link -->
<ng-container *ngIf="notification.link"> <!-- Notification without a link -->
<!-- Normal links --> <ng-container *ngIf="notification.notificationType === 'subscription'">
<ng-container *ngIf="!notification.useRouter"> <div class="flex flex-auto py-5 pl-6">
<a <ng-container *ngTemplateOutlet="notificationSubscription"></ng-container>
class="flex flex-auto py-5 pl-6 cursor-pointer" </div>
[href]="notification.link">
<ng-container *ngTemplateOutlet="notificationContent"></ng-container>
</a>
</ng-container>
<!-- Router links -->
<ng-container *ngIf="notification.useRouter">
<a
class="flex flex-auto py-5 pl-6 cursor-pointer"
[routerLink]="notification.link">
<ng-container *ngTemplateOutlet="notificationContent"></ng-container>
</a>
</ng-container> </ng-container>
<!-- Notification without a link -->
<ng-container *ngIf="notification.notificationType === 'couponRedemption'">
<div class="flex flex-auto py-5 pl-6">
<ng-container *ngTemplateOutlet="notificationRedemption"></ng-container>
</div>
</ng-container> </ng-container>
<!-- Notification without a link --> <!-- Notification without a link -->
<ng-container *ngIf="!notification.link"> <ng-container *ngIf="notification.notificationType === 'couponAcquisition'">
<div class="flex flex-auto py-5 pl-6"> <div class="flex flex-auto py-5 pl-6">
<ng-container *ngTemplateOutlet="notificationContent"></ng-container> <ng-container *ngTemplateOutlet="couponAcquisition"></ng-container>
</div> </div>
</ng-container> </ng-container>
<!-- Actions --> <!-- Actions -->
<div class="relative flex flex-col my-5 mr-6 ml-2"> <div class="relative flex flex-col my-5 mr-6 ml-2">
<!-- Indicator --> <!-- Indicator -->
<button <button class="w-6 h-6 min-h-6" mat-icon-button (click)="toggleRead(notification)"
class="w-6 h-6 min-h-6"
mat-icon-button
(click)="toggleRead(notification)"
[matTooltip]="notification.read ? 'Mark as unread' : 'Mark as read'"> [matTooltip]="notification.read ? 'Mark as unread' : 'Mark as read'">
<span <span class="w-2 h-2 rounded-full" [ngClass]="{'bg-gray-400 dark:bg-gray-500 sm:opacity-0 sm:group-hover:opacity-100': notification.read,
class="w-2 h-2 rounded-full"
[ngClass]="{'bg-gray-400 dark:bg-gray-500 sm:opacity-0 sm:group-hover:opacity-100': notification.read,
'bg-primary': !notification.read}"></span> 'bg-primary': !notification.read}"></span>
</button> </button>
<!-- Remove --> <button class="w-6 h-6 min-h-6 sm:opacity-0 sm:group-hover:opacity-100" mat-icon-button
<button (click)="delete(notification)" [matTooltip]="'Remove'" >
class="w-6 h-6 min-h-6 sm:opacity-0 sm:group-hover:opacity-100" <mat-icon class="icon-size-4" [svgIcon]="'heroicons_solid:x'"></mat-icon>
mat-icon-button
(click)="delete(notification)"
[matTooltip]="'Remove'">
<mat-icon
class="icon-size-4"
[svgIcon]="'heroicons_solid:x'"></mat-icon>
</button> </button>
</div> </div>
</div> </div>
<!-- Notification content template -->
<ng-template #notificationRedemption>
<!-- Icon -->
<ng-container>
<div
class="flex flex-shrink-0 items-center justify-center w-8 h-8 mr-4 rounded-full bg-gray-100 dark:bg-gray-700">
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_outline:credit-card'">
</mat-icon>
</div>
</ng-container>
<!-- Title, description & time -->
<div class="flex flex-col flex-auto">
<ng-container>
<div class="font-semibold line-clamp-1">Coupon Redeemed</div>
</ng-container>
<ng-container *ngIf="notification.message">
<div class="line-clamp-2" [innerHTML]="notification.message"></div>
</ng-container>
<div class="mt-2 text-sm leading-none text-secondary">
{{notification.timestamp | date:'MMM dd, h:mm a'}}
</div>
</div>
</ng-template>
<!-- Notification content template --> <!-- Notification content template -->
<ng-template #notificationContent> <ng-template #couponAcquisition>
<!-- Icon --> <!-- Icon -->
<ng-container *ngIf="notification.icon && !notification.image"> <ng-container>
<div class="flex flex-shrink-0 items-center justify-center w-8 h-8 mr-4 rounded-full bg-gray-100 dark:bg-gray-700"> <div
<mat-icon class="flex flex-shrink-0 items-center justify-center w-8 h-8 mr-4 rounded-full bg-gray-100 dark:bg-gray-700">
class="icon-size-5" <mat-icon class="icon-size-5" [svgIcon]="'heroicons_outline:credit-card'">
[svgIcon]="notification.icon">
</mat-icon> </mat-icon>
</div> </div>
</ng-container> </ng-container>
<!-- Title, description & time -->
<div class="flex flex-col flex-auto">
<ng-container>
<div class="font-semibold line-clamp-1">Coupon Claimed</div>
</ng-container>
<ng-container *ngIf="notification.message">
<div class="line-clamp-2" [innerHTML]="notification.message"></div>
</ng-container>
<div class="mt-2 text-sm leading-none text-secondary">
{{notification.timestamp | date:'MMM dd, h:mm a'}}
</div>
</div>
</ng-template>
<!-- Notification subscription template -->
<ng-template #notificationSubscription>
<!-- Image --> <!-- Image -->
<ng-container *ngIf="notification.image"> <ng-container *ngIf="notification.initiatorThumbnail">
<img <img class="flex-shrink-0 w-8 h-8 mr-4 rounded-full overflow-hidden object-cover object-center"
class="flex-shrink-0 w-8 h-8 mr-4 rounded-full overflow-hidden object-cover object-center" [src]="imageLoader(notification.initiatorThumbnail)" [alt]="'Notification image'">
[src]="notification.image"
[alt]="'Notification image'">
</ng-container> </ng-container>
<!-- Title, description & time --> <!-- Title, description & time -->
<div class="flex flex-col flex-auto"> <div class="flex flex-col flex-auto">
<ng-container *ngIf="notification.title"> <ng-container *ngIf="notification.title">
<div <div class="font-semibold line-clamp-1">New Subscriber</div>
class="font-semibold line-clamp-1"
[innerHTML]="notification.title"></div>
</ng-container> </ng-container>
<ng-container *ngIf="notification.description"> <ng-container>
<div <div class="line-clamp-2">{{notification.initiatorFirstname}}
class="line-clamp-2" {{notification.initiatorSurname}} started following you</div>
[innerHTML]="notification.description"></div>
</ng-container> </ng-container>
<div class="mt-2 text-sm leading-none text-secondary"> <div class="mt-2 text-sm leading-none text-secondary">
{{notification.time | date:'MMM dd, h:mm a'}} {{notification.timestamp | date:'MMM dd, h:mm a'}}
</div> </div>
</div> </div>
</ng-template> </ng-template>
@ -146,12 +151,11 @@
<ng-container *ngIf="!notifications || !notifications.length"> <ng-container *ngIf="!notifications || !notifications.length">
<div class="flex flex-col flex-auto items-center justify-center sm:justify-start py-12 px-8"> <div class="flex flex-col flex-auto items-center justify-center sm:justify-start py-12 px-8">
<div class="flex flex-0 items-center justify-center w-14 h-14 rounded-full bg-primary-100"> <div class="flex flex-0 items-center justify-center w-14 h-14 rounded-full bg-primary-100">
<mat-icon <mat-icon class="text-primary-500-700" [svgIcon]="'heroicons_outline:bell'"></mat-icon>
class="text-primary-500-700"
[svgIcon]="'heroicons_outline:bell'"></mat-icon>
</div> </div>
<div class="mt-5 text-2xl font-semibold tracking-tight">No notifications</div> <div class="mt-5 text-2xl font-semibold tracking-tight">No notifications</div>
<div class="w-full max-w-60 mt-1 text-md text-center text-secondary">When you have notifications, they will appear here.</div> <div class="w-full max-w-60 mt-1 text-md text-center text-secondary">When you have notifications,
they will appear here.</div>
</div> </div>
</ng-container> </ng-container>

156
src/app/layout/common/notifications/notifications.component.ts

@ -2,24 +2,27 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
import { Overlay, OverlayRef } from '@angular/cdk/overlay'; import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal'; import { TemplatePortal } from '@angular/cdk/portal';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Notification } from 'app/layout/common/notifications/notifications.types'; import { Notification } from 'app/layout/common/notifications/notifications.types';
import { NotificationsService } from 'app/layout/common/notifications/notifications.service'; import { NotificationsService } from 'app/layout/common/notifications/notifications.service';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from '@angular/fire/compat/firestore';
import { environment } from 'environments/environment';
import jwt_decode from 'jwt-decode';
import { AuthService } from 'app/core/auth/auth.service';
@Component({ @Component({
selector : 'notifications', selector: 'notifications',
templateUrl : './notifications.component.html', templateUrl: './notifications.component.html',
encapsulation : ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
exportAs : 'notifications' exportAs: 'notifications'
}) })
export class NotificationsComponent implements OnInit, OnDestroy export class NotificationsComponent implements OnInit, OnDestroy {
{
@ViewChild('notificationsOrigin') private _notificationsOrigin: MatButton; @ViewChild('notificationsOrigin') private _notificationsOrigin: MatButton;
@ViewChild('notificationsPanel') private _notificationsPanel: TemplateRef<any>; @ViewChild('notificationsPanel') private _notificationsPanel: TemplateRef<any>;
notificationCollection: AngularFirestoreCollection<any>;
notifications: Notification[]; notifications: any[];
unreadCount: number = 0; unreadCount: number = 0;
private _overlayRef: OverlayRef; private _overlayRef: OverlayRef;
private _unsubscribeAll: Subject<any> = new Subject<any>(); private _unsubscribeAll: Subject<any> = new Subject<any>();
@ -31,9 +34,13 @@ export class NotificationsComponent implements OnInit, OnDestroy
private _changeDetectorRef: ChangeDetectorRef, private _changeDetectorRef: ChangeDetectorRef,
private _notificationsService: NotificationsService, private _notificationsService: NotificationsService,
private _overlay: Overlay, private _overlay: Overlay,
private _viewContainerRef: ViewContainerRef private _viewContainerRef: ViewContainerRef,
) private db: AngularFirestore,
{ private _authService: AuthService
) {
const tokenInfo = this.getDecodedAccessToken(this._authService.relevantToken);
this.notificationCollection = db.collection("business_notifications").doc(tokenInfo.businessID).collection(tokenInfo.businessID);
} }
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
@ -43,36 +50,34 @@ export class NotificationsComponent implements OnInit, OnDestroy
/** /**
* On init * On init
*/ */
ngOnInit(): void ngOnInit(): void {
{
// Subscribe to notification changes // Subscribe to notification changes
this._notificationsService.notifications$ this.notificationCollection.valueChanges().subscribe((response) => {
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((notifications: Notification[]) => {
// Load the notifications // Load the notifications
this.notifications = notifications; this.notifications = response;
// Calculate the unread count // Calculate the unread count
this._calculateUnreadCount(); this._calculateUnreadCount();
this.notificationCollection.get().subscribe((response) => {
response.docs.forEach((e) => {
e.ref.update({ "docRef": e.id });
});
});
// Mark for check // Mark for check
this._changeDetectorRef.markForCheck(); this._changeDetectorRef.markForCheck();
}); })
} }
/** /**
* On destroy * On destroy
*/ */
ngOnDestroy(): void ngOnDestroy(): void {
{
// Unsubscribe from all subscriptions // Unsubscribe from all subscriptions
this._unsubscribeAll.next(); this._unsubscribeAll.next();
this._unsubscribeAll.complete(); this._unsubscribeAll.complete();
// Dispose the overlay // Dispose the overlay
if ( this._overlayRef ) if (this._overlayRef) {
{
this._overlayRef.dispose(); this._overlayRef.dispose();
} }
} }
@ -84,17 +89,14 @@ export class NotificationsComponent implements OnInit, OnDestroy
/** /**
* Open the notifications panel * Open the notifications panel
*/ */
openPanel(): void openPanel(): void {
{
// Return if the notifications panel or its origin is not defined // Return if the notifications panel or its origin is not defined
if ( !this._notificationsPanel || !this._notificationsOrigin ) if (!this._notificationsPanel || !this._notificationsOrigin) {
{
return; return;
} }
// Create the overlay if it doesn't exist // Create the overlay if it doesn't exist
if ( !this._overlayRef ) if (!this._overlayRef) {
{
this._createOverlay(); this._createOverlay();
} }
@ -105,39 +107,55 @@ export class NotificationsComponent implements OnInit, OnDestroy
/** /**
* Close the messages panel * Close the messages panel
*/ */
closePanel(): void closePanel(): void {
{
this._overlayRef.detach(); this._overlayRef.detach();
} }
/** /**
* Mark all notifications as read * Mark all notifications as read
*/ */
markAllAsRead(): void markAllAsRead(): void {
{
// Mark all as read // Mark all as read
this._notificationsService.markAllAsRead().subscribe();
this.notificationCollection.get().subscribe((response) => {
// Load the notifications
// this.notifications = response;
response.docs.forEach((e) => {
e.ref.update({ "read": true });
});
// // Calculate the unread count
this._calculateUnreadCount();
// Mark for check
this._changeDetectorRef.markForCheck();
})
} }
/** /**
* Toggle read status of the given notification * Toggle read status of the given notification
*/ */
toggleRead(notification: Notification): void toggleRead(notification): void {
{ this.notificationCollection.get().subscribe((response) => {
// Toggle the read status response.docs.forEach((e) => {
notification.read = !notification.read; if (e.id == notification.docRef) {
e.ref.update({ "read": !notification.read });
// Update the notification }
this._notificationsService.update(notification.id, notification).subscribe(); })
});
} }
/** /**
* Delete the given notification * Delete the given notification
*/ */
delete(notification: Notification): void delete(notification): void {
{
// Delete the notification // Delete the notification
this._notificationsService.delete(notification.id).subscribe(); this.notificationCollection.get().subscribe((response) => {
response.docs.forEach((e) => {
if (e.id == notification.docRef) {
e.ref.delete();
}
})
});
} }
/** /**
@ -146,8 +164,7 @@ export class NotificationsComponent implements OnInit, OnDestroy
* @param index * @param index
* @param item * @param item
*/ */
trackByFn(index: number, item: any): any trackByFn(index: number, item: any): any {
{
return item.id || index; return item.id || index;
} }
@ -158,39 +175,38 @@ export class NotificationsComponent implements OnInit, OnDestroy
/** /**
* Create the overlay * Create the overlay
*/ */
private _createOverlay(): void private _createOverlay(): void {
{
// Create the overlay // Create the overlay
this._overlayRef = this._overlay.create({ this._overlayRef = this._overlay.create({
hasBackdrop : true, hasBackdrop: true,
backdropClass : 'teso-backdrop-on-mobile', backdropClass: 'teso-backdrop-on-mobile',
scrollStrategy : this._overlay.scrollStrategies.block(), scrollStrategy: this._overlay.scrollStrategies.block(),
positionStrategy: this._overlay.position() positionStrategy: this._overlay.position()
.flexibleConnectedTo(this._notificationsOrigin._elementRef.nativeElement) .flexibleConnectedTo(this._notificationsOrigin._elementRef.nativeElement)
.withLockedPosition(true) .withLockedPosition(true)
.withPush(true) .withPush(true)
.withPositions([ .withPositions([
{ {
originX : 'start', originX: 'start',
originY : 'bottom', originY: 'bottom',
overlayX: 'start', overlayX: 'start',
overlayY: 'top' overlayY: 'top'
}, },
{ {
originX : 'start', originX: 'start',
originY : 'top', originY: 'top',
overlayX: 'start', overlayX: 'start',
overlayY: 'bottom' overlayY: 'bottom'
}, },
{ {
originX : 'end', originX: 'end',
originY : 'bottom', originY: 'bottom',
overlayX: 'end', overlayX: 'end',
overlayY: 'top' overlayY: 'top'
}, },
{ {
originX : 'end', originX: 'end',
originY : 'top', originY: 'top',
overlayX: 'end', overlayX: 'end',
overlayY: 'bottom' overlayY: 'bottom'
} }
@ -208,15 +224,23 @@ export class NotificationsComponent implements OnInit, OnDestroy
* *
* @private * @private
*/ */
private _calculateUnreadCount(): void private _calculateUnreadCount(): void {
{
let count = 0; let count = 0;
if ( this.notifications && this.notifications.length ) if (this.notifications && this.notifications.length) {
{
count = this.notifications.filter(notification => !notification.read).length; count = this.notifications.filter(notification => !notification.read).length;
} }
this.unreadCount = count; this.unreadCount = count;
} }
imageLoader(path: string): string {
return environment.apiURL + `followeruserdp/${path}`;
}
getDecodedAccessToken(token: string): any {
try {
return jwt_decode(token);
} catch (Error) {
return null;
}
}
} }

6
src/app/layout/common/user/user.component.html

@ -6,7 +6,7 @@
<img <img
class="w-10 h-10 rounded-full" class="w-10 h-10 rounded-full"
*ngIf="showAvatar && user.businessLogo" *ngIf="showAvatar && user.businessLogo"
[src]="imageLoader(user.businessLogo)"> [src]="imageLoader(user.businessLogo,user.handle)">
<mat-icon <mat-icon
*ngIf="!showAvatar || !user.businessLogo" *ngIf="!showAvatar || !user.businessLogo"
[svgIcon]="'heroicons_outline:user-circle'"></mat-icon> [svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
@ -28,10 +28,6 @@
<mat-icon [svgIcon]="'heroicons_outline:user-circle'"></mat-icon> <mat-icon [svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
<span>Profile</span> <span>Profile</span>
</button> </button>
<button mat-menu-item (click)="changePassword()">
<mat-icon [svgIcon]="'heroicons_outline:lock-closed'"></mat-icon>
<span class="mt-1.5 text-md "> Password</span>
</button>
<mat-divider class="my-2"></mat-divider> <mat-divider class="my-2"></mat-divider>
<button <button
mat-menu-item mat-menu-item

14
src/app/layout/common/user/user.component.ts

@ -82,10 +82,10 @@ export class UserComponent implements OnInit, OnDestroy {
} }
// Update the user // Update the user
this._userService.update({ this._userService.updateSub({
...this.user, ...this.user,
}).subscribe(); });
} }
/** /**
@ -97,13 +97,17 @@ export class UserComponent implements OnInit, OnDestroy {
profile(): void { profile(): void {
this._router.navigate(['/profile']); this._router.navigate(['/profile']);
} }
imageLoader(path: string): string { imageLoader(path: string, handle = false): string {
if (handle) {
return path;
} else {
return environment.apiURL + `shoplogo/${path}`; return environment.apiURL + `shoplogo/${path}`;
} }
}
changePassword() { changePassword() {
const dialogRef = this.dialog.open(SettingsComponent,{ const dialogRef = this.dialog.open(SettingsComponent, {
hasBackdrop: true, hasBackdrop: true,
disableClose:true, disableClose: true,
}) })
} }
} }

2
src/app/layout/layouts/vertical/compact/compact.component.html

@ -22,7 +22,7 @@
<div class="flex items-center pl-2 ml-auto space-x-0.5 sm:space-x-2"> <div class="flex items-center pl-2 ml-auto space-x-0.5 sm:space-x-2">
<!-- <languages></languages> --> <!-- <languages></languages> -->
<!-- <teso-fullscreen class="hidden md:block"></teso-fullscreen> --> <!-- <teso-fullscreen class="hidden md:block"></teso-fullscreen> -->
<goldcoins></goldcoins> <!-- <goldcoins></goldcoins> -->
<!-- <messages></messages> --> <!-- <messages></messages> -->
<notifications></notifications> <notifications></notifications>
<!-- <button class="lg:hidden" mat-icon-button (click)="quickChat.toggle()"> <!-- <button class="lg:hidden" mat-icon-button (click)="quickChat.toggle()">

5
src/app/models/businessModel.ts

@ -1,10 +1,12 @@
import { ProfileImage } from "./generalModel";
export interface Finance { export interface Finance {
businessID: string; businessID: string;
gold: number; gold: number;
} }
export interface TesoBusinessDetail { export interface TesoBusinessDetail {
businessId?: string, businessId?: string,
Handle?: string, handle?: boolean,
businessName?: string, businessName?: string,
businessEmail?: string, businessEmail?: string,
businessTin?: string, businessTin?: string,
@ -17,6 +19,7 @@ export interface TesoBusinessDetail {
businessDigitalAddress?: string, businessDigitalAddress?: string,
businessLatitude?: string, businessLatitude?: string,
businessLongitude?: string, businessLongitude?: string,
image?:ProfileImage,
} }
export interface BusinessCategory { export interface BusinessCategory {

52
src/app/models/dashSummary.ts

@ -0,0 +1,52 @@
export interface DashSummary {
summary:Summary;
views:ProductViews[];
recent:Transactions[];
visitorsCurrent:MonthVisits[];
visitorsPrevious:MonthVisits[];
redemptions:MonthRates[];
visits:MonthRates[];
sales:MonthRates[];
}
export interface Summary {
totalVisits: number;
couponRedeemed: number;
totalSales: number;
totalFreebie: number;
totalProximity: number;
totalDiscount: number;
visitLastMonth:number;
redemptionsLastMonth:number;
salesLastMonth:number;
}
export interface Transactions {
userGuid: String;
firstname: String;
surname: String;
timestamp: Date;
productName: String;
countId: number;
worth: number;
paid: number
}
export interface ProductViews {
userGuid: String;
firstname: String;
surname: String;
timesViewed: number;
pname: String;
productId: String;
picture: String;
}
export interface MonthVisits{
evisits:number;
inshop:number;
month:number;
year:number;
}
export interface MonthRates{
day:number;
rate:number;
}

5
src/app/models/generalModel.ts

@ -4,3 +4,8 @@ export interface ProductUpload {
imageSRC:string; imageSRC:string;
id?:string; id?:string;
} }
export interface ProfileImage {
file?: File;
imageSRC?:string;
id?:string;
}

6
src/app/pages/admin/Coupons/Active/active-coupons.component.ts

@ -27,7 +27,7 @@ export class ActiveCouponsComponent implements OnInit, AfterViewInit {
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
private _unsubscribeAll: Subject<any> = new Subject<any>(); private _unsubscribeAll: Subject<any> = new Subject<any>();
constructor(private _tesoMediaWatcherService: tesoMediaWatcherService, private confirmBoxEvokeService: ConfirmBoxEvokeService,public dialog: MatDialog, constructor(private _tesoMediaWatcherService: tesoMediaWatcherService, private confirmBoxEvokeService: ConfirmBoxEvokeService, public dialog: MatDialog,
private _couponsService: CouponsService) { } private _couponsService: CouponsService) { }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@ -62,12 +62,12 @@ export class ActiveCouponsComponent implements OnInit, AfterViewInit {
}); });
}); });
} }
showDetails(item:CouponsModel){ showDetails(item: CouponsModel) {
const dialogRef = this.dialog.open(DetailsDialogComponent, { const dialogRef = this.dialog.open(DetailsDialogComponent, {
disableClose: true, disableClose: true,
hasBackdrop: true, hasBackdrop: true,
data: { coupon: item }, data: { coupon: item },
width: "750px"
}); });
} }
} }

1
src/app/pages/admin/Coupons/DetailsDialog/details-dialog.component.ts

@ -60,6 +60,7 @@ export class DetailsDialogComponent implements OnInit {
this.products = d; this.products = d;
this.selectedProduct = this.products.find((p)=> p.productName == this.newCoupon.targetProduct); this.selectedProduct = this.products.find((p)=> p.productName == this.newCoupon.targetProduct);
}); });
console.log(this.newCoupon) console.log(this.newCoupon)
} }

4
src/app/pages/admin/Coupons/Inactive/inactive-coupons.component.html

@ -93,7 +93,7 @@
</td> </td>
</ng-container> </ng-container>
<!-- Amount --> <!-- Amount -->
<ng-container matColumnDef="actions"> <!-- <ng-container matColumnDef="actions">
<th mat-header-cell mat-sort-header *matHeaderCellDef> <th mat-header-cell mat-sort-header *matHeaderCellDef>
Actions Actions
</th> </th>
@ -104,7 +104,7 @@
</div> </div>
</td> </td>
</ng-container> </ng-container> -->
<!-- Footer --> <!-- Footer -->
<ng-container matColumnDef="recentOrdersTableFooter"> <ng-container matColumnDef="recentOrdersTableFooter">
<td class="py-6 px-0 border-0" mat-footer-cell *matFooterCellDef colspan="6"> <td class="py-6 px-0 border-0" mat-footer-cell *matFooterCellDef colspan="6">

2
src/app/pages/admin/Coupons/Inactive/inactive-coupons.component.ts

@ -12,7 +12,7 @@ import { MatTableDataSource } from '@angular/material/table';
}) })
export class InactiveCouponsComponent implements OnInit,AfterViewInit { export class InactiveCouponsComponent implements OnInit,AfterViewInit {
@Input() couponsDataSource: MatTableDataSource<any>; @Input() couponsDataSource: MatTableDataSource<any>;
couponsTableColumns: string[] = ['product', 'quantity', 'type', 'condition', 'range', 'claimed', 'status', 'expiration', 'actions']; couponsTableColumns: string[] = ['product', 'quantity', 'type', 'condition', 'range', 'claimed', 'status', 'expiration'];//, 'actions'];
@ViewChild('couponsTable', { read: MatSort }) couponsTableMatSort: MatSort; @ViewChild('couponsTable', { read: MatSort }) couponsTableMatSort: MatSort;
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
constructor() { } constructor() { }

2
src/app/pages/admin/Coupons/coupons.component.html

@ -8,6 +8,8 @@
<div class="text-3xl font-semibold tracking-tight leading-8">Coupons</div> <div class="text-3xl font-semibold tracking-tight leading-8">Coupons</div>
<div class="font-medium tracking-tight text-secondary">Here is a list of all your coupons on Teso ({{allcoupons.length}}) <div class="font-medium tracking-tight text-secondary">Here is a list of all your coupons on Teso ({{allcoupons.length}})
</div> </div>
<div class="font-small tracking-tight text-secondary" *ngIf="allcoupons.length > 0">({{activecouponsData.data.length}}) active coupons, ({{inactivecouponsData.data.length}}) inactive coupons
</div>
</div> </div>
<div class="flex flex-shrink-0 items-center mt-6 sm:mt-0 sm:ml-4"> <div class="flex flex-shrink-0 items-center mt-6 sm:mt-0 sm:ml-4">

297
src/app/pages/admin/Dashboard/dashboard.component.html

@ -6,14 +6,16 @@
<div class="flex items-center justify-between w-full"> <div class="flex items-center justify-between w-full">
<div> <div>
<div class="text-3xl font-semibold tracking-tight leading-8">Analytics dashboard</div> <div class="text-3xl font-semibold tracking-tight leading-8">Analytics dashboard</div>
<div class="font-medium tracking-tight text-secondary">Monitor metrics, check statistics and review performance on Teso</div> <div class="font-medium tracking-tight text-secondary">Monitor metrics, check statistics and review
performance on Teso</div>
</div> </div>
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 w-full mt-8"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 w-full mt-8">
<!-- Visitors overview --> <!-- Visitors overview -->
<div class="sm:col-span-2 lg:col-span-3 light flex flex-col flex-auto bg-card shadow rounded-2xl overflow-hidden"> <div
class="sm:col-span-2 lg:col-span-3 light flex flex-col flex-auto bg-card shadow rounded-2xl overflow-hidden">
<div class="flex items-center justify-between mt-10 ml-10 mr-6 sm:mr-10"> <div class="flex items-center justify-between mt-10 ml-10 mr-6 sm:mr-10">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="mr-4 text-2xl md:text-3xl font-semibold tracking-tight leading-7">Visitors Overview <div class="mr-4 text-2xl md:text-3xl font-semibold tracking-tight leading-7">Visitors Overview
@ -21,10 +23,13 @@
<div class="font-medium text-secondary">Number of unique visitors</div> <div class="font-medium text-secondary">Number of unique visitors</div>
</div> </div>
<div class="ml-2"> <div class="ml-2">
<mat-button-toggle-group class="hidden sm:inline-flex border-none space-x-1" value="this-year" #visitorsYearSelector="matButtonToggleGroup"> <mat-button-toggle-group class="hidden sm:inline-flex border-none space-x-1" value="this-year"
<mat-button-toggle class="px-1.5 rounded-full overflow-hidden border-none font-medium" value="last-year">Last Year #visitorsYearSelector="matButtonToggleGroup">
<mat-button-toggle class="px-1.5 rounded-full overflow-hidden border-none font-medium"
value="last-year" (click)="toggle()">Last Year
</mat-button-toggle> </mat-button-toggle>
<mat-button-toggle class="px-1.5 rounded-full overflow-hidden border-none font-medium" value="this-year">This Year <mat-button-toggle class="px-1.5 rounded-full overflow-hidden border-none font-medium"
value="this-year" (click)="toggle()">This Year
</mat-button-toggle> </mat-button-toggle>
</mat-button-toggle-group> </mat-button-toggle-group>
<div class="sm:hidden"> <div class="sm:hidden">
@ -32,111 +37,151 @@
<mat-icon [svgIcon]="'heroicons_outline:dots-vertical'"></mat-icon> <mat-icon [svgIcon]="'heroicons_outline:dots-vertical'"></mat-icon>
</button> </button>
<mat-menu #visitorsMenu="matMenu"> <mat-menu #visitorsMenu="matMenu">
<button mat-menu-item>This Year</button> <button mat-menu-item (click)="toggle()">This Year</button>
<button mat-menu-item>Last Year</button> <button mat-menu-item (click)="toggle()">Last Year</button>
</mat-menu> </mat-menu>
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col flex-auto h-80"> <div class="flex flex-col flex-auto" *ngIf="mainChartRendered">
<apx-chart class="flex-auto w-full h-full" [chart]="chartVisitors.chart" [colors]="chartVisitors.colors" [dataLabels]="chartVisitors.dataLabels" [fill]="chartVisitors.fill" [grid]="chartVisitors.grid" [series]="chartVisitors.series[visitorsYearSelector.value]" <p-chart type="bar" [data]="accountBalanceOptions" [options]="basicOptions">
[stroke]="chartVisitors.stroke" [tooltip]="chartVisitors.tooltip" [xaxis]="chartVisitors.xaxis" [yaxis]="chartVisitors.yaxis">
</apx-chart> </p-chart>
</div> </div>
<div class="h-90" style="display: block;justify-content: space-around;" *ngIf="!mainChartRendered">
<center>
<img src="assets/icons/notransaction.png" style="max-width: 400px;" />
<h6 style="color: gray;" class="whitespace-wrap">Sorry, you only get to see the nice graphs
after your very first transaction with a customer on Teso!!</h6>
</center>
</div> </div>
<!-- Conversions --> </div>
<!-- Redemptions -->
<div class="sm:col-span-2 lg:col-span-1 flex flex-col flex-auto bg-card shadow rounded-2xl overflow-hidden"> <div class="sm:col-span-2 lg:col-span-1 flex flex-col flex-auto bg-card shadow rounded-2xl overflow-hidden">
<div class="flex items-start justify-between m-6 mb-0"> <div class="flex items-start justify-between m-6 mb-0">
<div class="text-lg font-medium tracking-tight leading-6 truncate">Coupon Redemptions</div> <div class="text-lg font-medium tracking-tight leading-6 truncate">Coupon Redemptions</div>
<div class="ml-2"> <div class="ml-2">
<button class="h-6 min-h-6 px-2 rounded-full bg-hover" mat-button [matMenuTriggerFor]="conversionMenu"> <button class="h-6 min-h-6 px-2 rounded-full bg-hover" mat-button>
<span class="font-medium text-sm text-secondary">30 days</span> <span class="font-medium text-sm text-secondary">This month</span>
</button> </button>
<mat-menu #conversionMenu="matMenu">
<button mat-menu-item>30 days</button>
<button mat-menu-item>3 months</button>
<button mat-menu-item>9 months</button>
</mat-menu>
</div> </div>
</div> </div>
<div class="flex flex-col lg:flex-row lg:items-center mx-6 mt-3"> <div class="flex flex-col lg:flex-row lg:items-center mx-6 mt-3">
<div class="text-7xl font-bold tracking-tighter leading-tight">{{data.conversions.amount | number:'1.0-0'}} <div class="text-7xl font-bold tracking-tighter leading-tight">{{data.summary.couponRedeemed |
number:'1.0-0'}}
</div> </div>
<div class="flex lg:flex-col lg:ml-3"> <div class="flex lg:flex-col lg:ml-3">
<mat-icon class="icon-size-5 text-red-500" [svgIcon]="'heroicons_solid:trending-down'"> <mat-icon class="icon-size-5 text-red-500" [svgIcon]="'heroicons_solid:trending-down'"
*ngIf="redemptionRate < 0">
</mat-icon> </mat-icon>
<div class="flex items-center ml-1 lg:ml-0 lg:mt-0.5 text-md leading-none whitespace-nowrap text-secondary"> <mat-icon class="icon-size-5 text-green-500" [svgIcon]="'heroicons_solid:trending-up'"
<span class="font-medium text-red-500">2%</span> *ngIf="redemptionRate > 0">
<span class="ml-1">below target</span> </mat-icon>
<mat-icon class="icon-size-5 text-gray-500" [svgIcon]="'heroicons_solid:minus'"
*ngIf="redemptionRate == 0">
</mat-icon>
<div
class="flex items-center ml-1 lg:ml-0 lg:mt-0.5 text-md leading-none whitespace-nowrap text-secondary">
<span class="font-medium text-red-500" *ngIf="redemptionRate < 0">{{redemptionRate | number
: '1.2-2'}}%</span>
<span class="font-medium text-green-500" *ngIf="redemptionRate > 0">{{redemptionRate |
number : '1.2-2'}}%</span>
<span class="ml-1 font-medium text-red-500" *ngIf="redemptionRate < 0">decrement</span>
<span class="ml-1 font-medium text-green-500" *ngIf="redemptionRate > 0">increment</span>
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col flex-auto h-20"> <div class="flex flex-col flex-auto h-20">
<apx-chart class="flex-auto w-full h-full" [chart]="chartConversions.chart" [colors]="chartConversions.colors" [series]="chartConversions.series" [stroke]="chartConversions.stroke" [tooltip]="chartConversions.tooltip" [xaxis]="chartConversions.xaxis" <apx-chart class="flex-auto w-full h-full" [chart]="chartRedeemptions.chart"
[yaxis]="chartConversions.yaxis"></apx-chart> [colors]="chartRedeemptions.colors" [series]="chartRedeemptions.series"
[stroke]="chartRedeemptions.stroke" [tooltip]="chartRedeemptions.tooltip"
[xaxis]="chartRedeemptions.xaxis" [yaxis]="chartRedeemptions.yaxis" *ngIf="redemptionsRendered">
</apx-chart>
</div> </div>
</div> </div>
<!-- Impressions --> <!-- Visits -->
<div class="flex flex-col flex-auto bg-card shadow rounded-2xl overflow-hidden"> <div class="flex flex-col flex-auto bg-card shadow rounded-2xl overflow-hidden">
<div class="flex items-start justify-between m-6 mb-0"> <div class="flex items-start justify-between m-6 mb-0">
<div class="text-lg font-medium tracking-tight leading-6 truncate">Total Sales</div> <div class="text-lg font-medium tracking-tight leading-6 truncate">Visits</div>
<div class="ml-2"> <div class="ml-2">
<button class="h-6 min-h-6 px-2 rounded-full bg-hover" mat-button [matMenuTriggerFor]="impressionsMenu"> <button class="h-6 min-h-6 px-2 rounded-full bg-hover" mat-button>
<span class="font-medium text-sm text-secondary">30 days</span> <span class="font-medium text-sm text-secondary">This month</span>
</button> </button>
<mat-menu #impressionsMenu="matMenu">
<button mat-menu-item>30 days</button>
<button mat-menu-item>3 months</button>
<button mat-menu-item>9 months</button>
</mat-menu>
</div> </div>
</div> </div>
<div class="flex flex-col lg:flex-row lg:items-center mx-6 mt-3"> <div class="flex flex-col lg:flex-row lg:items-center mx-6 mt-3">
<div class="text-7xl font-bold tracking-tighter leading-tight">{{data.impressions.amount | number:'1.0-0'}} <div class="text-7xl font-bold tracking-tighter leading-tight">{{data.summary.totalVisits |
number:'1.0-0'}}
</div> </div>
<div class="flex lg:flex-col lg:ml-3"> <div class="flex lg:flex-col lg:ml-3">
<mat-icon class="icon-size-5 text-red-500" [svgIcon]="'heroicons_solid:trending-down'"> <mat-icon class="icon-size-5 text-red-500" [svgIcon]="'heroicons_solid:trending-down'"
*ngIf="visitsRate < 0">
</mat-icon> </mat-icon>
<div class="flex items-center ml-1 lg:ml-0 lg:mt-0.5 text-md leading-none whitespace-nowrap text-secondary"> <mat-icon class="icon-size-5 text-green-500" [svgIcon]="'heroicons_solid:trending-up'"
<span class="font-medium text-red-500">4%</span> *ngIf="visitsRate > 0">
<span class="ml-1">below target</span> </mat-icon>
<mat-icon class="icon-size-5 text-gray-500" [svgIcon]="'heroicons_solid:minus'"
*ngIf="visitsRate == 0">
</mat-icon>
<div
class="flex items-center ml-1 lg:ml-0 lg:mt-0.5 text-md leading-none whitespace-nowrap text-secondary">
<span class="font-medium text-red-500" *ngIf="visitsRate < 0">{{visitsRate | number :
'1.2-2'}}%</span>
<span class="font-medium text-green-500" *ngIf="visitsRate > 0">{{visitsRate | number :
'1.2-2'}}%</span>
<span class="ml-1 font-medium text-red-500" *ngIf="visitsRate < 0">decrement</span>
<span class="ml-1 font-medium text-green-500" *ngIf="visitsRate > 0">increment</span>
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col flex-auto h-20"> <div class="flex flex-col flex-auto h-20">
<apx-chart class="flex-auto w-full h-full" [chart]="chartImpressions.chart" [colors]="chartImpressions.colors" [series]="chartImpressions.series" [stroke]="chartImpressions.stroke" [tooltip]="chartImpressions.tooltip" [xaxis]="chartImpressions.xaxis" <apx-chart class="flex-auto w-full h-full" [chart]="chartVisits.chart" [colors]="chartVisits.colors"
[yaxis]="chartImpressions.yaxis"></apx-chart> [series]="chartVisits.series" [stroke]="chartVisits.stroke" [tooltip]="chartVisits.tooltip"
[xaxis]="chartVisits.xaxis" [yaxis]="chartVisits.yaxis" *ngIf="visitsRendered"></apx-chart>
</div> </div>
</div> </div>
<!-- Visits --> <!-- Sales -->
<div class="flex flex-col flex-auto bg-card shadow rounded-2xl overflow-hidden"> <div class="flex flex-col flex-auto bg-card shadow rounded-2xl overflow-hidden">
<div class="flex items-start justify-between m-6 mb-0"> <div class="flex items-start justify-between m-6 mb-0">
<div class="text-lg font-medium tracking-tight leading-6 truncate">Visits</div> <div class="text-lg font-medium tracking-tight leading-6 truncate">Total Sales</div>
<div class="ml-2"> <div class="ml-2">
<button class="h-6 min-h-6 px-2 rounded-full bg-hover" mat-button [matMenuTriggerFor]="impressionsMenu"> <button class="h-6 min-h-6 px-2 rounded-full bg-hover" mat-button>
<span class="font-medium text-sm text-secondary">30 days</span> <span class="font-medium text-sm text-secondary">This month</span>
</button> </button>
<mat-menu #impressionsMenu="matMenu">
<button mat-menu-item>30 days</button>
<button mat-menu-item>3 months</button>
<button mat-menu-item>9 months</button>
</mat-menu>
</div> </div>
</div> </div>
<div class="flex flex-col lg:flex-row lg:items-center mx-6 mt-3"> <div class="flex flex-col lg:flex-row lg:items-center mx-6 mt-3">
<div class="text-7xl font-bold tracking-tighter leading-tight">{{data.visits.amount | number:'1.0-0'}} <div class="text-7xl font-bold tracking-tighter leading-tight">{{data.summary.totalSales |
number:'1.0-0'}}
</div> </div>
<div class="flex lg:flex-col lg:ml-3"> <div class="flex lg:flex-col lg:ml-3">
<mat-icon class="icon-size-5 text-red-500" [svgIcon]="'heroicons_solid:trending-down'"> <mat-icon class="icon-size-5 text-red-500" [svgIcon]="'heroicons_solid:trending-down'"
*ngIf="salesRate < 0">
</mat-icon>
<mat-icon class="icon-size-5 text-green-500" [svgIcon]="'heroicons_solid:trending-up'"
*ngIf="salesRate > 0">
</mat-icon> </mat-icon>
<div class="flex items-center ml-1 lg:ml-0 lg:mt-0.5 text-md leading-none whitespace-nowrap text-secondary"> <mat-icon class="icon-size-5 text-gray-500" [svgIcon]="'heroicons_solid:minus'"
<span class="font-medium text-red-500">4%</span> *ngIf="salesRate == 0">
<span class="ml-1">below target</span> </mat-icon>
<div
class="flex items-center ml-1 lg:ml-0 lg:mt-0.5 text-md leading-none whitespace-nowrap text-secondary">
<span class="font-medium text-red-500" *ngIf="salesRate < 0">{{salesRate | number :
'1.2-2'}}%</span>
<span class="font-medium text-green-500" *ngIf="salesRate > 0">{{salesRate | number :
'1.2-2'}}%</span>
<span class="ml-1 font-medium text-red-500" *ngIf="salesRate < 0">decrement</span>
<span class="ml-1 font-medium text-green-500" *ngIf="salesRate > 0">increment</span>
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col flex-auto h-20"> <div class="flex flex-col flex-auto h-20">
<apx-chart class="flex-auto w-full h-full" [chart]="chartVisits.chart" [colors]="chartVisits.colors" [series]="chartVisits.series" [stroke]="chartVisits.stroke" [tooltip]="chartVisits.tooltip" [xaxis]="chartVisits.xaxis" [yaxis]="chartVisits.yaxis"></apx-chart> <apx-chart class="flex-auto w-full h-full" [chart]="chartImpressions.chart"
[colors]="chartImpressions.colors" [series]="chartImpressions.series"
[stroke]="chartImpressions.stroke" [tooltip]="chartImpressions.tooltip"
[xaxis]="chartImpressions.xaxis" [yaxis]="chartImpressions.yaxis" *ngIf="salesRendered">
</apx-chart>
</div> </div>
</div> </div>
</div> </div>
@ -146,10 +191,11 @@
<div class="xl:col-span-2 flex flex-col flex-auto bg-card shadow rounded-2xl overflow-hidden"> <div class="xl:col-span-2 flex flex-col flex-auto bg-card shadow rounded-2xl overflow-hidden">
<div class="p-6"> <div class="p-6">
<div class="mr-4 text-lg font-medium tracking-tight leading-6 truncate">Recent transactions</div> <div class="mr-4 text-lg font-medium tracking-tight leading-6 truncate">Recent transactions</div>
<!-- <div class="text-secondary font-medium">1 pending, 4 completed</div> --> <div class="text-secondary font-medium">Top 10 recent transactions</div>
</div> </div>
<div class="overflow-x-auto mx-6"> <div class="overflow-x-auto mx-6">
<table class="w-full bg-transparent" mat-table matSort [dataSource]="recentTransactionsDataSource" [trackBy]="trackByFn" #recentTransactionsTable> <table class="w-full bg-transparent" mat-table matSort [dataSource]="recentTransactionsDataSource"
[trackBy]="trackByFn" #recentTransactionsTable>
<!-- Transaction ID --> <!-- Transaction ID -->
<ng-container matColumnDef="transactionId"> <ng-container matColumnDef="transactionId">
@ -158,7 +204,7 @@
</th> </th>
<td mat-cell *matCellDef="let transaction"> <td mat-cell *matCellDef="let transaction">
<span class="pr-6 font-medium text-sm text-secondary whitespace-nowrap"> <span class="pr-6 font-medium text-sm text-secondary whitespace-nowrap">
{{transaction.transactionId}} {{transaction.countId}}
</span> </span>
</td> </td>
</ng-container> </ng-container>
@ -170,7 +216,7 @@
</th> </th>
<td mat-cell *matCellDef="let transaction"> <td mat-cell *matCellDef="let transaction">
<span class="pr-6 whitespace-nowrap"> <span class="pr-6 whitespace-nowrap">
{{transaction.date | date:'MMM dd, y'}} {{transaction.timestamp | date:'MMM dd, y'}}
</span> </span>
</td> </td>
</ng-container> </ng-container>
@ -182,58 +228,58 @@
</th> </th>
<td mat-cell *matCellDef="let transaction"> <td mat-cell *matCellDef="let transaction">
<span class="pr-6 whitespace-nowrap"> <span class="pr-6 whitespace-nowrap">
{{transaction.name}} {{transaction.firstname}} {{transaction.surname}}
</span> </span>
</td> </td>
</ng-container> </ng-container>
<!-- Product -->
<!-- Amount --> <ng-container matColumnDef="productName">
<ng-container matColumnDef="discount">
<th mat-header-cell mat-sort-header *matHeaderCellDef> <th mat-header-cell mat-sort-header *matHeaderCellDef>
Amount Discounted Item
</th> </th>
<td mat-cell *matCellDef="let transaction"> <td mat-cell *matCellDef="let transaction">
<span class="pr-6 font-medium whitespace-nowrap"> <span class="pr-6 font-medium whitespace-nowrap">
{{(transaction.amount - 900) | currency:'GH¢'}} {{transaction.productName }}
</span> </span>
</td> </td>
</ng-container> </ng-container>
<!-- Amount --> <!-- Amount -->
<ng-container matColumnDef="amount"> <ng-container matColumnDef="discount">
<th mat-header-cell mat-sort-header *matHeaderCellDef> <th mat-header-cell mat-sort-header *matHeaderCellDef>
Amount Paid Amount Discounted
</th> </th>
<td mat-cell *matCellDef="let transaction"> <td mat-cell *matCellDef="let transaction">
<span class="pr-6 font-medium whitespace-nowrap"> <span class="pr-6 font-medium whitespace-nowrap">
{{transaction.amount | currency:'GH¢'}} {{(transaction.worth) | currency:'GH¢'}}
</span> </span>
</td> </td>
</ng-container> </ng-container>
<!-- Status --> <!-- Amount -->
<ng-container matColumnDef="status"> <ng-container matColumnDef="amount">
<th mat-header-cell mat-sort-header *matHeaderCellDef> <th mat-header-cell mat-sort-header *matHeaderCellDef>
Status Amount Paid
</th> </th>
<td mat-cell *matCellDef="let transaction"> <td mat-cell *matCellDef="let transaction">
<span class="inline-flex items-center font-bold text-xs px-2.5 py-0.5 rounded-full tracking-wide uppercase" [ngClass]="{'bg-red-200 text-red-800 dark:bg-red-600 dark:text-red-50': transaction.status === 'pending', <span class="pr-6 font-medium whitespace-nowrap">
'bg-green-200 text-green-800 dark:bg-green-600 dark:text-green-50': transaction.status === 'completed'}"> {{transaction.paid | currency:'GH¢'}}
<span class="leading-relaxed whitespace-nowrap">{{transaction.status}}</span>
</span> </span>
</td> </td>
</ng-container> </ng-container>
<!-- Footer -->
<!-- Footer
<ng-container matColumnDef="recentOrdersTableFooter"> <ng-container matColumnDef="recentOrdersTableFooter">
<td class="py-6 px-0 border-0" mat-footer-cell *matFooterCellDef colspan="6"> <td class="py-6 px-0 border-0" mat-footer-cell *matFooterCellDef colspan="6">
<button mat-stroked-button>See all transactions</button> <button mat-stroked-button>See all transactions</button>
</td> </td>
</ng-container> </ng-container> -->
<tr mat-header-row *matHeaderRowDef="recentTransactionsTableColumns"></tr> <tr mat-header-row *matHeaderRowDef="recentTransactionsTableColumns"></tr>
<tr class="order-row h-16" mat-row *matRowDef="let row; columns: recentTransactionsTableColumns;"></tr> <tr class="order-row h-16" mat-row
<tr class="h-16 border-0" mat-footer-row *matFooterRowDef="['recentOrdersTableFooter']"></tr> *matRowDef="let row; columns: recentTransactionsTableColumns;"></tr>
<!-- <tr class="h-16 border-0" mat-footer-row *matFooterRowDef="['recentOrdersTableFooter']"></tr> -->
</table> </table>
</div> </div>
</div> </div>
@ -245,7 +291,7 @@
<div class="mr-4 text-lg font-medium tracking-tight leading-6 truncate" *ngIf="couponStats"> <div class="mr-4 text-lg font-medium tracking-tight leading-6 truncate" *ngIf="couponStats">
Coupon Statistics</div> Coupon Statistics</div>
<div class="mr-4 text-lg font-medium tracking-tight leading-6 truncate" *ngIf="!couponStats"> <div class="mr-4 text-lg font-medium tracking-tight leading-6 truncate" *ngIf="!couponStats">
Coupon Views</div> Product Views</div>
<!-- <div class="text-secondary font-medium">Monthly budget summary</div> --> <!-- <div class="text-secondary font-medium">Monthly budget summary</div> -->
</div> </div>
<div class="ml-auto -mt-2 -mr-2"> <div class="ml-auto -mt-2 -mr-2">
@ -256,65 +302,118 @@
</div> </div>
</div> </div>
<div class="mt-6" *ngIf="!couponStats"> <div class="mt-6" *ngIf="!couponStats">
The following Teso users have shown interest in some of your coupons. Click to further engage them by sending them personalized coupons The following Teso users have shown interest in some of your products. Click to further engage them
by sending them personalized coupons
</div> </div>
<div class="mt-6" *ngIf="couponStats"> <div class="mt-6" *ngIf="couponStats">
The chart below depicts your shops top performing types of coupons The chart below depicts your shops top performing types of coupons
</div> </div>
<div class="my-8 space-y-8"> <div class="my-8 space-y-8" *ngIf="couponStats">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex items-center justify-center w-14 h-14 rounded bg-red-100 text-red-800 dark:bg-red-600 dark:text-red-50"> <div
class="flex items-center justify-center w-14 h-14 rounded bg-red-100 text-red-800 dark:bg-red-600 dark:text-red-50">
<mat-icon class="text-current" [svgIcon]="'heroicons_outline:credit-card'"></mat-icon> <mat-icon class="text-current" [svgIcon]="'heroicons_outline:credit-card'"></mat-icon>
</div> </div>
<div class="flex-auto ml-4 leading-none"> <div class="flex-auto ml-4 leading-none">
<div class="text-sm font-medium text-secondary">Freebies</div> <div class="text-sm font-medium text-secondary">Freebies</div>
<div class="mt-2 font-medium text-2xl">{{dataE.budget.expenses | currency:'GH¢'}}</div> <div class="mt-2 font-medium text-2xl">{{financialStats.totalFreebie | currency:'GH¢'}}
<mat-progress-bar class="mt-3 rounded-full" [color]="'warn'" [mode]="'determinate'" [value]="(dataE.budget.expenses * 100) / dataE.budget.expensesLimit"> </div>
<mat-progress-bar class="mt-3 rounded-full" [color]="'warn'" [mode]="'determinate'"
[value]="freebieRate">
</mat-progress-bar> </mat-progress-bar>
</div> </div>
<div class="flex items-end justify-end min-w-18 mt-auto ml-6"> <div class="flex items-end justify-end min-w-18 mt-auto ml-6">
<div class="text-lg leading-none">2.6%</div> <div class="text-lg leading-none">{{freebieRate | number : '1.2-2'}}%</div>
<mat-icon class="text-green-600 icon-size-4 ml-1" [svgIcon]="'heroicons_solid:arrow-narrow-down'"></mat-icon> <mat-icon class="text-green-600 icon-size-4 ml-1"
[svgIcon]="'heroicons_solid:arrow-narrow-down'"></mat-icon>
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex items-center justify-center w-14 h-14 rounded bg-indigo-100 text-indigo-800 dark:bg-indigo-600 dark:text-indigo-50"> <div
class="flex items-center justify-center w-14 h-14 rounded bg-indigo-100 text-indigo-800 dark:bg-indigo-600 dark:text-indigo-50">
<mat-icon class="text-current" [svgIcon]="'heroicons_outline:cash'"></mat-icon> <mat-icon class="text-current" [svgIcon]="'heroicons_outline:cash'"></mat-icon>
</div> </div>
<div class="flex-auto ml-4 leading-none"> <div class="flex-auto ml-4 leading-none">
<div class="text-sm font-medium text-secondary">Discount</div> <div class="text-sm font-medium text-secondary">Discount</div>
<div class="mt-2 font-medium text-2xl">{{dataE.budget.savings | currency:'GH¢'}}</div> <div class="mt-2 font-medium text-2xl">{{financialStats.totalDiscount | currency:'GH¢'}}
<mat-progress-bar class="mt-3 rounded-full" [mode]="'determinate'" [value]="(dataE.budget.savings * 100) / dataE.budget.savingsGoal"> </div>
<mat-progress-bar class="mt-3 rounded-full" [mode]="'determinate'"
[value]="discountRate">
</mat-progress-bar> </mat-progress-bar>
</div> </div>
<div class="flex items-end justify-end min-w-18 mt-auto ml-6"> <div class="flex items-end justify-end min-w-18 mt-auto ml-6">
<div class="text-lg leading-none">12.7%</div> <div class="text-lg leading-none">{{discountRate | number : '1.2-2'}}%</div>
<mat-icon class="text-red-600 icon-size-4 ml-1" [svgIcon]="'heroicons_solid:arrow-narrow-up'"></mat-icon> <mat-icon class="text-red-600 icon-size-4 ml-1"
[svgIcon]="'heroicons_solid:arrow-narrow-up'"></mat-icon>
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex items-center justify-center w-14 h-14 rounded bg-teal-100 text-teal-800 dark:bg-teal-600 dark:text-teal-50"> <div
class="flex items-center justify-center w-14 h-14 rounded bg-teal-100 text-teal-800 dark:bg-teal-600 dark:text-teal-50">
<mat-icon class="text-current" [svgIcon]="'heroicons_outline:light-bulb'"></mat-icon> <mat-icon class="text-current" [svgIcon]="'heroicons_outline:light-bulb'"></mat-icon>
</div> </div>
<div class="flex-auto ml-4 leading-none"> <div class="flex-auto ml-4 leading-none">
<div class="text-sm font-medium text-secondary">Proximity Coupons</div> <div class="text-sm font-medium text-secondary">Proximity Coupons</div>
<div class="mt-2 font-medium text-2xl">{{dataE.budget.bills | currency:'GH¢'}}</div> <div class="mt-2 font-medium text-2xl">{{financialStats.totalProximity |
<mat-progress-bar class="mt-3 rounded-full" [mode]="'determinate'" [value]="(dataE.budget.bills * 100) / dataE.budget.billsLimit"></mat-progress-bar> currency:'GH¢'}}</div>
<mat-progress-bar class="mt-3 rounded-full" [mode]="'determinate'"
[value]="proximityRate">
</mat-progress-bar>
</div> </div>
<div class="flex items-end justify-end min-w-18 mt-auto ml-6"> <div class="flex items-end justify-end min-w-18 mt-auto ml-6">
<div class="text-lg leading-none">105.7%</div> <div class="text-lg leading-none">{{proximityRate | number : '1.2-2'}}%</div>
<mat-icon class="text-red-600 icon-size-4 ml-1" [svgIcon]="'heroicons_solid:arrow-narrow-up'"></mat-icon> <mat-icon class="text-red-600 icon-size-4 ml-1"
[svgIcon]="'heroicons_solid:arrow-narrow-up'"></mat-icon>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="my-8 space-y-8" *ngIf="!couponStats ">
<div class="flex flex-col">
<div class="flex items-center" *ngFor="let view of data.views">
<div style="display: flex;padding:10px;width: 100%;justify-content: space-between; border-bottom: lightgrey solid 0.4px;">
<div class="users-row" style="display: flex;padding:10px;width: 100%;">
<div id="circle">
<div>
<span>{{view.firstname.substring(0,2)}}</span>
</div>
</div>
<div class="flex flex-col">
<span class="pr-6 font-medium text-sm text-secondary whitespace-nowrap">
{{view.firstname}} {{view.surname}}
</span>
<span class="pr-6 font-small text-sm text-secondary wrap"
style=" word-break: break-word;">
{{view.pname}}
</span>
<span class="pr-6 font-small text-sm text-secondary whitespace-nowrap">
Number of Times Viewed : {{view.timesViewed}}
</span>
<div style="align-items: center;display: flex;margin-top: 10px;"
*ngIf="isScreenSmall">
<button mat-button style="background-color: #003445;color: white;"
(click)="generateCoupon(view)">Send
Coupon</button>
</div>
</div>
</div>
<div style="align-items: center;display: flex;justify-content: end;"
*ngIf="!isScreenSmall">
<button mat-button style="background-color: #003445;color: white;"
(click)="generateCoupon(view)">Send
Coupon</button>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

21
src/app/pages/admin/Dashboard/dashboard.component.scss

@ -0,0 +1,21 @@
.users-row {
cursor: pointer;
}
#circle {
background: #00a4be;
width: 45px;
height: 45px;
min-width: 45px;
border-radius: 64px;
color: #003445;
font-weight: bold;
text-align: center;
justify-content: center;
display: flex;
align-content: center;
align-items: center;
font-size: initial;
margin-right: 15px;
}

3548
src/app/pages/admin/Dashboard/dashboard.component.ts

File diff suppressed because it is too large

8
src/app/pages/admin/Dashboard/dashboard.module.ts

@ -13,11 +13,16 @@ import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { PercentagePipe } from 'app/pipes/percentage-pipe'
import {ChartModule} from 'primeng/chart';
import { CommonModule } from '@angular/common';
import { CarouselModule } from 'primeng/carousel';;
@NgModule({ @NgModule({
declarations: [ declarations: [
DashboardComponent DashboardComponent,
PercentagePipe
], ],
imports : [ imports : [
RouterModule.forChild(dashboardRoutes), RouterModule.forChild(dashboardRoutes),
@ -30,6 +35,7 @@ import { MatTableModule } from '@angular/material/table';
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,
MatTooltipModule, MatTooltipModule,
ChartModule,
NgApexchartsModule, NgApexchartsModule,
SharedModule SharedModule
] ]

37
src/app/pages/admin/Dashboard/dashboard.service.ts

@ -3,6 +3,8 @@ import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { analytics as analyticsData } from 'app/mock-api/dashboards/analytics/data'; import { analytics as analyticsData } from 'app/mock-api/dashboards/analytics/data';
import { environment } from 'environments/environment';
import { DashSummary } from 'app/models/dashSummary';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -10,6 +12,17 @@ import { analytics as analyticsData } from 'app/mock-api/dashboards/analytics/da
export class DashboardService export class DashboardService
{ {
private _data: BehaviorSubject<any> = new BehaviorSubject(null); private _data: BehaviorSubject<any> = new BehaviorSubject(null);
private _dataGraph: BehaviorSubject<any> = new BehaviorSubject(null);
private dashboard: DashSummary = {
summary: undefined,
views: [],
recent: [],
visitorsCurrent: [],
visitorsPrevious: [],
redemptions: [],
visits: [],
sales: []
};
/** /**
* Constructor * Constructor
@ -25,10 +38,14 @@ export class DashboardService
/** /**
* Getter for data * Getter for data
*/ */
get data$(): Observable<any> get data$(): Observable<DashSummary>
{ {
return this._data.asObservable(); return this._data.asObservable();
} }
get dataGraph$(): Observable<DashSummary>
{
return this._dataGraph.asObservable();
}
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
// @ Public methods // @ Public methods
@ -37,13 +54,19 @@ export class DashboardService
/** /**
* Get data * Get data
*/ */
getData(): Observable<any> getData(): Observable<DashSummary>
{
return this._httpClient.get(environment.apiURL + 'dashboard/summary').pipe(
tap((response: DashSummary) => {
this._data.next(response);
})
);
}
getDataGraph(): Observable<DashSummary>
{ {
console.log(analyticsData) return this._httpClient.get(environment.apiURL + 'dashboard/dash-graph-overview').pipe(
return this._httpClient.get('api/dashboards/analytics').pipe( tap((response: DashSummary) => {
tap((response: any) => { this._dataGraph.next(response);
console.log(response)
this._data.next(analyticsData);
}) })
); );
} }

47
src/app/pages/admin/Products/EditProduct/edit-product.component.html

@ -28,12 +28,13 @@
<strong class="tileHead">Product Name </strong> <strong class="tileHead">Product Name </strong>
</mat-label> </mat-label>
<mat-form-field appearance="fill" style="width: 350px;"> <mat-form-field appearance="fill" style="width: 350px;">
<input matInput style="margin-left:10px;" type="text" [(ngModel)]="product.productName"> <input matInput style="margin-left:10px;" type="text"
[(ngModel)]="product.productName">
</mat-form-field> </mat-form-field>
</div> </div>
<div style="display:flex;"> <div style="display:flex;">
<mat-label style="margin-right: 20px;"> <mat-label style="margin-right: 30px;">
<strong>Product Category</strong> <strong class="tileHead">Product Category </strong>
</mat-label> </mat-label>
<mat-form-field appearance="fill" style="width: 350px;"> <mat-form-field appearance="fill" style="width: 350px;">
<mat-select [(ngModel)]="product.categoryID"> <mat-select [(ngModel)]="product.categoryID">
@ -50,7 +51,8 @@
</mat-label> </mat-label>
<mat-form-field appearance="fill" style="width: 350px;"> <mat-form-field appearance="fill" style="width: 350px;">
<span matPrefix>GH¢ &nbsp;</span> <span matPrefix>GH¢ &nbsp;</span>
<input matInput style="margin-left:10px;" type="number" [(ngModel)]="product.unitPrice"> <input matInput style="margin-left:10px;" type="number"
[(ngModel)]="product.unitPrice">
</mat-form-field> </mat-form-field>
</div> </div>
@ -78,26 +80,37 @@
<div class="font-medium tracking-tight text-secondary">Add up to 10 high <div class="font-medium tracking-tight text-secondary">Add up to 10 high
quality images of the product quality images of the product
</div> </div>
<div class="font-medium tracking-tight text-secondary"> <div class="font-medium tracking-tight text-secondary"
{{product.images.length}} out of *ngIf="product.images != null">{{product.images.length}} out of
<strong>10</strong> <strong>10</strong>
</div> </div>
</div> </div>
<input style="display: none" type="file" (change)="onFileSelected($event)" <input style="display: none" type="file" (change)="onFileSelected($event)"
#fileInput accept="image/*" capture="user"> #fileInput accept="image/*" capture="user">
<button style="background-color: blue;color:white;margin-right:20px;" mat-button <button style="background-color: blue;color:white;margin-right:20px;" mat-button
(click)="fileInput.click()" [disabled]="product.images.length > 9">Add Image (click)="fileInput.click()" [disabled]="product.images.length > 9"
*ngIf="product.images != null">Add Image
</button> </button>
</div> </div>
<div class="imageList"> <div class="imageList">
<div class="imageHolder" *ngFor="let images of product.images"> <div class="imageHolder" *ngFor="let images of product.images">
<img src="{{images.imageSRC}}" class="productImage"> <div *ngIf="images.productID != '%local%'">
<img [src]="imageLoader(images.path)" class="productImage">
<button class="deleteelement" style="border:none;" <button class="deleteelement" style="border:none;"
(click)="removeImage(images)"> (click)="removeImage(images)">
<mat-icon [svgIcon]="'heroicons_outline:trash'" style="color: red;"> <mat-icon [svgIcon]="'heroicons_outline:trash'" style="color: red;">
</mat-icon> </mat-icon>
</button> </button>
</div> </div>
<div *ngIf="images.productID == '%local%'">
<img src="{{images.path}}" class="productImage">
<button class="deleteelement" style="border:none;"
(click)="removeImage(images)">
<mat-icon [svgIcon]="'heroicons_outline:trash'" style="color: red;">
</mat-icon>
</button>
</div>
</div>
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
@ -193,7 +206,8 @@
<div class="font-medium tracking-tight text-secondary">Add up to 10 high <div class="font-medium tracking-tight text-secondary">Add up to 10 high
quality images of the product quality images of the product
</div> </div>
<div class="font-medium tracking-tight text-secondary">0 out of <div class="font-medium tracking-tight text-secondary"
*ngIf="product.images != null">{{product.images.length}} out of
<strong>10</strong> <strong>10</strong>
</div> </div>
</div> </div>
@ -207,19 +221,30 @@
</div> </div>
<div class="imageList"> <div class="imageList">
<div class="imageHolder" *ngFor="let images of productImages"> <div class="imageHolder" *ngFor="let images of productImages">
<img src="{{images.imageSRC}}" class="productImage"> <div *ngIf="images.productID != '%local%'">
<img [src]="imageLoader(images.path)" class="productImage">
<button class="deleteelement" style="border:none;" <button class="deleteelement" style="border:none;"
(click)="removeImage(images)"> (click)="removeImage(images)">
<mat-icon [svgIcon]="'heroicons_outline:trash'" style="color: red;"> <mat-icon [svgIcon]="'heroicons_outline:trash'" style="color: red;">
</mat-icon> </mat-icon>
</button> </button>
</div> </div>
<div *ngIf="images.productID == '%local%'">
<img src="{{images.path}}" class="productImage">
<button class="deleteelement" style="border:none;"
(click)="removeImage(images)">
<mat-icon [svgIcon]="'heroicons_outline:trash'" style="color: red;">
</mat-icon>
</button>
</div>
</div>
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
<div class="row" style="display: flex;justify-content:space-around;width:100%;"> <div class="row" style="display: flex;justify-content:space-around;width:100%;">
<button style="background-color: green;color:white;margin-right:20px;" mat-button (click)="submit()">Save Changes</button> <button style="background-color: green;color:white;margin-right:20px;" mat-button
(click)="submit()">Save Changes</button>
<button style="background-color: firebrick;color:white;margin-left:10px;" mat-button <button style="background-color: firebrick;color:white;margin-left:10px;" mat-button
cdkFocusInitial>Cancel</button> cdkFocusInitial>Cancel</button>
</div> </div>

63
src/app/pages/admin/Products/EditProduct/edit-product.component.ts

@ -4,6 +4,7 @@ import { ConfirmBoxEvokeService } from '@costlydeveloper/ngx-awesome-popup';
import { tesoMediaWatcherService } from '@teso/services/media-watcher'; import { tesoMediaWatcherService } from '@teso/services/media-watcher';
import { ProductUpload } from 'app/models/generalModel'; import { ProductUpload } from 'app/models/generalModel';
import { ProductCategory, ProductImages, ProductsModel } from 'app/models/productsModel'; import { ProductCategory, ProductImages, ProductsModel } from 'app/models/productsModel';
import { environment } from 'environments/environment';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { ProductsService } from '../products.service'; import { ProductsService } from '../products.service';
@ -47,7 +48,9 @@ export class EditProductComponent implements OnInit {
isScreenSmall: boolean; isScreenSmall: boolean;
productImages: ProductUpload[] = []; productImages: ProductUpload[] = [];
product: ProductsModel; product: ProductsModel;
initalImages: string[];
categories: ProductCategory[] = []; categories: ProductCategory[] = [];
isLoading = true;
private _unsubscribeAll: Subject<any> = new Subject<any>(); private _unsubscribeAll: Subject<any> = new Subject<any>();
constructor(private _tesoMediaWatcherService: tesoMediaWatcherService, constructor(private _tesoMediaWatcherService: tesoMediaWatcherService,
private confirmBoxEvokeService: ConfirmBoxEvokeService, private changeDetector: ChangeDetectorRef, private confirmBoxEvokeService: ConfirmBoxEvokeService, private changeDetector: ChangeDetectorRef,
@ -58,11 +61,14 @@ export class EditProductComponent implements OnInit {
if (this.product == null) { if (this.product == null) {
this.router.navigate(['products']); this.router.navigate(['products']);
} }
_productService.getImages(this.productID).subscribe((response: ProductImages[]) => {
this.initalImages = response.map(e => e.path);
});
this._productService.categories$.pipe(takeUntil(this._unsubscribeAll)).subscribe((d) => { this._productService.categories$.pipe(takeUntil(this._unsubscribeAll)).subscribe((d) => {
this.categories = d; this.categories = d;
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
// this.isLoading = false; this.isLoading = false;
if (this.product != null) { if (this.product != null) {
this.product.categoryID = d.find((e) => e.catName = this.product.categoryID).catCode; this.product.categoryID = d.find((e) => e.catName = this.product.categoryID).catCode;
} }
@ -81,16 +87,7 @@ export class EditProductComponent implements OnInit {
}); });
} }
quillConfig = modules; quillConfig = modules;
items = [
{ id: 1, name: 'Python' },
{ id: 2, name: 'Node Js' },
{ id: 3, name: 'Java' },
{ id: 4, name: 'PHP', disabled: true },
{ id: 5, name: 'Django' },
{ id: 6, name: 'Angular' },
{ id: 7, name: 'Vue' },
{ id: 8, name: 'ReactJs' },
];
ngOnDestroy(): void { ngOnDestroy(): void {
// Unsubscribe from all subscriptions // Unsubscribe from all subscriptions
this._unsubscribeAll.next(); this._unsubscribeAll.next();
@ -101,7 +98,6 @@ export class EditProductComponent implements OnInit {
const file: File = event.target.files[0]; const file: File = event.target.files[0];
if (file.type.includes("image")) { if (file.type.includes("image")) {
console.log(file)
if (this.productImages.length == 0) { if (this.productImages.length == 0) {
var productImage: ProductUpload = { var productImage: ProductUpload = {
file: file, file: file,
@ -112,9 +108,6 @@ export class EditProductComponent implements OnInit {
let reader = new FileReader(); let reader = new FileReader();
reader.onload = (event: any) => { reader.onload = (event: any) => {
productImage.imageSRC = event.target.result; productImage.imageSRC = event.target.result;
}
reader.readAsDataURL(file);
var prodImage: ProductImages = { var prodImage: ProductImages = {
id: productImage.id, id: productImage.id,
productID: '%local%', productID: '%local%',
@ -124,6 +117,10 @@ export class EditProductComponent implements OnInit {
this.product.images.push(prodImage); this.product.images.push(prodImage);
this.productImages.push(productImage); this.productImages.push(productImage);
}
reader.readAsDataURL(file);
} else { } else {
var productImage: ProductUpload = { var productImage: ProductUpload = {
file: file, file: file,
@ -135,9 +132,6 @@ export class EditProductComponent implements OnInit {
let reader = new FileReader(); let reader = new FileReader();
reader.onload = (event: any) => { reader.onload = (event: any) => {
productImage.imageSRC = event.target.result; productImage.imageSRC = event.target.result;
}
reader.readAsDataURL(file);
var prodImage: ProductImages = { var prodImage: ProductImages = {
id: productImage.id, id: productImage.id,
productID: '%local%', productID: '%local%',
@ -146,6 +140,9 @@ export class EditProductComponent implements OnInit {
this.product.images.push(prodImage); this.product.images.push(prodImage);
this.productImages.push(productImage); this.productImages.push(productImage);
} }
reader.readAsDataURL(file);
}
} else { } else {
this.confirmBoxEvokeService.danger("Incompatible File", "Only images can be added", "OK").subscribe(); this.confirmBoxEvokeService.danger("Incompatible File", "Only images can be added", "OK").subscribe();
} }
@ -159,11 +156,33 @@ export class EditProductComponent implements OnInit {
} }
submit() { submit() {
var imagesToDelete: any[] = [];
this.initalImages.forEach((e) => {
if (!this.product.images.map((e) => e.path).includes(e)) {
imagesToDelete.push(e);
}
});
this.isLoading = true;
if (this.product.images.length == 0) { if (this.product.images.length == 0) {
this.confirmBoxEvokeService.warning("Add Product Images", "To add a new product, you must add at least one image to support it", "OK").subscribe(); this.confirmBoxEvokeService.warning("Action Required", "You must add at least one image of the product", "OK").subscribe();
} else { } else if (this.product.categoryID == '' || this.product.productDesc == '' || this.product.productName == '' || this.product.unitPrice == 0) {
this.confirmBoxEvokeService.warning("Missing Fields", "To make changes to a product, you must fill up all fields", "OK").subscribe();
} }
else {
this.confirmBoxEvokeService.success("Confirm Action", `Are you sure would like to make changes ${this.product.productName} ?`, "YES", "NO").subscribe(response => {
if (response.clickedButtonID == "yes") {
this._productService.editProduct(this.product, this.productImages, imagesToDelete).subscribe(() => {
this.confirmBoxEvokeService.success("Success", "Changes saved successfully !!!", "OK").subscribe( response =>{
this._productService.getData();
this.router.navigate(['products']);
});
});
}
});
}
this.isLoading = false;
}
imageLoader(path: string): string {
return environment.apiURL + `imagingproducts/${path}`;
} }
} }

29
src/app/pages/admin/Products/products.service.ts

@ -70,6 +70,14 @@ export class ProductsService {
return found; return found;
} }
getImages(id: string): Observable<ProductImages[]> {
var found = this.products.find(item => item.productID === id);
return this._httpClient.get(environment.apiURL + `allimages/${id}`).pipe(
tap((response: ProductImages[]) => {
}));
}
getCategories(): Observable<ProductCategory[]> { getCategories(): Observable<ProductCategory[]> {
return this._httpClient.get(environment.apiURL + `productcategories`).pipe( return this._httpClient.get(environment.apiURL + `productcategories`).pipe(
tap((response: ProductCategory[]) => { tap((response: ProductCategory[]) => {
@ -94,4 +102,25 @@ export class ProductsService {
this.getData(); this.getData();
})); }));
} }
editProduct(product: ProductsModel, productImage: ProductUpload[], remove: string[]): Observable<any> {
const formData = new FormData();
const images = productImage.map((e) => e.file);
formData.set("productID", product.productID);
formData.set("productName", product.productName);
formData.set("productDesc", product.productDesc);
formData.set("categoryId", product.categoryID);
formData.set("unitPrice", product.unitPrice.toString());
for (var i = 0; i < images.length; i++) {
formData.append("file[]", images[i]);
}
for (var i = 0; i < remove.length; i++) {
formData.append("removeImage", remove[i]);
console.log(remove[i])
}
// console.log(remove)
return this._httpClient.post(environment.apiURL + "products/editproduct", formData);
}
} }

2
src/app/pages/admin/Profile/InfoDialog/information-dialog.component.html

@ -77,7 +77,7 @@
</div> </div>
<div style="display: flex;justify-content:center;width: 100%;"> <div style="display: flex;justify-content:center;width: 100%;">
<button mat-button style="background-color: green;color: white;">Confirm Changes</button> <button mat-button style="background-color: green;color: white;">Ok</button>
</div> </div>
</div> </div>
</mat-dialog-content> </mat-dialog-content>

2
src/app/pages/admin/Profile/InfoDialog/information-dialog.component.ts

@ -28,7 +28,7 @@ export class InformationDialogComponent implements OnInit {
closeDialog() { closeDialog() {
this.data.details.businessCategory = this.selectedCategory.categoryCode; this.data.details.businessCategory = this.selectedCategory.categoryCode;
this._userService.update(this.data.details); this._userService.updateSub(this.data.details);
this.dialogRef.close(true); this.dialogRef.close(true);
} }

3
src/app/pages/admin/Profile/Information/information.component.html

@ -48,6 +48,3 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="text-center" style="margin-top:10px;">
<!-- <RadzenButton Text="Save Changes" Click="editProduct" Style="background-color:forestgreen !important;" /> -->
</div>

2
src/app/pages/admin/Profile/Information/information.component.ts

@ -6,6 +6,7 @@ import { ConfirmBoxEvokeService } from '@costlydeveloper/ngx-awesome-popup';
import { tesoMediaWatcherService } from '@teso/services/media-watcher'; import { tesoMediaWatcherService } from '@teso/services/media-watcher';
import { UserService } from 'app/core/user/user.service'; import { UserService } from 'app/core/user/user.service';
import { BusinessCategory, TesoBusinessDetail } from 'app/models/businessModel'; import { BusinessCategory, TesoBusinessDetail } from 'app/models/businessModel';
import { ProfileImage } from 'app/models/generalModel';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { FollowersService } from '../../Followers/followers.service'; import { FollowersService } from '../../Followers/followers.service';
@ -22,6 +23,7 @@ import { ProfileService } from '../profile.service';
}) })
export class InformationComponent implements OnInit { export class InformationComponent implements OnInit {
@Input() details: TesoBusinessDetail; @Input() details: TesoBusinessDetail;
@Input() uploads:ProfileImage;
currentCategory: string; currentCategory: string;
categories: BusinessCategory[] = []; categories: BusinessCategory[] = [];
private _unsubscribeAll: Subject<any> = new Subject<any>(); private _unsubscribeAll: Subject<any> = new Subject<any>();

4
src/app/pages/admin/Profile/QRCode/qrcode-dialog.component.html

@ -0,0 +1,4 @@
<img [src]="image" class="productImage">
<a class="btn btn-primary" [href]="image" download style="display: flex;justify-content: center;">
<button mat-button style="background-color: #0152cc;color: white;" >Download</button>
</a>

0
src/app/pages/admin/Profile/QRCode/qrcode-dialog.component.scss

25
src/app/pages/admin/Profile/QRCode/qrcode-dialog.component.spec.ts

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QrcodeDialogComponent } from './qrcode-dialog.component';
describe('QrcodeDialogComponent', () => {
let component: QrcodeDialogComponent;
let fixture: ComponentFixture<QrcodeDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ QrcodeDialogComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(QrcodeDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

20
src/app/pages/admin/Profile/QRCode/qrcode-dialog.component.ts

@ -0,0 +1,20 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-qrcode-dialog',
templateUrl: './qrcode-dialog.component.html',
styleUrls: ['./qrcode-dialog.component.scss']
})
export class QrcodeDialogComponent implements OnInit {
image: any = "";
constructor(@Inject(MAT_DIALOG_DATA) public data: { image: string }, private domSanitizer: DomSanitizer,
public dialogRef: MatDialogRef<QrcodeDialogComponent>) {
this.image = this.domSanitizer.bypassSecurityTrustUrl(data.image);
}
ngOnInit(): void {
}
}

22
src/app/pages/admin/Profile/profile.component.html

@ -9,15 +9,15 @@
#fileInput accept="image/*"> #fileInput accept="image/*">
<div class="text-center"> <div class="text-center">
<img id="logo" class="profile-user-img img-fluid img-circle" [src]="imageLoader(profile.businessLogo)" <img id="logo" class="profile-user-img img-fluid img-circle" [src]="imageLoader(profile.businessLogo)"
style="height:150px;width:150px;" *ngIf="newProfilePicture === undefined"> style="height:150px;width:150px;" *ngIf="newProfilePicture.imageSRC === undefined">
<img id="logo" class="profile-user-img img-fluid img-circle" src="{{newProfilePicture.imageSRC}}" <img id="logo" class="profile-user-img img-fluid img-circle" src="{{newProfilePicture.imageSRC}}"
style="height:150px;width:150px;" *ngIf="newProfilePicture != undefined"> style="height:150px;width:150px;" *ngIf="newProfilePicture.imageSRC != undefined">
</div> </div>
<div class="text-center" style="margin-top:10px;"> <div class="text-center" style="margin-top:10px;">
<button mat-button style="background-color: #0152cc;color: white;" <button mat-button style="background-color: #0152cc;color: white;"
(click)="fileInput.click()" *ngIf="newProfilePicture === undefined">Select</button> (click)="fileInput.click()" *ngIf="newProfilePicture.imageSRC === undefined">Select</button>
<button mat-button style="background-color: rgb(163, 35, 35);color: white;" <button mat-button style="background-color: rgb(163, 35, 35);color: white;"
(click)="clear()" *ngIf="newProfilePicture != undefined">Clear</button> (click)="clear()" *ngIf="newProfilePicture.imageSRC != undefined">Clear</button>
</div> </div>
</form> </form>
@ -29,7 +29,7 @@
<a href="followers" class="float-right" style="text-decoration: none;color: #0152cc;"> {{subscribers.length}}</a> <a href="followers" class="float-right" style="text-decoration: none;color: #0152cc;"> {{subscribers.length}}</a>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<button class=" sm:inline-flex" mat-flat-button style="background-color: #0152cc;margin-right:10px;"> <button class=" sm:inline-flex" mat-flat-button style="background-color: #0152cc;margin-right:10px;" (click)="generateCode()">
<a href="javascript:void(0)" style="color: #fff;text-decoration: none;margin-right:10px;" >Generate QR-Code</a> <a href="javascript:void(0)" style="color: #fff;text-decoration: none;margin-right:10px;" >Generate QR-Code</a>
</button> </button>
<!-- <a class="btn btn-secondary btn-lg" onclick="generateCode"></a> --> <!-- <a class="btn btn-secondary btn-lg" onclick="generateCode"></a> -->
@ -49,16 +49,22 @@
<div class="card-body"> <div class="card-body">
<p class="text-muted"> <p class="text-muted">
<strong><i class="fas fa-book mr-1"></i> </strong> <strong><i class="fas fa-book mr-1"></i> </strong>
<span id="shop-desc">{{profile.businessDescription}}</span> <span id="shop-desc">
<div [innerHTML]="profile.businessDescription">
</div>
</span>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-9"> <div class="col-md-9">
<div class="card"> <div class="card">
<business-information [details]="profile"></business-information> <business-information [details]="profile" [uploads]="newProfilePicture"></business-information>
<div class="text-center" style="margin-top:10px;margin-bottom: 20px;">
<button mat-button style="background-color: #0152cc;color: white;" (click)="SaveChanges()">Save Changes</button>
</div>
</div> </div>
</div> </div>
</div> </div>

37
src/app/pages/admin/Profile/profile.component.ts

@ -5,6 +5,7 @@ import { ConfirmBoxEvokeService } from '@costlydeveloper/ngx-awesome-popup';
import { tesoMediaWatcherService } from '@teso/services/media-watcher'; import { tesoMediaWatcherService } from '@teso/services/media-watcher';
import { UserService } from 'app/core/user/user.service'; import { UserService } from 'app/core/user/user.service';
import { TesoBusinessDetail } from 'app/models/businessModel'; import { TesoBusinessDetail } from 'app/models/businessModel';
import { ProfileImage } from 'app/models/generalModel';
import { TesoUserDetails } from 'app/models/userModel'; import { TesoUserDetails } from 'app/models/userModel';
import { environment } from 'environments/environment'; import { environment } from 'environments/environment';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -12,6 +13,7 @@ import { takeUntil } from 'rxjs/operators';
import { FollowersService } from '../Followers/followers.service'; import { FollowersService } from '../Followers/followers.service';
import { DescriptionDialogComponent } from './DescriptionDialog/description-dialog.component'; import { DescriptionDialogComponent } from './DescriptionDialog/description-dialog.component';
import { ProfileService } from './profile.service'; import { ProfileService } from './profile.service';
import { QrcodeDialogComponent } from './QRCode/qrcode-dialog.component';
@Component({ @Component({
selector: 'app-profile', selector: 'app-profile',
@ -21,8 +23,8 @@ import { ProfileService } from './profile.service';
}) })
export class ProfileComponent implements OnInit { export class ProfileComponent implements OnInit {
profile: TesoBusinessDetail = {}; profile: TesoBusinessDetail = {};
subscribers:TesoUserDetails[]=[]; subscribers: TesoUserDetails[] = [];
newProfilePicture:any; newProfilePicture: ProfileImage = {};
private _unsubscribeAll: Subject<any> = new Subject<any>(); private _unsubscribeAll: Subject<any> = new Subject<any>();
constructor(private router: Router, private _tesoMediaWatcherService: tesoMediaWatcherService, constructor(private router: Router, private _tesoMediaWatcherService: tesoMediaWatcherService,
private _profileService: ProfileService, private _userService: UserService, private _profileService: ProfileService, private _userService: UserService,
@ -40,24 +42,22 @@ export class ProfileComponent implements OnInit {
imageLoader(path: string): string { imageLoader(path: string): string {
return environment.apiURL + `shoplogo/${path}`; return environment.apiURL + `shoplogo/${path}`;
} }
editShopDescription(){ editShopDescription() {
const dialogReference = this.dialog.open(DescriptionDialogComponent,{ const dialogReference = this.dialog.open(DescriptionDialogComponent, {
disableClose: true, disableClose: true,
hasBackdrop: true, hasBackdrop: true,
data: { details: this.profile.businessDescription }, data: { details: this.profile.businessDescription },
}); });
dialogReference.afterClosed().subscribe((d)=> { dialogReference.afterClosed().subscribe((d) => {
this.profile.businessDescription = d; this.profile.businessDescription = d;
}); });
} }
onFileSelected(event) { onFileSelected(event) {
const file: File = event.target.files[0]; const file: File = event.target.files[0];
if (file.type.includes("image")) { if (file.type.includes("image")) {
this.newProfilePicture = { this.newProfilePicture = {
file: file, file: file,
highlight: true,
imageSRC: "", imageSRC: "",
}; };
let reader = new FileReader(); let reader = new FileReader();
@ -69,7 +69,26 @@ export class ProfileComponent implements OnInit {
this.confirmBoxEvokeService.danger("Incompatible File", "Only images can be added", "OK").subscribe(); this.confirmBoxEvokeService.danger("Incompatible File", "Only images can be added", "OK").subscribe();
} }
} }
clear(){
this.newProfilePicture={}; clear() {
console.log(this.newProfilePicture)
this.newProfilePicture = {};
}
SaveChanges() {
this._userService.update(this.profile, this.newProfilePicture).subscribe((response) => {
console.log(response)
});
}
generateCode() {
var dataImagge;
this._profileService.getQRCode().subscribe((response) => {
dataImagge = response;
this.dialog.open(QrcodeDialogComponent, {
hasBackdrop: true,
width: "400px",
height: "450px",
data: { image: dataImagge }
});
});
} }
} }

4
src/app/pages/admin/Profile/profile.module.ts

@ -27,6 +27,7 @@ import { InformationComponent } from './Information/information.component';
import { SettingsComponent } from './Settings/settings.component'; import { SettingsComponent } from './Settings/settings.component';
import { DescriptionDialogComponent } from './DescriptionDialog/description-dialog.component'; import { DescriptionDialogComponent } from './DescriptionDialog/description-dialog.component';
import { InformationDialogComponent } from './InfoDialog/information-dialog.component'; import { InformationDialogComponent } from './InfoDialog/information-dialog.component';
import { QrcodeDialogComponent } from './QRCode/qrcode-dialog.component';
@ -36,7 +37,8 @@ import { InformationDialogComponent } from './InfoDialog/information-dialog.comp
InformationComponent, InformationComponent,
SettingsComponent, SettingsComponent,
DescriptionDialogComponent, DescriptionDialogComponent,
InformationDialogComponent InformationDialogComponent,
QrcodeDialogComponent
], ],
imports: [ imports: [
RouterModule.forChild(profileRoutes), RouterModule.forChild(profileRoutes),

23
src/app/pages/admin/Profile/profile.service.ts

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators'; import { switchMap, tap } from 'rxjs/operators';
import { analytics as analyticsData } from 'app/mock-api/dashboards/analytics/data'; import { analytics as analyticsData } from 'app/mock-api/dashboards/analytics/data';
import { environment } from 'environments/environment'; import { environment } from 'environments/environment';
import { BusinessCategory, TesoBusinessDetail } from 'app/models/businessModel'; import { BusinessCategory, TesoBusinessDetail } from 'app/models/businessModel';
@ -61,4 +61,21 @@ export class ProfileService {
this._businessCategory.next(response); this._businessCategory.next(response);
})); }));
} }
getQRCode(): Observable<any> {
const headers = new HttpHeaders().set('Content-Type', 'text/plain; charset=utf-8');
const requestOptions: Object = {
headers: headers,
responseType: 'text'
}
return this._httpClient.get<string>(environment.apiURL + `business/generate-qrcode`, requestOptions).pipe(
switchMap((response: any) => {
return of(response);
})
);
}
editProfile(profile:TesoBusinessDetail){
}
} }

2
src/app/pages/admin/TempShop/temp-shop.component.ts

@ -17,7 +17,7 @@ export class TempShopComponent implements OnInit {
selectedCategory: BusinessCategory; selectedCategory: BusinessCategory;
newBusiness: TesoBusinessDetail = { newBusiness: TesoBusinessDetail = {
businessId: "", businessId: "",
Handle: "", handle: false,
businessName: "", businessName: "",
businessEmail: "", businessEmail: "",
businessTin: "", businessTin: "",

62
src/app/pages/auth/confirmation-required/confirmation-required.component.html

@ -1,62 +0,0 @@
<div class="flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-auto min-w-0">
<div class="md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-4 sm:p-12 md:p-16 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none sm:bg-card">
<div class="w-full max-w-80 sm:w-80 mx-auto sm:mx-0">
<!-- Logo -->
<div class="w-12">
<img src="assets/images/logo/logo.svg">
</div>
<!-- Title -->
<div class="mt-8 text-4xl font-extrabold tracking-tight leading-tight">Confirmation required</div>
<div class="mt-4">
A confirmation mail with instructions has been sent to your email address. Follow those instructions to confirm your email address and activate your account.
</div>
<!-- Form footer -->
<div class="mt-8 text-md font-medium text-secondary">
<span>Return to</span>
<a class="ml-1 text-primary-500 hover:underline" [routerLink]="['/sign-in']">sign in
</a>
</div>
</div>
</div>
<div class="relative hidden md:flex flex-auto items-center justify-center w-1/2 h-full p-16 lg:px-28 overflow-hidden bg-gray-800 dark:border-l">
<!-- Background - @formatter:off -->
<!-- Rings -->
<svg class="absolute inset-0 pointer-events-none" viewBox="0 0 960 540" width="100%" height="100%" preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
<circle r="234" cx="196" cy="23"></circle>
<circle r="234" cx="790" cy="491"></circle>
</g>
</svg>
<!-- Dots -->
<svg class="absolute -top-16 -right-16 text-gray-700" viewBox="0 0 220 192" width="220" height="192" fill="none">
<defs>
<pattern id="837c3e70-6c3a-44e6-8854-cc48c737b659" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<rect x="0" y="0" width="4" height="4" fill="currentColor"></rect>
</pattern>
</defs>
<rect width="220" height="192" fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)"></rect>
</svg>
<!-- @formatter:on -->
<!-- Content -->
<div class="z-10 relative w-full max-w-2xl">
<div class="text-7xl font-bold leading-none text-gray-100">
<div>Welcome to</div>
<div>our community</div>
</div>
<div class="mt-6 text-lg tracking-tight leading-6 text-gray-400">
teso helps developers to build organized and well coded dashboards full of beautiful and rich modules. Join us and start building your application today.
</div>
<div class="flex items-center mt-8">
<div class="flex flex-0 items-center -space-x-1.5">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/female-18.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/female-11.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/male-09.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/male-16.jpg">
</div>
<div class="ml-4 font-medium tracking-tight text-gray-400">More than 17k people joined us, it's your turn</div>
</div>
</div>
</div>
</div>

18
src/app/pages/auth/confirmation-required/confirmation-required.component.ts

@ -1,18 +0,0 @@
import { Component, ViewEncapsulation } from '@angular/core';
import { tesoAnimations } from '@teso/animations';
@Component({
selector : 'auth-confirmation-required',
templateUrl : './confirmation-required.component.html',
encapsulation: ViewEncapsulation.None,
animations : tesoAnimations
})
export class AuthConfirmationRequiredComponent
{
/**
* Constructor
*/
constructor()
{
}
}

22
src/app/pages/auth/confirmation-required/confirmation-required.module.ts

@ -1,22 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { tesoCardModule } from '@teso/components/card';
import { SharedModule } from 'app/shared/shared.module';
import { AuthConfirmationRequiredComponent } from 'app/pages/auth/confirmation-required/confirmation-required.component';
import { authConfirmationRequiredRoutes } from 'app/pages/auth/confirmation-required/confirmation-required.routing';
@NgModule({
declarations: [
AuthConfirmationRequiredComponent
],
imports : [
RouterModule.forChild(authConfirmationRequiredRoutes),
MatButtonModule,
tesoCardModule,
SharedModule
]
})
export class AuthConfirmationRequiredModule
{
}

9
src/app/pages/auth/confirmation-required/confirmation-required.routing.ts

@ -1,9 +0,0 @@
import { Route } from '@angular/router';
import { AuthConfirmationRequiredComponent } from 'app/pages/auth/confirmation-required/confirmation-required.component';
export const authConfirmationRequiredRoutes: Route[] = [
{
path : '',
component: AuthConfirmationRequiredComponent
}
];

93
src/app/pages/auth/forgot-password/forgot-password.component.html

@ -1,93 +0,0 @@
<div class="flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-auto min-w-0">
<div class="md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-4 sm:p-12 md:p-16 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none sm:bg-card">
<div class="w-full max-w-80 sm:w-80 mx-auto sm:mx-0">
<!-- Logo -->
<div class="w-12">
<img src="assets/images/logo/logo.svg">
</div>
<!-- Title -->
<div class="mt-8 text-4xl font-extrabold tracking-tight leading-tight">Forgot password?</div>
<div class="mt-0.5 font-medium">Fill the form to reset your password</div>
<!-- Alert -->
<teso-alert class="mt-8 -mb-4" *ngIf="showAlert" [appearance]="'outline'" [showIcon]="false" [type]="alert.type" [@shake]="alert.type === 'error'">
{{alert.message}}
</teso-alert>
<!-- Forgot password form -->
<form class="mt-8" [formGroup]="forgotPasswordForm" #forgotPasswordNgForm="ngForm">
<!-- Email field -->
<mat-form-field class="w-full">
<mat-label>Email address</mat-label>
<input id="email" matInput [formControlName]="'email'">
<mat-error *ngIf="forgotPasswordForm.get('email').hasError('required')">
Email address is required
</mat-error>
<mat-error *ngIf="forgotPasswordForm.get('email').hasError('email')">
Please enter a valid email address
</mat-error>
</mat-form-field>
<!-- Submit button -->
<button class="teso-mat-button-large w-full mt-3" mat-flat-button [color]="'primary'" [disabled]="forgotPasswordForm.disabled" (click)="sendResetLink()">
<span *ngIf="!forgotPasswordForm.disabled">
Send reset link
</span>
<mat-progress-spinner
*ngIf="forgotPasswordForm.disabled"
[diameter]="24"
[mode]="'indeterminate'"></mat-progress-spinner>
</button>
<!-- Form footer -->
<div class="mt-8 text-md font-medium text-secondary">
<span>Return to</span>
<a class="ml-1 text-primary-500 hover:underline" [routerLink]="['/sign-in']">sign in
</a>
</div>
</form>
</div>
</div>
<div class="relative hidden md:flex flex-auto items-center justify-center w-1/2 h-full p-16 lg:px-28 overflow-hidden bg-gray-800 dark:border-l">
<!-- Background - @formatter:off -->
<!-- Rings -->
<svg class="absolute inset-0 pointer-events-none" viewBox="0 0 960 540" width="100%" height="100%" preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
<circle r="234" cx="196" cy="23"></circle>
<circle r="234" cx="790" cy="491"></circle>
</g>
</svg>
<!-- Dots -->
<svg class="absolute -top-16 -right-16 text-gray-700" viewBox="0 0 220 192" width="220" height="192" fill="none">
<defs>
<pattern id="837c3e70-6c3a-44e6-8854-cc48c737b659" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<rect x="0" y="0" width="4" height="4" fill="currentColor"></rect>
</pattern>
</defs>
<rect width="220" height="192" fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)"></rect>
</svg>
<!-- @formatter:on -->
<!-- Content -->
<div class="z-10 relative w-full max-w-2xl">
<div class="text-7xl font-bold leading-none text-gray-100">
<div>Welcome to</div>
<div>our community</div>
</div>
<div class="mt-6 text-lg tracking-tight leading-6 text-gray-400">
teso helps developers to build organized and well coded dashboards full of beautiful and rich modules. Join us and start building your application today.
</div>
<div class="flex items-center mt-8">
<div class="flex flex-0 items-center -space-x-1.5">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/female-18.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/female-11.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/male-09.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/male-16.jpg">
</div>
<div class="ml-4 font-medium tracking-tight text-gray-400">More than 17k people joined us, it's your turn</div>
</div>
</div>
</div>
</div>

105
src/app/pages/auth/forgot-password/forgot-password.component.ts

@ -1,105 +0,0 @@
import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { finalize } from 'rxjs/operators';
import { tesoAnimations } from '@teso/animations';
import { tesoAlertType } from '@teso/components/alert';
import { AuthService } from 'app/core/auth/auth.service';
@Component({
selector : 'auth-forgot-password',
templateUrl : './forgot-password.component.html',
encapsulation: ViewEncapsulation.None,
animations : tesoAnimations
})
export class AuthForgotPasswordComponent implements OnInit
{
@ViewChild('forgotPasswordNgForm') forgotPasswordNgForm: NgForm;
alert: { type: tesoAlertType; message: string } = {
type : 'success',
message: ''
};
forgotPasswordForm: FormGroup;
showAlert: boolean = false;
/**
* Constructor
*/
constructor(
private _authService: AuthService,
private _formBuilder: FormBuilder
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Create the form
this.forgotPasswordForm = this._formBuilder.group({
email: ['', [Validators.required, Validators.email]]
});
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Send the reset link
*/
sendResetLink(): void
{
// Return if the form is invalid
if ( this.forgotPasswordForm.invalid )
{
return;
}
// Disable the form
this.forgotPasswordForm.disable();
// Hide the alert
this.showAlert = false;
// Forgot password
this._authService.forgotPassword(this.forgotPasswordForm.get('email').value)
.pipe(
finalize(() => {
// Re-enable the form
this.forgotPasswordForm.enable();
// Reset the form
this.forgotPasswordNgForm.resetForm();
// Show the alert
this.showAlert = true;
})
)
.subscribe(
(response) => {
// Set the alert
this.alert = {
type : 'success',
message: 'Password reset sent! You\'ll receive an email if you are registered on our system.'
};
},
(response) => {
// Set the alert
this.alert = {
type : 'error',
message: 'Email does not found! Are you sure you are already a member?'
};
}
);
}
}

32
src/app/pages/auth/forgot-password/forgot-password.module.ts

@ -1,32 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { tesoCardModule } from '@teso/components/card';
import { tesoAlertModule } from '@teso/components/alert';
import { SharedModule } from 'app/shared/shared.module';
import { AuthForgotPasswordComponent } from 'app/pages/auth/forgot-password/forgot-password.component';
import { authForgotPasswordRoutes } from 'app/pages/auth/forgot-password/forgot-password.routing';
@NgModule({
declarations: [
AuthForgotPasswordComponent
],
imports : [
RouterModule.forChild(authForgotPasswordRoutes),
MatButtonModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatProgressSpinnerModule,
tesoCardModule,
tesoAlertModule,
SharedModule
]
})
export class AuthForgotPasswordModule
{
}

9
src/app/pages/auth/forgot-password/forgot-password.routing.ts

@ -1,9 +0,0 @@
import { Route } from '@angular/router';
import { AuthForgotPasswordComponent } from 'app/pages/auth/forgot-password/forgot-password.component';
export const authForgotPasswordRoutes: Route[] = [
{
path : '',
component: AuthForgotPasswordComponent
}
];

122
src/app/pages/auth/reset-password/reset-password.component.html

@ -1,122 +0,0 @@
<div class="flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-auto min-w-0">
<div class="md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-4 sm:p-12 md:p-16 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none sm:bg-card">
<div class="w-full max-w-80 sm:w-80 mx-auto sm:mx-0">
<!-- Logo -->
<div class="w-12">
<img src="assets/images/logo/logo.svg">
</div>
<!-- Title -->
<div class="mt-8 text-4xl font-extrabold tracking-tight leading-tight">Reset your password</div>
<div class="mt-0.5 font-medium">Create a new password for your account</div>
<!-- Alert -->
<teso-alert class="mt-8 -mb-4" *ngIf="showAlert" [appearance]="'outline'" [showIcon]="false" [type]="alert.type" [@shake]="alert.type === 'error'">
{{alert.message}}
</teso-alert>
<!-- Reset password form -->
<form class="mt-8" [formGroup]="resetPasswordForm" #resetPasswordNgForm="ngForm">
<!-- Password field -->
<mat-form-field class="w-full">
<mat-label>Password</mat-label>
<input id="password" matInput type="password" [formControlName]="'password'" #passwordField>
<button mat-icon-button type="button" (click)="passwordField.type === 'password' ? passwordField.type = 'text' : passwordField.type = 'password'" matSuffix>
<mat-icon
class="icon-size-5"
*ngIf="passwordField.type === 'password'"
[svgIcon]="'heroicons_solid:eye'"></mat-icon>
<mat-icon
class="icon-size-5"
*ngIf="passwordField.type === 'text'"
[svgIcon]="'heroicons_solid:eye-off'"></mat-icon>
</button>
<mat-error>
Password is required
</mat-error>
</mat-form-field>
<!-- Password confirm field -->
<mat-form-field class="w-full">
<mat-label>Password (Confirm)</mat-label>
<input id="password-confirm" matInput type="password" [formControlName]="'passwordConfirm'" #passwordConfirmField>
<button mat-icon-button type="button" (click)="passwordConfirmField.type === 'password' ? passwordConfirmField.type = 'text' : passwordConfirmField.type = 'password'" matSuffix>
<mat-icon
class="icon-size-5"
*ngIf="passwordConfirmField.type === 'password'"
[svgIcon]="'heroicons_solid:eye'"></mat-icon>
<mat-icon
class="icon-size-5"
*ngIf="passwordConfirmField.type === 'text'"
[svgIcon]="'heroicons_solid:eye-off'"></mat-icon>
</button>
<mat-error *ngIf="resetPasswordForm.get('passwordConfirm').hasError('required')">
Password confirmation is required
</mat-error>
<mat-error *ngIf="resetPasswordForm.get('passwordConfirm').hasError('mustMatch')">
Passwords must match
</mat-error>
</mat-form-field>
<!-- Submit button -->
<button class="teso-mat-button-large w-full mt-3" mat-flat-button [color]="'primary'" [disabled]="resetPasswordForm.disabled" (click)="resetPassword()">
<span *ngIf="!resetPasswordForm.disabled">
Reset your password
</span>
<mat-progress-spinner
*ngIf="resetPasswordForm.disabled"
[diameter]="24"
[mode]="'indeterminate'"></mat-progress-spinner>
</button>
<!-- Form footer -->
<div class="mt-8 text-md font-medium text-secondary">
<span>Return to</span>
<a class="ml-1 text-primary-500 hover:underline" [routerLink]="['/sign-in']">sign in
</a>
</div>
</form>
</div>
</div>
<div class="relative hidden md:flex flex-auto items-center justify-center w-1/2 h-full p-16 lg:px-28 overflow-hidden bg-gray-800 dark:border-l">
<!-- Background - @formatter:off -->
<!-- Rings -->
<svg class="absolute inset-0 pointer-events-none" viewBox="0 0 960 540" width="100%" height="100%" preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
<circle r="234" cx="196" cy="23"></circle>
<circle r="234" cx="790" cy="491"></circle>
</g>
</svg>
<!-- Dots -->
<svg class="absolute -top-16 -right-16 text-gray-700" viewBox="0 0 220 192" width="220" height="192" fill="none">
<defs>
<pattern id="837c3e70-6c3a-44e6-8854-cc48c737b659" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<rect x="0" y="0" width="4" height="4" fill="currentColor"></rect>
</pattern>
</defs>
<rect width="220" height="192" fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)"></rect>
</svg>
<!-- @formatter:on -->
<!-- Content -->
<div class="z-10 relative w-full max-w-2xl">
<div class="text-7xl font-bold leading-none text-gray-100">
<div>Welcome to</div>
<div>our community</div>
</div>
<div class="mt-6 text-lg tracking-tight leading-6 text-gray-400">
teso helps developers to build organized and well coded dashboards full of beautiful and rich modules. Join us and start building your application today.
</div>
<div class="flex items-center mt-8">
<div class="flex flex-0 items-center -space-x-1.5">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/female-18.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/female-11.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/male-09.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/male-16.jpg">
</div>
<div class="ml-4 font-medium tracking-tight text-gray-400">More than 17k people joined us, it's your turn</div>
</div>
</div>
</div>
</div>

111
src/app/pages/auth/reset-password/reset-password.component.ts

@ -1,111 +0,0 @@
import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { finalize } from 'rxjs/operators';
import { tesoAnimations } from '@teso/animations';
import { tesoValidators } from '@teso/validators';
import { tesoAlertType } from '@teso/components/alert';
import { AuthService } from 'app/core/auth/auth.service';
@Component({
selector : 'auth-reset-password',
templateUrl : './reset-password.component.html',
encapsulation: ViewEncapsulation.None,
animations : tesoAnimations
})
export class AuthResetPasswordComponent implements OnInit
{
@ViewChild('resetPasswordNgForm') resetPasswordNgForm: NgForm;
alert: { type: tesoAlertType; message: string } = {
type : 'success',
message: ''
};
resetPasswordForm: FormGroup;
showAlert: boolean = false;
/**
* Constructor
*/
constructor(
private _authService: AuthService,
private _formBuilder: FormBuilder
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Create the form
this.resetPasswordForm = this._formBuilder.group({
password : ['', Validators.required],
passwordConfirm: ['', Validators.required]
},
{
validators: tesoValidators.mustMatch('password', 'passwordConfirm')
}
);
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Reset password
*/
resetPassword(): void
{
// Return if the form is invalid
if ( this.resetPasswordForm.invalid )
{
return;
}
// Disable the form
this.resetPasswordForm.disable();
// Hide the alert
this.showAlert = false;
// Send the request to the server
this._authService.resetPassword(this.resetPasswordForm.get('password').value)
.pipe(
finalize(() => {
// Re-enable the form
this.resetPasswordForm.enable();
// Reset the form
this.resetPasswordNgForm.resetForm();
// Show the alert
this.showAlert = true;
})
)
.subscribe(
(response) => {
// Set the alert
this.alert = {
type : 'success',
message: 'Your password has been reset.'
};
},
(response) => {
// Set the alert
this.alert = {
type : 'error',
message: 'Something went wrong, please try again.'
};
}
);
}
}

32
src/app/pages/auth/reset-password/reset-password.module.ts

@ -1,32 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { tesoCardModule } from '@teso/components/card';
import { tesoAlertModule } from '@teso/components/alert';
import { SharedModule } from 'app/shared/shared.module';
import { AuthResetPasswordComponent } from 'app/pages/auth/reset-password/reset-password.component';
import { authResetPasswordRoutes } from 'app/pages/auth/reset-password/reset-password.routing';
@NgModule({
declarations: [
AuthResetPasswordComponent
],
imports : [
RouterModule.forChild(authResetPasswordRoutes),
MatButtonModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatProgressSpinnerModule,
tesoCardModule,
tesoAlertModule,
SharedModule
]
})
export class AuthResetPasswordModule
{
}

9
src/app/pages/auth/reset-password/reset-password.routing.ts

@ -1,9 +0,0 @@
import { Route } from '@angular/router';
import { AuthResetPasswordComponent } from 'app/pages/auth/reset-password/reset-password.component';
export const authResetPasswordRoutes: Route[] = [
{
path : '',
component: AuthResetPasswordComponent
}
];

94
src/app/pages/auth/sign-in/sign-in.component.css

@ -21,3 +21,97 @@
letter-spacing: 5px; letter-spacing: 5px;
text-shadow: 3px 3px 20px #C3C76C, -2px 1px 30px #C3C76C; text-shadow: 3px 3px 20px #C3C76C, -2px 1px 30px #C3C76C;
} }
.img-circle {
border-radius: 50%;
}
.profile-user-img {
border: 3px solid #adb5bd;
margin: 0 auto;
padding: 3px;
width: 80px;
}
.img-fluid {
max-width: 100%;
height: auto;
}
img {
vertical-align: middle;
border-style: none;
}
.text-center {
text-align: center !important;
}
.card-title {
float: left;
font-size: 1.1rem;
font-weight: 400;
margin: 0;
}
.card-title {
margin-bottom: .75rem;
}
.card {
position: relative;
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 0 solid rgba(0,0,0,.125);
border-radius: .25rem;
}
.card {
position: relative;
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 0 solid rgba(0,0,0,.125);
border-radius: .25rem;
padding-top: 30px;
}
.ui-button.btn-primary, .btn-primary.ui-paginator-element {
background-color: #0152cc;
}
.logo {
display: block;
width: 100px;
margin: 0px auto;
border-radius: 20px;
background: #222;
padding: 10px;
}
#circle {
background: #d7e8eb;
width: 150px;
height: 150px;
min-width: 45px;
border-radius: 80px;
color: #003445;
font-weight: bold;
text-align: center;
justify-content: center;
display: flex;
align-content: center;
align-items: center;
font-size: initial;
}

122
src/app/pages/auth/sign-in/sign-in.component.html

@ -1,7 +1,7 @@
<div class="flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-auto min-w-0"> <div class="flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-auto min-w-0">
<div *ngIf="isScreenSmall" <div *ngIf="isScreenSmall"
class="mobileAuth md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-4 sm:p-12 md:p-16 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none sm:bg-card"> class="mobileAuth md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-4 sm:p-12 md:p-16 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none sm:bg-card">
<div class="w-full max-w-80 sm:w-80 mx-auto sm:mx-0" style="background-color: rgb(6 6 6 / 44%);color: white;"> <div class="w-full max-w-80 sm:w-80 mx-auto sm:mx-0" style="background-color: rgb(6 6 6 / 44%);color: white;" *ngIf="!hide">
<div class="w-full sm:w-auto py-8 px-4 sm:p-12 sm:rounded-2xl sm:shadow sm:bg-card"> <div class="w-full sm:w-auto py-8 px-4 sm:p-12 sm:rounded-2xl sm:shadow sm:bg-card">
<!-- Logo --> <!-- Logo -->
<div class="w-20"> <div class="w-20">
@ -21,14 +21,15 @@
</teso-alert> </teso-alert>
<!-- Sign in form --> <!-- Sign in form -->
<form #signInNgForm="ngForm" [formGroup]="signInForm">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<ngx-mat-intl-tel-input [preferredCountries]="['gh', 'ng']" [onlyCountries]="['gh', 'ng']" <ngx-mat-intl-tel-input [preferredCountries]="['gh', 'ng']" [onlyCountries]="['gh', 'ng']"
[enablePlaceholder]="true" [enableSearch]="false" name="phone"> [enablePlaceholder]="true" [enableSearch]="false" name="phone" formControlName="phone">
</ngx-mat-intl-tel-input> </ngx-mat-intl-tel-input>
<mat-hint style="color: wheat;">Standard call, message, or data rates may apply.</mat-hint> <mat-hint style="color: wheat;">Standard call, message, or data rates may apply.</mat-hint>
</mat-form-field> </mat-form-field>
</form>
<!-- Actions --> <!-- Actions -->
<div class="inline-flex items-end justify-between w-full mt-3.5"> <div class="inline-flex items-end justify-between w-full mt-3.5">
<mat-checkbox [color]="'primary'"> <mat-checkbox [color]="'primary'">
@ -37,25 +38,63 @@
</div> </div>
<!-- Submit button --> <!-- Submit button -->
<button class="teso-mat-button-large w-full mt-6" mat-flat-button [color]="'primary'" <button class="teso-mat-button-large w-full mt-6" id="tesoSubmit" mat-flat-button [color]="'primary'">
(click)="signIn()"> <span *ngIf="!signInForm.disabled">
<span>
Sign in Sign in
</span> </span>
<!-- <mat-progress-spinner <mat-progress-spinner
*ngIf="signInForm.disabled" *ngIf="signInForm.disabled"
[diameter]="24" [diameter]="24"
[mode]="'indeterminate'"></mat-progress-spinner> --> [mode]="'indeterminate'"></mat-progress-spinner>
</button> </button>
</div> </div>
</div> </div>
<div class="d-flex justify-content-center align-items-center" *ngIf="hide"
style="background-color: #484444c4 !important;padding: 20px;">
<div class="position-relative" style="display: grid;justify-content: center;">
<div class="card p-2 text-center" style="display: contents;">
<input style="display: none" type="file" (change)="onFileSelected($event)" #fileInput
accept="image/*">
<div class="text-center" style="width: 100%;justify-content: center;display: flex;">
<div class="text-center" *ngIf="newProfilePicture.imageSRC === undefined">
<div id="circle" (click)="fileInput.click()">
<div>
<span style="cursor:pointer;font-size: 63px;">+</span>
</div>
</div>
</div>
<img id="logo" class="profile-user-img img-fluid img-circle"
src="{{newProfilePicture.imageSRC}}" style="height:150px;width:150px;"
*ngIf="newProfilePicture.imageSRC != undefined">
</div>
<div class="text-center" style="margin-top:10px;margin-bottom:10px;">
<button mat-button style="background-color: rgb(163, 35, 35);color: white;" (click)="clear()"
*ngIf="newProfilePicture.imageSRC != undefined">Clear</button>
</div>
<mat-form-field appearance="outline" style="width: 300px; align-items: center;">
<mat-label style="color: white;">Business Name</mat-label>
<input matInput style="margin-left:10px;" type="text" style="color: white;">
</mat-form-field>
<mat-form-field appearance="outline" style="width: 300px; align-items: center;">
<mat-label style="color: white;">Digital Address</mat-label>
<input matInput style="margin-left:10px;" type="text" style="color: white;">
</mat-form-field>
<button mat-button style="background-color: #0152cc;color: white;" (click)="signUP()">Continue</button>
</div>
</div>
</div>
</div> </div>
<div id="welcomeBoard" <div id="welcomeBoard"
class="relative hidden md:flex flex-auto items-center justify-center w-1/2 h-full p-5 lg:px-20 overflow-hidden bg-gray-800 dark:border-l"> class="relative hidden md:flex flex-auto items-center justify-center w-1/2 h-full p-5 lg:px-20 overflow-hidden bg-gray-800 dark:border-l"
style="height: 100vh !important;">
<div class="w-full max-w-80 sm:w-80 mx-auto sm:mx-0"> <div class="w-full max-w-80 sm:w-80 mx-auto sm:mx-0" *ngIf="!hide">
<div class="w-full sm:w-auto py-5 px-4 sm:p-8 sm:rounded-2xl sm:shadow sm:bg-card" style="background-color: #484444c4 !important;color: white !important;"> <form [formGroup]="signInForm">
<div class="w-full sm:w-auto py-5 px-4 sm:p-8 sm:rounded-2xl sm:shadow sm:bg-card"
style="background-color: #484444c4 !important;color: white !important;">
<!-- Logo --> <!-- Logo -->
<div class="w-30" style="width: 100%;justify-content: center;display: flex;"> <div class="w-30" style="width: 100%;justify-content: center;display: flex;">
<img src="assets/images/logo.png"> <img src="assets/images/logo.png">
@ -76,30 +115,73 @@
<!-- Sign in form --> <!-- Sign in form -->
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<ngx-mat-intl-tel-input [preferredCountries]="['gh', 'ng']" [onlyCountries]="['gh', 'ng']" <ngx-mat-intl-tel-input [preferredCountries]="['gh', 'ng']" [onlyCountries]="['gh', 'ng']"
[enablePlaceholder]="true" [enableSearch]="false" name="phone"> [enablePlaceholder]="true" [enableSearch]="false" name="phone" formControlName="phone">
</ngx-mat-intl-tel-input> </ngx-mat-intl-tel-input>
<mat-hint style="color: white !important;">Standard call, message, or data rates may apply.</mat-hint> <mat-hint style="color: wheat;">Standard call, message, or data rates may apply.</mat-hint>
</mat-form-field> </mat-form-field>
<!-- Actions --> <!-- Actions -->
<div class="inline-flex items-end justify-between w-full mt-3.5"> <!-- <div class="inline-flex items-end justify-between w-full mt-3.5">
<mat-checkbox [color]="'primary'"> <mat-checkbox [color]="'primary'">
Remember me Remember me
</mat-checkbox> </mat-checkbox>
</div> </div> -->
<mat-error *ngIf="signInForm.controls['phone']?.errors?.required">Required Field</mat-error>
<mat-error *ngIf="signInForm.controls['phone']?.errors?.validatePhoneNumber">Invalid Number
</mat-error>
<!-- Submit button --> <!-- Submit button -->
<button class="teso-mat-button-large w-full mt-6" mat-flat-button [color]="'primary'" <button class="teso-mat-button-large w-full mt-6" id="tesoSubmit" mat-flat-button [color]="'primary'"
(click)="signIn()"> (click)="authenticate()">
<span> <span *ngIf="!signInForm.disabled">
Sign in Sign in
</span> </span>
<!-- <mat-progress-spinner <mat-progress-spinner
*ngIf="signInForm.disabled" *ngIf="signInForm.disabled"
[diameter]="24" [diameter]="24"
[mode]="'indeterminate'"></mat-progress-spinner> --> [mode]="'indeterminate'"></mat-progress-spinner>
</button> </button>
</div> </div>
</form>
</div>
<div class="d-flex justify-content-center align-items-center" *ngIf="hide"
style="background-color: #484444c4 !important;width: 400px;padding: 20px;">
<div class="position-relative" style="display: grid;justify-content: center;">
<div class="card p-2 text-center" style="display: contents;">
<input style="display: none" type="file" (change)="onFileSelected($event)" #fileInput
accept="image/*">
<div class="text-center" style="width: 100%;justify-content: center;display: flex;">
<div class="text-center" *ngIf="newProfilePicture.imageSRC === undefined">
<div id="circle" (click)="fileInput.click()">
<div>
<span style="cursor:pointer;font-size: 63px;">+</span>
</div>
</div>
</div>
<img id="logo" class="profile-user-img img-fluid img-circle"
src="{{newProfilePicture.imageSRC}}" style="height:150px;width:150px;"
*ngIf="newProfilePicture.imageSRC != undefined">
</div>
<div class="text-center" style="margin-top:10px;margin-bottom:10px;">
<button mat-button style="background-color: rgb(163, 35, 35);color: white;" (click)="clear()"
*ngIf="newProfilePicture.imageSRC != undefined">Clear</button>
</div>
<mat-form-field appearance="outline" style="width: 350px; align-items: center;">
<mat-label style="color: white;">Business Name</mat-label>
<input matInput style="margin-left:10px;" type="text" style="color: white;" [(ngModel)]="businessName">
</mat-form-field>
<mat-form-field appearance="outline" style="width: 350px; align-items: center;">
<mat-label style="color: white;">Digital Address</mat-label>
<input matInput style="margin-left:10px;" type="text" style="color: white;" [(ngModel)]="digitalAddress">
</mat-form-field>
<button mat-button style="background-color: #0152cc;color: white;" (click)="signUP()">Continue</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

211
src/app/pages/auth/sign-in/sign-in.component.ts

@ -1,12 +1,23 @@
import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, NgForm, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { tesoAnimations } from '@teso/animations'; import { tesoAnimations } from '@teso/animations';
import { tesoAlertType } from '@teso/components/alert'; import { tesoAlertType } from '@teso/components/alert';
import { tesoMediaWatcherService } from '@teso/services/media-watcher'; import { tesoMediaWatcherService } from '@teso/services/media-watcher';
import { AuthService } from 'app/core/auth/auth.service'; import { AuthService } from 'app/core/auth/auth.service';
import { WindowService } from 'app/window.service';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { getAuth, RecaptchaVerifier, signInWithPhoneNumber } from "firebase/auth";
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
import 'firebase/compat/firestore';
import { environment } from 'environments/environment';
import { MatDialog } from '@angular/material/dialog';
import { VerificationDialogComponent } from '../verification/verification-dialog.component';
import { ProfileImage } from 'app/models/generalModel';
declare var grecaptcha: any;
declare var $: any;
@Component({ @Component({
selector: 'auth-sign-in', selector: 'auth-sign-in',
@ -17,14 +28,29 @@ import { takeUntil } from 'rxjs/operators';
}) })
export class AuthSignInComponent implements OnInit { export class AuthSignInComponent implements OnInit {
signInForm: FormGroup;
alert: { type: tesoAlertType; message: string } = { alert: { type: tesoAlertType; message: string } = {
type: 'success', type: 'success',
message: '' message: ''
}; };
isScreenSmall: boolean; isScreenSmall: boolean;
showAlert: boolean = false; showAlert: boolean = false;
windowRef: any;
private _unsubscribeAll: Subject<any> = new Subject<any>(); private _unsubscribeAll: Subject<any> = new Subject<any>();
unregistered: boolean;
responseError: string;
renderLoading: boolean;
phonenumber;
hide = false;
businessName: String = "";
digitalAddress: String = "";
newProfilePicture: ProfileImage = {};
appVerifier;
recaptchedID;
tryagin = 0;
previousNumber = "";
running = false;
signLabel = "Sign in";
/** /**
* Constructor * Constructor
*/ */
@ -33,8 +59,14 @@ export class AuthSignInComponent implements OnInit {
private _authService: AuthService, private _authService: AuthService,
private _formBuilder: FormBuilder, private _formBuilder: FormBuilder,
private _router: Router, private _router: Router,
private _tesoMediaWatcherService: tesoMediaWatcherService private win: WindowService,
public dialog: MatDialog,
private _tesoMediaWatcherService: tesoMediaWatcherService,
private fb: FormBuilder
) { ) {
this.signInForm = this.fb.group({
phone: [undefined, [Validators.required]],
});
} }
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
@ -45,6 +77,9 @@ export class AuthSignInComponent implements OnInit {
* On init * On init
*/ */
ngOnInit(): void { ngOnInit(): void {
this.signInForm = new FormGroup({
phone: new FormControl()
});
// Create the form // Create the form
this._tesoMediaWatcherService.onMediaChange$ this._tesoMediaWatcherService.onMediaChange$
.pipe(takeUntil(this._unsubscribeAll)) .pipe(takeUntil(this._unsubscribeAll))
@ -62,49 +97,149 @@ export class AuthSignInComponent implements OnInit {
/** /**
* Sign in * Sign in
*/ */
signIn(): void { authenticate(): void {
// Return if the form is invalid console.log(this.tryagin)
var signInForm = this._formBuilder.group({ console.log(this.phonenumber)
email: ['support@tesoapp.com', [Validators.required, Validators.email]], // if(!this.phonenumber && this.phonenumber != this.previousNumber){
password: ['admin', Validators.required], this.phonenumber = this.signInForm.get('phone').value;
rememberMe: [''] // }
}); console.log(this.phonenumber)
// Hide the alert
this.showAlert = false; this.showAlert = false;
if (this.phonenumber)
if (this.tryagin < 3 || this.phonenumber != this.previousNumber) {
this.tryagin++;
if (this.recaptchedID != undefined)
grecaptcha.reset(this.recaptchedID);
const auth = firebase.auth();
this.unregistered = false;
this.responseError = "";
this.signInForm.disable();
this.renderLoading = true;
this.windowRef = this.win.windowRef
if (this.appVerifier === undefined) {
this.windowRef.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('tesoSubmit', {
'size': 'invisible',
'callback': (response) => {
// this.renderLoading = false;
// console.log(response)
}
});
this.appVerifier = this.windowRef.recaptchaVerifier;
this.appVerifier.render().then((widgetId) => {
this.recaptchedID = widgetId
});
}
auth.signInWithPhoneNumber(this.phonenumber, this.appVerifier)
.then((confirmationResult) => {
this.windowRef.confirmationResult = confirmationResult;
const dialogResult = this.dialog.open(VerificationDialogComponent, {
disableClose: true,
hasBackdrop: true,
height: '400px',
data: {
windowRef: this.windowRef,
phone: this.phonenumber
},
});
// Sign in dialogResult.afterClosed().subscribe((response) => {
this._authService.signIn(signInForm.value) if (response == "verified") {
.subscribe(
() => {
// Set the redirect url.
// The '/signed-in-redirect' is a dummy url to catch the request and redirect the user
// to the correct page after a successful sign in. This way, that url can be set via
// routing file and we don't have to touch here.
const redirectURL = this._activatedRoute.snapshot.queryParamMap.get('redirectURL') || '/signed-in-redirect'; const redirectURL = this._activatedRoute.snapshot.queryParamMap.get('redirectURL') || '/signed-in-redirect';
// Navigate to the redirect url
this._router.navigateByUrl(redirectURL); this._router.navigateByUrl(redirectURL);
} else if (response == "unregistered") {
this.hide = true;
} else if (response == "wrong verification code") {
this.signInForm.enable();
grecaptcha.reset(this.recaptchedID);
this.alert = {
type: "error",
message: "You enter the wrong OTP code. Try again (" + this.tryagin + "/3)"
}
if (this.previousNumber != this.phonenumber) {
this.tryagin = 1;
}
// this.signInForm.reset();
this.previousNumber = this.phonenumber;
this.showAlert = true;
} else if (response == "resend") {
this.signInForm.enable();
grecaptcha.reset(this.recaptchedID);
this.alert = {
type: "error",
message: "Make sure you entered the right phone number and try again (" + this.tryagin + "/3)"
}
if (this.previousNumber != this.phonenumber) {
this.tryagin = 1;
}
// this.signInForm.reset();
this.previousNumber = this.phonenumber;
this.showAlert = true;
}
else {
this.signInForm.enable();
// this.signInForm.reset();
grecaptcha.reset(this.recaptchedID);
this.previousNumber = this.phonenumber;
this.alert = {
type: "error",
message: "An error occurred while authenticating you please try again after a while. " +
"This could be as a result of entering the OTP or poor internet connection"
}
this.showAlert = true;
}
});
}).catch((error) => {
// this.signInForm.reset();
grecaptcha.reset(this.recaptchedID);
this.previousNumber = this.phonenumber;
});
} else {
// this.signInForm.reset();
grecaptcha.reset(this.recaptchedID);
this.previousNumber = this.phonenumber;
if (this.tryagin >= 3) {
this.alert = {
type: "error",
message: "An error occurred while authenticating you please try again after a while. This could be as a result of entering the OTP"
}
this.showAlert = true;
} else {
this.alert = {
type: "error",
message: "An error occurred while authenticating you please try again after a while this could poor internet connection"
}
this.showAlert = true;
}
}
}, }
(response) => {
// Re-enable the form
signInForm.enable();
// Reset the form
// Set the alert onFileSelected(event) {
this.alert = { const file: File = event.target.files[0];
type: 'error', if (file.type.includes("image")) {
message: 'Wrong email or password' this.newProfilePicture = {
file: file,
imageSRC: "",
}; };
let reader = new FileReader();
reader.onload = (event: any) => {
this.newProfilePicture.imageSRC = event.target.result;
}
reader.readAsDataURL(file);
}
}
// Show the alert clear() {
this.showAlert = true; console.log(this.newProfilePicture)
this.newProfilePicture = {};
}
signUP() {
this._authService.signUp(this.phonenumber, this.businessName, this.digitalAddress, this.newProfilePicture).subscribe((response) => {
if (response.business != undefined) {
const redirectURL = this._activatedRoute.snapshot.queryParamMap.get('redirectURL') || '/signed-in-redirect';
this._router.navigateByUrl(redirectURL);
} }
); })
} }
} }

8
src/app/pages/auth/sign-in/sign-in.module.ts

@ -13,10 +13,12 @@ import { AuthSignInComponent } from 'app/pages/auth/sign-in/sign-in.component';
import { authSignInRoutes } from 'app/pages/auth/sign-in/sign-in.routing'; import { authSignInRoutes } from 'app/pages/auth/sign-in/sign-in.routing';
import { NgxMatIntlTelInputModule } from 'ngx-mat-intl-tel-input'; import { NgxMatIntlTelInputModule } from 'ngx-mat-intl-tel-input';
import {MatCardModule} from '@angular/material/card'; import {MatCardModule} from '@angular/material/card';
import { CommonModule } from '@angular/common';
@NgModule({ @NgModule({
declarations: [ declarations: [
AuthSignInComponent AuthSignInComponent,
], ],
imports : [ imports : [
RouterModule.forChild(authSignInRoutes), RouterModule.forChild(authSignInRoutes),
@ -30,8 +32,10 @@ import {MatCardModule} from '@angular/material/card';
MatProgressSpinnerModule, MatProgressSpinnerModule,
tesoCardModule, tesoCardModule,
tesoAlertModule, tesoAlertModule,
SharedModule SharedModule,
CommonModule,
] ]
}) })
export class AuthSignInModule export class AuthSignInModule
{ {

106
src/app/pages/auth/sign-in/signOld.txt

@ -1,106 +0,0 @@
<!-- <div class="flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-auto min-w-0">
<div *ngIf="isScreenSmall"
class="md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-4 sm:p-12 md:p-16 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none sm:bg-card">
<div class="w-full max-w-80 sm:w-80 mx-auto sm:mx-0">
<div class="w-full sm:w-auto py-8 px-4 sm:p-12 sm:rounded-2xl sm:shadow sm:bg-card">
<!-- Logo -->
<div class="w-20">
<img src="assets/images/logo.png">
</div>
<!-- Title -->
<div class="mt-8 text-4xl font-extrabold tracking-tight leading-tight">Sign in</div>
<div class="flex items-baseline mt-0.5 font-medium">
<div>Enter phone number of business</div>
</div>
<!-- Alert -->
<teso-alert class="mt-8 -mb-4" *ngIf="showAlert" [appearance]="'outline'" [showIcon]="false"
[type]="alert.type" [@shake]="alert.type === 'error'">
{{alert.message}}
</teso-alert>
<!-- Sign in form -->
<mat-form-field appearance="outline">
<ngx-mat-intl-tel-input [preferredCountries]="['gh', 'ng']" [onlyCountries]="['gh', 'ng']"
[enablePlaceholder]="true" [enableSearch]="false" name="phone">
</ngx-mat-intl-tel-input>
<mat-hint>Standard call, message, or data rates may apply.</mat-hint>
</mat-form-field>
<!-- Actions -->
<div class="inline-flex items-end justify-between w-full mt-3.5">
<mat-checkbox [color]="'primary'">
Remember me
</mat-checkbox>
</div>
<!-- Submit button -->
<button class="teso-mat-button-large w-full mt-6" mat-flat-button [color]="'primary'"
(click)="signIn()">
<span>
Sign in
</span>
<!-- <mat-progress-spinner
*ngIf="signInForm.disabled"
[diameter]="24"
[mode]="'indeterminate'"></mat-progress-spinner> -->
</button>
</div>
</div>
</div>
<div id="welcomeBoard"
class="relative hidden md:flex flex-auto items-center justify-center w-1/2 h-full p-5 lg:px-20 overflow-hidden bg-gray-800 dark:border-l">
<div class="w-full max-w-80 sm:w-80 mx-auto sm:mx-0">
<div class="w-full sm:w-auto py-5 px-4 sm:p-8 sm:rounded-2xl sm:shadow sm:bg-card" style="background-color: #484444c4 !important;color: white !important;">
<!-- Logo -->
<div class="w-30" style="width: 100%;justify-content: center;display: flex;">
<img src="assets/images/logo.png">
</div>
<!-- Title -->
<div class="mt-8 text-4xl font-extrabold tracking-tight leading-tight">Sign in</div>
<div class="flex items-baseline mt-0.5 font-medium">
<div>Enter phone number of business</div>
</div>
<!-- Alert -->
<teso-alert class="mt-8 -mb-4" *ngIf="showAlert" [appearance]="'outline'" [showIcon]="false"
[type]="alert.type" [@shake]="alert.type === 'error'">
{{alert.message}}
</teso-alert>
<!-- Sign in form -->
<mat-form-field appearance="outline">
<ngx-mat-intl-tel-input [preferredCountries]="['gh', 'ng']" [onlyCountries]="['gh', 'ng']"
[enablePlaceholder]="true" [enableSearch]="false" name="phone">
</ngx-mat-intl-tel-input>
<mat-hint style="color: white !important;">Standard call, message, or data rates may apply.</mat-hint>
</mat-form-field>
<!-- Actions -->
<div class="inline-flex items-end justify-between w-full mt-3.5">
<mat-checkbox [color]="'primary'">
Remember me
</mat-checkbox>
</div>
<!-- Submit button -->
<button class="teso-mat-button-large w-full mt-6" mat-flat-button [color]="'primary'"
(click)="signIn()">
<span>
Sign in
</span>
<!-- <mat-progress-spinner
*ngIf="signInForm.disabled"
[diameter]="24"
[mode]="'indeterminate'"></mat-progress-spinner> -->
</button>
</div>
</div>
</div>
</div> -->

136
src/app/pages/auth/sign-up/sign-up.component.html

@ -1,136 +0,0 @@
<div class="flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-auto min-w-0">
<div class="md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-4 sm:p-12 md:p-16 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none sm:bg-card">
<div class="w-full max-w-80 sm:w-80 mx-auto sm:mx-0">
<!-- Logo -->
<div class="w-12">
<img src="assets/images/logo/logo.svg">
</div>
<!-- Title -->
<div class="mt-8 text-4xl font-extrabold tracking-tight leading-tight">Sign up</div>
<div class="flex items-baseline mt-0.5 font-medium">
<div>Already have an account?</div>
<a class="ml-1 text-primary-500 hover:underline" [routerLink]="['/pages/authentication/sign-in']">Sign in
</a>
</div>
<!-- Alert -->
<teso-alert class="mt-8 -mb-4" *ngIf="showAlert" [appearance]="'outline'" [showIcon]="false" [type]="alert.type" [@shake]="alert.type === 'error'">
{{alert.message}}
</teso-alert>
<!-- Sign Up form -->
<form class="mt-8" [formGroup]="signUpForm">
<!-- Name field -->
<mat-form-field class="w-full">
<mat-label>Full name</mat-label>
<input id="name" matInput [formControlName]="'name'">
<mat-error *ngIf="signUpForm.get('name').hasError('required')">
Full name is required
</mat-error>
</mat-form-field>
<!-- Email field -->
<mat-form-field class="w-full">
<mat-label>Email address</mat-label>
<input id="email" matInput [formControlName]="'email'">
<mat-error *ngIf="signUpForm.get('email').hasError('required')">
Email address is required
</mat-error>
<mat-error *ngIf="signUpForm.get('email').hasError('email')">
Please enter a valid email address
</mat-error>
</mat-form-field>
<!-- Password field -->
<mat-form-field class="w-full">
<mat-label>Password</mat-label>
<input id="password" matInput type="password" [formControlName]="'password'" #passwordField>
<button mat-icon-button type="button" (click)="passwordField.type === 'password' ? passwordField.type = 'text' : passwordField.type = 'password'" matSuffix>
<mat-icon
class="icon-size-5"
*ngIf="passwordField.type === 'password'"
[svgIcon]="'heroicons_solid:eye'"></mat-icon>
<mat-icon
class="icon-size-5"
*ngIf="passwordField.type === 'text'"
[svgIcon]="'heroicons_solid:eye-off'"></mat-icon>
</button>
<mat-error>
Password is required
</mat-error>
</mat-form-field>
<!-- Company field -->
<mat-form-field class="w-full">
<mat-label>Company</mat-label>
<input id="company-confirm" matInput [formControlName]="'company'">
</mat-form-field>
<!-- ToS and PP -->
<div class="inline-flex items-end w-full mt-1.5">
<mat-checkbox [color]="'primary'" [formControlName]="'agreements'">
<span>I agree to the</span>
<a class="ml-1 text-primary-500 hover:underline" [routerLink]="['./']">Terms of Service
</a>
<span>and</span>
<a class="ml-1 text-primary-500 hover:underline" [routerLink]="['./']">Privacy Policy
</a>
</mat-checkbox>
</div>
<!-- Submit button -->
<button class="teso-mat-button-large w-full mt-6" mat-flat-button [color]="'primary'" [disabled]="signUpForm.disabled" (click)="signUp()">
<span *ngIf="!signUpForm.disabled">
Create your free account
</span>
<mat-progress-spinner
*ngIf="signUpForm.disabled"
[diameter]="24"
[mode]="'indeterminate'"></mat-progress-spinner>
</button>
</form>
</div>
</div>
<div class="relative hidden md:flex flex-auto items-center justify-center w-1/2 h-full p-16 lg:px-28 overflow-hidden bg-gray-800 dark:border-l">
<!-- Background - @formatter:off -->
<!-- Rings -->
<svg class="absolute inset-0 pointer-events-none" viewBox="0 0 960 540" width="100%" height="100%" preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
<circle r="234" cx="196" cy="23"></circle>
<circle r="234" cx="790" cy="491"></circle>
</g>
</svg>
<!-- Dots -->
<svg class="absolute -top-16 -right-16 text-gray-700" viewBox="0 0 220 192" width="220" height="192" fill="none">
<defs>
<pattern id="837c3e70-6c3a-44e6-8854-cc48c737b659" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<rect x="0" y="0" width="4" height="4" fill="currentColor"></rect>
</pattern>
</defs>
<rect width="220" height="192" fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)"></rect>
</svg>
<!-- @formatter:on -->
<!-- Content -->
<div class="z-10 relative w-full max-w-2xl">
<div class="text-7xl font-bold leading-none text-gray-100">
<div>Welcome to</div>
<div>our community</div>
</div>
<div class="mt-6 text-lg tracking-tight leading-6 text-gray-400">
teso helps developers to build organized and well coded dashboards full of beautiful and rich modules. Join us and start building your application today.
</div>
<div class="flex items-center mt-8">
<div class="flex flex-0 items-center -space-x-1.5">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/female-18.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/female-11.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/male-09.jpg">
<img class="flex-0 w-10 h-10 rounded-full ring-4 ring-offset-1 ring-gray-800 ring-offset-gray-800 object-cover" src="assets/images/avatars/male-16.jpg">
</div>
<div class="ml-4 font-medium tracking-tight text-gray-400">More than 17k people joined us, it's your turn</div>
</div>
</div>
</div>
</div>

104
src/app/pages/auth/sign-up/sign-up.component.ts

@ -1,104 +0,0 @@
import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { tesoAnimations } from '@teso/animations';
import { tesoAlertType } from '@teso/components/alert';
import { AuthService } from 'app/core/auth/auth.service';
@Component({
selector : 'auth-sign-up',
templateUrl : './sign-up.component.html',
encapsulation: ViewEncapsulation.None,
animations : tesoAnimations
})
export class AuthSignUpComponent implements OnInit
{
@ViewChild('signUpNgForm') signUpNgForm: NgForm;
alert: { type: tesoAlertType; message: string } = {
type : 'success',
message: ''
};
signUpForm: FormGroup;
showAlert: boolean = false;
/**
* Constructor
*/
constructor(
private _authService: AuthService,
private _formBuilder: FormBuilder,
private _router: Router
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Create the form
this.signUpForm = this._formBuilder.group({
name : ['', Validators.required],
email : ['', [Validators.required, Validators.email]],
password : ['', Validators.required],
company : [''],
agreements: ['', Validators.requiredTrue]
}
);
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Sign up
*/
signUp(): void
{
// Do nothing if the form is invalid
if ( this.signUpForm.invalid )
{
return;
}
// Disable the form
this.signUpForm.disable();
// Hide the alert
this.showAlert = false;
// Sign up
this._authService.signUp(this.signUpForm.value)
.subscribe(
(response) => {
// Navigate to the confirmation required page
this._router.navigateByUrl('/confirmation-required');
},
(response) => {
// Re-enable the form
this.signUpForm.enable();
// Reset the form
this.signUpNgForm.resetForm();
// Set the alert
this.alert = {
type : 'error',
message: 'Something went wrong, please try again.'
};
// Show the alert
this.showAlert = true;
}
);
}
}

34
src/app/pages/auth/sign-up/sign-up.module.ts

@ -1,34 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { tesoCardModule } from '@teso/components/card';
import { tesoAlertModule } from '@teso/components/alert';
import { SharedModule } from 'app/shared/shared.module';
import { AuthSignUpComponent } from 'app/pages/auth/sign-up/sign-up.component';
import { authSignupRoutes } from 'app/pages/auth/sign-up/sign-up.routing';
@NgModule({
declarations: [
AuthSignUpComponent
],
imports : [
RouterModule.forChild(authSignupRoutes),
MatButtonModule,
MatCheckboxModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatProgressSpinnerModule,
tesoCardModule,
tesoAlertModule,
SharedModule
]
})
export class AuthSignUpModule
{
}

9
src/app/pages/auth/sign-up/sign-up.routing.ts

@ -1,9 +0,0 @@
import { Route } from '@angular/router';
import { AuthSignUpComponent } from 'app/pages/auth/sign-up/sign-up.component';
export const authSignupRoutes: Route[] = [
{
path : '',
component: AuthSignUpComponent
}
];

25
src/app/pages/auth/verification/verification-dialog.component.html

@ -0,0 +1,25 @@
<div class="d-flex justify-content-center align-items-center">
<div class="position-relative">
<div class="card p-2 text-center" style="display: contents;">
<div class="w-30" style="width: 100% !important;justify-content: center;display: flex;">
<img src="assets/images/logo.png" style="width:140px">
</div>
<h6>Please enter the one time password <br> </h6>
<div> <span> sent to {{data.phone}}</span> </div>
<div id="otp" class="inputs d-flex flex-row justify-content-center mt-2">
<ng-otp-input (onInputChange)="onOtpChange($event)" [config]="config"></ng-otp-input>
</div>
<div class="mt-4"> <button class="btn btn-danger px-4 validate" mat-flat-button (click)="verifyCode()"
*ngIf="!signInForm.disabled"> <span>
Verify
</span>
</button> </div>
<div class="lds-dual-ring" *ngIf="signInForm.disabled"></div>
<div class="mt-3 content flex justify-content-center align-items-center"
style="width: 100% !important;justify-content: space-between !important;">
<span>Didn't get the code ? </span>
<a href="javascript:void(0)" class="text-decoration-none ms-3" (click)="closeDialog()">Try again</a>
</div>
</div>
</div>
</div>

82
src/app/pages/auth/verification/verification-dialog.component.scss

@ -0,0 +1,82 @@
.height-100 {
height: 100vh
}
.card {
width: 400px;
border: none;
height: 300px;
box-shadow: 0px 5px 20px 0px #252f3b;
z-index: 1;
display: flex;
justify-content: center;
align-items: center
}
.card h6 {
color: #252f3b;
font-size: 20px
}
.inputs input {
width: 40px;
height: 40px
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
margin: 0
}
.form-control:focus {
box-shadow: none;
border: 2px solid #252f3b
}
.validate {
border-radius: 20px;
height: 40px;
background-color: #252f3b;
border: 1px solid #252f3b;
width: 100%;
color:white;
}
.content a {
color: royalblue;
transition: all 0.5s
}
.content a:hover {
color: rgb(179, 155, 24)
}
.lds-dual-ring {
display: flex;
justify-content: center;
width: 100%;
height: 40px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 40px;
height: 40px;
margin: 8px;
border-radius: 50%;
border: 6px solid #003445;
border-color: #003445 transparent #003445 transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

90
src/app/pages/auth/verification/verification-dialog.component.ts

@ -0,0 +1,90 @@
import { Component, Inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { AuthService } from 'app/core/auth/auth.service';
import { WindowService } from 'app/window.service';
import { NgOtpInputComponent, NgOtpInputConfig } from 'ng-otp-input';
@Component({
selector: 'app-verification-dialog',
templateUrl: './verification-dialog.component.html',
styleUrls: ['./verification-dialog.component.scss'],
encapsulation: ViewEncapsulation.Emulated,
})
export class VerificationDialogComponent implements OnInit {
@ViewChild(NgOtpInputComponent, { static: false }) ngOtpInput: NgOtpInputComponent;
config: NgOtpInputConfig = {
allowNumbersOnly: true,
length: 6,
isPasswordInput: false,
disableAutoFocus: false,
placeholder: ''
};
otp: string;
showOtpComponent = true;
signInForm: FormGroup;
verificationCode;
errorString = "FirebaseError: Firebase: The SMS verification code used to create the phone auth credential is invalid. Please resend the verification code sms and be sure to use the verification code provided by the user"
constructor(@Inject(MAT_DIALOG_DATA) public data: { windowRef: any, phone: String },
public dialogRef: MatDialogRef<VerificationDialogComponent>,
private _authService: AuthService, private fb: FormBuilder
) {
this.signInForm = this.fb.group({
phone: [undefined, [Validators.required]],
});
}
ngOnInit(): void {
// this.verifyCode();
}
verifyCode() {
this.signInForm.disable();
this.data.windowRef.confirmationResult
.confirm(this.verificationCode)
.then(result => {
if (result.user !== "") {
this._authService.authenticate(this.data.phone)
.subscribe(
() => {
this.dialogRef.close("verified")
},
(response) => {
if (response.error = "Unregistered") {
this.dialogRef.close("unregistered")
} else {
this.dialogRef.close("error")
}
}
);
}
}).catch((error) => {
if (error.toString().includes(this.errorString)) {
this.dialogRef.close("wrong verification code")
} else {
this.dialogRef.close("error")
}
});
}
closeDialog(){
this.dialogRef.close("resend")
}
onOtpChange(otp) {
this.otp = otp;
if (otp.toString().length == 6)
this.verificationCode = otp;
}
toggleDisable() {
if (this.ngOtpInput.otpForm) {
if (this.ngOtpInput.otpForm.disabled) {
this.ngOtpInput.otpForm.enable();
} else {
this.ngOtpInput.otpForm.disable();
}
}
}
}

13
src/app/pipes/percentage-pipe.ts

@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'percentage'
})
export class PercentagePipe implements PipeTransform {
transform(value: number): string {
let newvale = parseFloat((Math.round(value*10^2)).toString());
return newvale + "%";
}
}

11
src/app/window.service.ts

@ -0,0 +1,11 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class WindowService {
get windowRef() {
return window
}
}

BIN
src/assets/icons/notransaction.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

12
src/environments/environment.prod.ts

@ -1,5 +1,15 @@
export const environment = { export const environment = {
production: true, production: true,
apiURL:"https://test.tesoapp.com/v2/" apiURL:"https://test.tesoapp.com/v2/",
// apiURL:"https://localhost:7076/" // apiURL:"https://localhost:7076/"
firebaseConfig :{
apiKey: "AIzaSyDrFGHgBJxi4EgCThwBZaT7IUtQUhbhS8s",
authDomain: "teso-ghana.firebaseapp.com",
databaseURL: "https://teso-ghana-default-rtdb.firebaseio.com",
projectId: "teso-ghana",
storageBucket: "teso-ghana.appspot.com",
messagingSenderId: "280510379185",
appId: "1:280510379185:web:7a3e0afbfd43e93d19de90",
measurementId: "G-82HYCB3QDE"
}
}; };

14
src/environments/environment.ts

@ -4,8 +4,18 @@
export const environment = { export const environment = {
production: false, production: false,
apiURL:"https://test.tesoapp.com/v2/" apiURL:"https://test.tesoapp.com/v2/",
// apiURL:"https://localhost:7076/" // apiURL:"https://localhost:7076/",
firebaseConfig :{
apiKey: "AIzaSyDrFGHgBJxi4EgCThwBZaT7IUtQUhbhS8s",
authDomain: "teso-ghana.firebaseapp.com",
databaseURL: "https://teso-ghana-default-rtdb.firebaseio.com",
projectId: "teso-ghana",
storageBucket: "teso-ghana.appspot.com",
messagingSenderId: "280510379185",
appId: "1:280510379185:web:7a3e0afbfd43e93d19de90",
measurementId: "G-82HYCB3QDE"
}
}; };
/* /*

Loading…
Cancel
Save