From 4369ebb2a5f1ee014bf1fe5386c8464f255fce01 Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Wed, 19 Jun 2024 13:56:56 +0700 Subject: [PATCH] feat(deps): :sparkles: Add support for more departments --- client/src/app/app-routing.module.ts | 92 +++++++-------- client/src/app/core/auth/userPermissions.ts | 23 +++- .../app/core/callback/callback.component.ts | 37 +++--- .../core/department/department.component.ts | 105 ++++++++++++------ client/src/app/shared/helpers/debugger.ts | 24 +++- .../src/app/shared/helpers/notFoundHandler.ts | 16 ++- client/src/app/shared/helpers/recipe.ts | 3 + .../environments/environment.development.ts | 3 +- client/src/environments/environment.ts | 3 +- server/country.settings.json | 98 +++++++++------- server/data/data.go | 20 ++++ server/data/database.db | Bin 36864 -> 36864 bytes server/git_recipe_worker.go | 3 +- server/helpers/filereader.go | 24 ++-- server/middlewares/authorized.go | 2 + server/routers/recipe.go | 4 +- server/routers/user.go | 4 + server/server.go | 3 + server/services/logger/logger.go | 1 + 19 files changed, 296 insertions(+), 169 deletions(-) diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 8a37a05..c9252c8 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -1,4 +1,4 @@ -import { inject, NgModule } from '@angular/core'; +import { inject, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, @@ -6,14 +6,14 @@ import { RouterModule, RouterStateSnapshot, Routes, -} from '@angular/router'; -import { UserService } from './core/services/user.service'; -import { map } from 'rxjs'; -import { UserPermissions } from './core/auth/userPermissions'; +} from "@angular/router"; +import { UserService } from "./core/services/user.service"; +import { map } from "rxjs"; +import { UserPermissions } from "./core/auth/userPermissions"; const authGuard: CanActivateFn = ( route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + state: RouterStateSnapshot, ) => { const userService: UserService = inject(UserService); const router: Router = inject(Router); @@ -23,12 +23,12 @@ const authGuard: CanActivateFn = ( if (isAuth) { return true; } - return router.createUrlTree(['/login'], { + return router.createUrlTree(["/login"], { queryParams: { redirectUrl: state.url, }, }); - }) + }), ); }; @@ -42,21 +42,23 @@ const permissionsGuard: ( const user = userService.getCurrentUser(); if (user == null) - return router.createUrlTree(['/login'], { + return router.createUrlTree(["/login"], { queryParams: { redirectUrl: state.url, }, }); + console.log("require following permissions", requiredPermissions); + if ( requiredPermissions.every((permission) => - user.permissions.includes(permission) + user.permissions.includes(permission), ) ) { return true; } - return router.createUrlTree(['/unauthorized'], { + return router.createUrlTree(["/unauthorized"], { queryParams: { redirectUrl: state.url, }, @@ -73,47 +75,47 @@ const loginGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => { return true; } // redirect to redirectUrl query param - console.log("redirectURL", route.queryParams['redirectUrl']); + console.log("redirectURL", route.queryParams["redirectUrl"]); return router.createUrlTree([ - router.parseUrl(route.queryParams['redirectUrl'] ?? 'departments'), + router.parseUrl(route.queryParams["redirectUrl"] ?? "departments"), ]); // return false; - }) + }), ); }; const routes: Routes = [ { - path: 'login', + path: "login", loadComponent: () => - import('./core/auth/auth.component').then((m) => m.AuthComponent), + import("./core/auth/auth.component").then((m) => m.AuthComponent), canActivate: [loginGuard], }, { - path: 'callback', + path: "callback", loadComponent: () => - import('./core/callback/callback.component').then( - (m) => m.CallbackComponent + import("./core/callback/callback.component").then( + (m) => m.CallbackComponent, ), }, { - path: 'departments', + path: "departments", loadComponent: () => - import('./core/department/department.component').then( - (m) => m.DepartmentComponent + import("./core/department/department.component").then( + (m) => m.DepartmentComponent, ), canActivate: [authGuard], }, { - path: ':department', + path: ":department", loadComponent: () => - import('./core/layout/layout.component').then((m) => m.LayoutComponent), + import("./core/layout/layout.component").then((m) => m.LayoutComponent), children: [ { - path: 'recipes', + path: "recipes", loadComponent: () => - import('./features/recipes/recipes.component').then( - (m) => m.RecipesComponent + import("./features/recipes/recipes.component").then( + (m) => m.RecipesComponent, ), canActivate: [ authGuard, @@ -121,10 +123,10 @@ const routes: Routes = [ ], }, { - path: 'recipe/:productCode', + path: "recipe/:productCode", loadComponent: () => import( - './features/recipes/recipe-details/recipe-details.component' + "./features/recipes/recipe-details/recipe-details.component" ).then((m) => m.RecipeDetailsComponent), canActivate: [ authGuard, @@ -132,38 +134,38 @@ const routes: Routes = [ ], }, { - path: 'log', + path: "log", loadComponent: () => - import('./features/changelog/changelog.component').then( - (m) => m.ChangelogComponent + import("./features/changelog/changelog.component").then( + (m) => m.ChangelogComponent, ), }, { - path: 'materials', + path: "materials", loadComponent: () => import( - './features/material-settings/material-settings.component' + "./features/material-settings/material-settings.component" ).then((m) => m.MaterialSettingsComponent), }, { - path: 'toppings', + path: "toppings", loadComponent: () => - import('./features/toppings/toppings.component').then( - (t) => t.ToppingsComponent + import("./features/toppings/toppings.component").then( + (t) => t.ToppingsComponent, ), }, ], }, { - path: '', - pathMatch: 'full', - redirectTo: 'departments', + path: "", + pathMatch: "full", + redirectTo: "departments", }, { - path: 'unauthorized', + path: "unauthorized", loadComponent: () => - import('./core/auth/unauthorized.component').then( - (m) => m.UnauthorizedComponent + import("./core/auth/unauthorized.component").then( + (m) => m.UnauthorizedComponent, ), }, // { @@ -172,8 +174,8 @@ const routes: Routes = [ // import('./core/notfound.component').then((m) => m.NotfoundComponent), // }, { - path: '**', - redirectTo: 'departments', + path: "**", + redirectTo: "departments", }, ]; diff --git a/client/src/app/core/auth/userPermissions.ts b/client/src/app/core/auth/userPermissions.ts index 78b2a18..7935d40 100644 --- a/client/src/app/core/auth/userPermissions.ts +++ b/client/src/app/core/auth/userPermissions.ts @@ -8,13 +8,28 @@ export enum UserPermissions { VIEWER = 1 << 4, EDITOR = 1 << 7, + DUBAI_PERMISSION = 1 << 8, + COUNTER_PERMISSION = 1 << 9, + SINGAPORE_PERMISSION = 1 << 10, + COCKTAIL_PERMISSION = 1 << 11, + // add new permission by shifting after 7. eg. 8,9,... // also do add at server - SUPER_ADMIN_PERMISSION = THAI_PERMISSION | MALAY_PERMISSION | AUS_PERMISSION | ALPHA3_PERMISSION | (EDITOR | VIEWER) + SUPER_ADMIN_PERMISSION = THAI_PERMISSION | + MALAY_PERMISSION | + AUS_PERMISSION | + ALPHA3_PERMISSION | + COUNTER_PERMISSION | + SINGAPORE_PERMISSION | + DUBAI_PERMISSION | + COCKTAIL_PERMISSION | + (EDITOR | VIEWER), } -export function getPermissions(perms: number) : UserPermissions[] { - return Object.values(UserPermissions) - .filter(permission => typeof permission === 'number' && (perms & permission) !== 0) as UserPermissions[]; +export function getPermissions(perms: number): UserPermissions[] { + return Object.values(UserPermissions).filter( + (permission) => + typeof permission === "number" && (perms & permission) !== 0, + ) as UserPermissions[]; } diff --git a/client/src/app/core/callback/callback.component.ts b/client/src/app/core/callback/callback.component.ts index 8f6986d..e485f17 100644 --- a/client/src/app/core/callback/callback.component.ts +++ b/client/src/app/core/callback/callback.component.ts @@ -1,37 +1,42 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { UserService } from '../services/user.service'; -import {getPermissions} from "../auth/userPermissions"; +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { UserService } from "../services/user.service"; +import { getPermissions } from "../auth/userPermissions"; @Component({ - selector: 'app-callback', + selector: "app-callback", standalone: true, - templateUrl: './callback.component.html', + templateUrl: "./callback.component.html", }) export class CallbackComponent implements OnInit { constructor( private route: ActivatedRoute, private router: Router, - private userService: UserService + private userService: UserService, ) {} ngOnInit(): void { this.route.queryParams.subscribe((params) => { - console.log(params); + console.log(params, params["permissions"]); - if (params['email'] && params['name'] && params['picture'] && params['permissions']) { + if ( + params["email"] && + params["name"] && + params["picture"] && + params["permissions"] + ) { this.userService.setAuth({ - email: params['email'], - name: params['name'], - picture: params['picture'], - permissions: getPermissions(params['permissions']), + email: params["email"], + name: params["name"], + picture: params["picture"], + permissions: getPermissions(params["permissions"]), }); } - if (params['redirect_to']) { - void this.router.navigate([params['redirect_to']]); + if (params["redirect_to"]) { + void this.router.navigate([params["redirect_to"]]); } else { - void this.router.navigate(['/']); + void this.router.navigate(["/"]); } }); } diff --git a/client/src/app/core/department/department.component.ts b/client/src/app/core/department/department.component.ts index 9958071..d74dcb0 100644 --- a/client/src/app/core/department/department.component.ts +++ b/client/src/app/core/department/department.component.ts @@ -1,11 +1,11 @@ -import { Component } from '@angular/core'; -import { CommonModule, NgOptimizedImage } from '@angular/common'; -import { Router } from '@angular/router'; -import { UserService } from '../services/user.service'; -import { UserPermissions } from '../auth/userPermissions'; -import { NotFoundHandler } from 'src/app/shared/helpers/notFoundHandler'; -import { AsyncStorage } from 'src/app/shared/helpers/asyncStorage'; -import { getCountryMapSwitcher } from 'src/app/shared/helpers/recipe'; +import { Component } from "@angular/core"; +import { CommonModule, NgOptimizedImage } from "@angular/common"; +import { Router } from "@angular/router"; +import { UserService } from "../services/user.service"; +import { UserPermissions } from "../auth/userPermissions"; +import { NotFoundHandler } from "src/app/shared/helpers/notFoundHandler"; +import { AsyncStorage } from "src/app/shared/helpers/asyncStorage"; +import { getCountryMapSwitcher } from "src/app/shared/helpers/recipe"; @Component({ standalone: true, @@ -56,33 +56,43 @@ import { getCountryMapSwitcher } from 'src/app/shared/helpers/recipe'; `, }) - - export class DepartmentComponent { - acccessibleCountries:string[] = []; + acccessibleCountries: string[] = []; // top row country countries: { id: string; img: string }[] = [ { - id: 'tha', - img: 'assets/departments/tha_plate.png', + id: "tha", + img: "assets/departments/tha_plate.png", }, { - id: 'mys', - img: 'assets/departments/mys_plate.png', + id: "mys", + img: "assets/departments/mys_plate.png", }, { - id: 'aus', - img: 'assets/departments/aus_plate.png', + id: "aus", + img: "assets/departments/aus_plate.png", }, // add new country here! + { + id: "sgp", + img: "assets/departments/sgp_plate.png", + }, + { + id: "counter", + img: "assets/departments/counter01_plate.png", + }, ]; // bottom row country alphas: { id: string; img: string }[] = [ { - id: 'alpha-3', - img: 'assets/departments/alpha-3.png', + id: "alpha-3", + img: "assets/departments/alpha-3.png", + }, + { + id: "cocktail", + img: "assets/departments/cocktail_tha.png", }, ]; @@ -91,45 +101,66 @@ export class DepartmentComponent { constructor( private router: Router, - private _userService: UserService + private _userService: UserService, ) { let perms = _userService.getCurrentUser()!.permissions; - console.log("GainAccesses",perms) + console.log("GainAccesses", perms); for (let perm of perms) { switch (perm) { case UserPermissions.THAI_PERMISSION: - this.acccessibleCountries.push('tha'); + this.acccessibleCountries.push("tha"); break; case UserPermissions.MALAY_PERMISSION: - this.acccessibleCountries.push('mys'); + this.acccessibleCountries.push("mys"); break; case UserPermissions.AUS_PERMISSION: - this.acccessibleCountries.push('aus'); + this.acccessibleCountries.push("aus"); break; case UserPermissions.ALPHA3_PERMISSION: - this.acccessibleCountries.push('alpha-3'); + this.acccessibleCountries.push("alpha-3"); + break; + case UserPermissions.SINGAPORE_PERMISSION: + this.acccessibleCountries.push("sgp"); + break; + case UserPermissions.COUNTER_PERMISSION: + this.acccessibleCountries.push("counter"); + break; + case UserPermissions.DUBAI_PERMISSION: + this.acccessibleCountries.push("dubai"); + break; + case UserPermissions.COCKTAIL_PERMISSION: + this.acccessibleCountries.push("cocktail"); break; default: break; } } + + console.log("OK", this.acccessibleCountries); } onClick(id: string) { // add handler for redirect - this.notfoundHandler.handleSwitchCountry(id, async () => { - // set country - await AsyncStorage.setItem('currentRecipeCountry', getCountryMapSwitcher(id)); - // set filename, don't know which file was a target so use default - await AsyncStorage.setItem('currentRecipeFile', 'default'); - }, async () => { - // set country to `tha` - await AsyncStorage.setItem('currentRecipeCountry', 'Thailand'); - // set filename, don't know which file was a target so use default - await AsyncStorage.setItem('currentRecipeFile', 'default'); - // safely return to recipes - }); + this.notfoundHandler.handleSwitchCountry( + id, + async () => { + // set country + await AsyncStorage.setItem( + "currentRecipeCountry", + getCountryMapSwitcher(id), + ); + // set filename, don't know which file was a target so use default + await AsyncStorage.setItem("currentRecipeFile", "default"); + }, + async () => { + // set country to `tha` + await AsyncStorage.setItem("currentRecipeCountry", "Thailand"); + // set filename, don't know which file was a target so use default + await AsyncStorage.setItem("currentRecipeFile", "default"); + // safely return to recipes + }, + ); void this.router.navigate([`/${id}/recipes`]); } } diff --git a/client/src/app/shared/helpers/debugger.ts b/client/src/app/shared/helpers/debugger.ts index ed5818b..3ee9194 100644 --- a/client/src/app/shared/helpers/debugger.ts +++ b/client/src/app/shared/helpers/debugger.ts @@ -133,7 +133,6 @@ export class Debugger { } }); break; - default: if(cmd.startsWith("fn::")){ @@ -204,6 +203,23 @@ export class Debugger { } else if(cmd.startsWith("var::")){ let varname = cmd.substring(5); this.output.push(JSON.stringify(holdon[varname])); + } else if(cmd.startsWith("load::")){ + + } else { + // assume varname + + + if(cmd.endsWith("#window")){ + let exposed: string = cmd.split(" ")[0]; + console.log("try exposed to window "+exposed); + let estr = "window."+exposed+" = \""+holdon[exposed]+"\""; + console.log("estr", estr); + let eval_res = eval(estr); + console.log("eval_res", eval_res); + this.output.push(JSON.stringify(eval_res)); + } else { + this.output.push(JSON.stringify(holdon[cmd])); + } } break; @@ -217,9 +233,9 @@ export class Debugger { } } else if (!line.startsWith("//") && line != ""){ - if(!line.startsWith("window") && !line.startsWith("let") && !line.startsWith("var")){ - line = "window."+line; - } + // if(!line.startsWith("window") && !line.startsWith("let") && !line.startsWith("var")){ + // line = "window."+line; + // } try { diff --git a/client/src/app/shared/helpers/notFoundHandler.ts b/client/src/app/shared/helpers/notFoundHandler.ts index 79f540d..2f1c059 100644 --- a/client/src/app/shared/helpers/notFoundHandler.ts +++ b/client/src/app/shared/helpers/notFoundHandler.ts @@ -4,7 +4,10 @@ export function departmentList() { 'tha', 'mys', 'aus', - 'alpha-3' + 'alpha-3', + 'sgp', + 'dubai', + 'counter' ]; } @@ -28,7 +31,16 @@ export class NotFoundHandler { handleSwitchCountry(country: string, callback: () => Promise, error?: () => Promise) { let checkCondition = departmentList().includes(country); if(checkCondition) { - callback(); + // callback(); + Promise.allSettled([ + Promise.resolve(callback()) + ]).then((results) => { + // console.log("Resolved callback: ",results) + if(results[0].status == 'fulfilled'){ + console.info("Callback executed successfully"); + } + }); + } else { if(error) error(); diff --git a/client/src/app/shared/helpers/recipe.ts b/client/src/app/shared/helpers/recipe.ts index 073276f..098556d 100644 --- a/client/src/app/shared/helpers/recipe.ts +++ b/client/src/app/shared/helpers/recipe.ts @@ -114,6 +114,9 @@ export var countryMap: Tuple[] = [ new Tuple('tha', 'Thailand'), new Tuple('mys', 'Malaysia'), new Tuple('aus', 'Australia'), + new Tuple('sgp', 'Singapore'), + new Tuple('dubai', 'UAE Dubai'), + new Tuple('counter', 'Counter') ]; export function getCountryMapSwitcher(param: string) { diff --git a/client/src/environments/environment.development.ts b/client/src/environments/environment.development.ts index dd9d9ef..b207d5b 100644 --- a/client/src/environments/environment.development.ts +++ b/client/src/environments/environment.development.ts @@ -1,4 +1,5 @@ export const environment = { production: false, - api: 'http://localhost:8080', + api: "http://localhost:8080", + imgApi: "http://localhost:36527", }; diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts index 16ac628..3ebd6d0 100644 --- a/client/src/environments/environment.ts +++ b/client/src/environments/environment.ts @@ -1,4 +1,5 @@ export const environment = { production: true, - api: 'https://recipe.taobin.io:8090/api', + api: "https://recipe.taobin.io:8090/api", + imgApi: "https://recipe.taobin.io:8090/img", }; diff --git a/server/country.settings.json b/server/country.settings.json index 63b9053..f9fce2d 100644 --- a/server/country.settings.json +++ b/server/country.settings.json @@ -1,44 +1,56 @@ [ - { - "ignore": true, - "name": "New Country in full name", - "short": "Short name", - "permissions": "use number after 128, do not use 16 and 128" - }, - { - "name": "Thailand", - "short": "tha", - "permissions": 1, - "dir": "tha" - }, - { - "name": "Malaysia", - "short": "mys", - "permissions": 2, - "dir": "mys" - }, - { - "name": "Australia", - "short": "aus", - "permissions": 4, - "dir": "aus" - }, - { - "name": "Alpha3", - "short": "alpha-3", - "permissions": 8, - "dir": "alpha-3" - }, - { - "name": "UAE Dubai", - "short": "uae-dubai", - "permissions": 256, - "dir": "dubai" - }, - { - "name": "Counter Cafe", - "short": "counter", - "permissions": 512, - "dir": "counter" - } -] \ No newline at end of file + { + "ignore": true, + "name": "New Country in full name", + "short": "Short name", + "permissions": "use number after 128, do not use 16 and 128" + }, + { + "name": "Thailand", + "short": "tha", + "permissions": 1, + "dir": "tha" + }, + { + "name": "Malaysia", + "short": "mys", + "permissions": 2, + "dir": "mys" + }, + { + "name": "Australia", + "short": "aus", + "permissions": 4, + "dir": "aus" + }, + { + "name": "Alpha3", + "short": "alpha-3", + "permissions": 8, + "dir": "alpha-3" + }, + { + "name": "UAE Dubai", + "short": "uae-dubai", + "permissions": 256, + "dir": "dubai" + }, + { + "name": "Counter Cafe", + "short": "counter", + "permissions": 512, + "dir": "counter01" + }, + { + "name": "Singapore", + "short": "sgp", + "permissions": 1024, + "dir": "sgp" + }, + { + "name": "Cocktail", + "short": "cocktail", + "permissions": 2048, + "dir": "cocktail_tha" + } +] diff --git a/server/data/data.go b/server/data/data.go index ebf4756..b44fd4a 100644 --- a/server/data/data.go +++ b/server/data/data.go @@ -183,6 +183,22 @@ var ( CountryID: "aus", CountryName: "Australia", }, + { + CountryID: "dubai", + CountryName: "UAE Dubai", + }, + { + CountryID: "counter01", + CountryName: "Counter Cafe", + }, + { + CountryID: "sgp", + CountryName: "Singapore", + }, + { + CountryID: "cocktail_tha", + CountryName: "Cocktail", + }, } ) @@ -304,6 +320,8 @@ func NewData(taoLogger *logger.TaoLogger, redisClient *RedisCli) *Data { // log.Panic("Error when read default recipe file:", err) // } + fmt.Println(CurrentCountryIDMap) + return &Data{ CurrentFile: currentFileMap, CurrentCountryID: CurrentCountryIDMap, @@ -1178,9 +1196,11 @@ func (d *Data) GetSubmenusOfRecipe(countryID, filename, productCode string) ([]m func (d *Data) GetCountryNameByID(countryID string) (string, error) { for _, country := range d.Countries { if country.CountryID == countryID { + fmt.Println("Found " + countryID) return country.CountryName, nil } } + fmt.Println("Not found " + countryID) return "", fmt.Errorf("country ID: %s not found", countryID) } diff --git a/server/data/database.db b/server/data/database.db index dbb70d0e2d0a7300c9b6bbb7fe73561f60228649..756734a6756e4ff1fad23e4f2b78c47201208d5f 100644 GIT binary patch literal 36864 zcmeI*%WoUU836EIt|*a0q;2XTtP8Xl_t1!lhMw7-eGw>N#ikn}vf{{O8v_Z5+1*)j zW4=tT>hw?)GJ>3P2nqzqrD$$F744Ay%ou&m(DIlJ(g5M5JBKL zA1RT`*~fg}Z-*oAa4g+>Z7)s)iUy+|Pmrm6M^RPf211IW2>D%>-_zUka$#lqLO!eK zmM>crN^SJf#^z^A>6zQg=KJ~w`j0nuH~+IKp1Hm8++{mKIS7CN2!H?xfB*=900@A< zlPqxjN@=Zf?V9?SC*1FfV9@KuNxM5ZJXqSS(q03dm7}foavY+c?Ht^E zdFNouu&gS&d$4)QiVsnc5ib?l;k`#_gTfUK5eYVdwCjZgR7AaM0Lqwo}u4 z&7C`UC%YUC!@T(AyR2`cVH_k!qe<}9>e_1MCzZ=UGm^HEJ+0j>tX5upQN5obp4gAY z!z0lTMDC?_0XDaCE2Z0(dsg)yD$iyoX0^1My@#r+Umgw3qDi+erTg?&)+*KO>am_R zfnUf|D{0$D{a6Mz4m$F5w0rU4C~HjPm6Nzm&dl!qt;Xx<;y&oreKfNl+B(^&8f$Bn z>#{m^qL8m(X3xS!iTw7b2S!J6Ke5?%7>|-)B7ZRGRySmJs{Xkmzu*P}AOHd&00JNY z0w4eaAOHd&00JQJGzpYdtyubQ^zfh8K1#3u<%j-?{>9UjAmjo9AOHd&00JNY0w4ea zAOHd&00JN&CpD;ArJPN1SeG2Cy!eOfe`psV00JNY0w4eaAOHd&00JNY0wD0T36!;B z`Rw!m;>IV6{+^CD|Frp%T!b44fB*=900@8p2!H?xfB*=9z!NVpDwbM5@Cv56VY)6S zo*m)9r6y)U6k;zj3HA(PTc+?VCt`g*ipPA!r_Z$qHxfR;e%!CgUu>ZN&L9TE#Kx2% zV%I6DGkZ=^ccs*-m}#An>HA(pIW|luz|189=A2qMw0(;h!gkC+g#1yQWDm8$4f$K= zl8&samUI@at*@0@SFN-*VtGEXnTI*0vNmG-*k=L7w(q#U?NS;F_Yv=R2gAu@ZL~~% zZxu?dD_M4gurL(N#)i!-%$N|^Cyu}ojRI;%LBNSW>>LgG1L>D6Q`bnHp1sHosiY*> zvUn)#;tYF?8`v7qrI>EG$e_qdy5S<@8CtgC0s|3+ z7qnJ+kx_)n_5Wi<|E>O5|3v?n{^zfS5Gnxy5C8!X009sH0T2KI5C8!X0D*6r!1pyz zoj(JkSkX*%&RmI=?`f7gZ&HJn?l5NpLgA|BmFLeBNPqvIw(+T=zr6YW#;2vfp1*&~ zP6VV20w4eaAOHd&@J$goChH~KHm@9)Z!#-1X=n!A4}FV8lp5IcFripZ-84`l#tGtK zh?2o@821kmL!q4hD*s$rIGNRIV(KB{)=gTcj-1JPAcA-(+SKqoLeoRwdOu)5NoQozrk>)=X&Ku+E1|%(QtE zd8FLImkw9^!$~s{o@sD{8r+E@E*tqA^Yib>nPx&{#CF{xbvey><_#&EN&fgtM_l+X z{&fay^2qWG&kuuu*hc7G67jer--yZJv@gk^4I~>(zE7E?z}&hur%#b73mLmw z7)misEjU9`;F=&vq68SmtyCg$n&iWN^X6Y@0+h!DGfFd>oI;;#b*r`3Em1;SAy8SF zgR(3KZgKDkZ zO_ht2h+Zuh8mW>O$YtFQ!Y3yy|(wo