2019-11-13 15:19:04 +01:00
import { ChangeDetectorRef , Component , OnDestroy , OnInit } from '@angular/core' ;
2019-12-21 14:32:43 +01:00
import { ActivatedRoute , NavigationEnd , Params , Router } from '@angular/router' ;
2019-10-24 09:44:29 +02:00
import { EnvProperties } from './openaireLibrary/utils/properties/env-properties' ;
import { EnvironmentSpecificService } from './openaireLibrary/utils/properties/environment-specific.service' ;
2019-12-12 15:20:03 +01:00
import { Session , User } from './openaireLibrary/login/utils/helper.class' ;
2019-10-24 09:44:29 +02:00
import { UserManagementService } from "./openaireLibrary/services/user-management.service" ;
2020-06-03 15:44:03 +02:00
import { StakeholderService } from "./openaireLibrary/monitor/services/stakeholder.service" ;
2019-12-21 14:32:43 +01:00
import { BehaviorSubject , Subscriber } from "rxjs" ;
2019-12-23 14:54:37 +01:00
import { LayoutService } from "./openaireLibrary/dashboard/sharedComponents/sidebar/layout.service" ;
2020-10-12 14:54:57 +02:00
import { MenuItem , RootMenuItem } from "./openaireLibrary/sharedComponents/menu" ;
2020-10-23 15:58:16 +02:00
import { Stakeholder , Topic , Visibility } from "./openaireLibrary/monitor/entities/stakeholder" ;
2020-07-10 10:38:52 +02:00
import { LinksResolver } from "./search/links-resolver" ;
2020-10-19 11:07:43 +02:00
import { Header } from "./openaireLibrary/sharedComponents/navigationBar.component" ;
2020-10-23 15:58:16 +02:00
import { arrow_left } from "./openaireLibrary/utils/icons/icons" ;
2020-10-30 12:33:51 +01:00
import { properties } from "../environments/environment" ;
2019-11-24 17:30:04 +01:00
2019-10-24 09:44:29 +02:00
@Component ( {
2019-12-20 12:48:35 +01:00
selector : 'app-root' ,
templateUrl : './app.component.html'
2019-10-24 09:44:29 +02:00
} )
2019-11-24 17:30:04 +01:00
export class AppComponent implements OnInit , OnDestroy {
2020-11-01 16:41:56 +01:00
properties : EnvProperties = properties ;
2019-12-20 12:48:35 +01:00
user : User ;
2019-12-21 14:32:43 +01:00
params : BehaviorSubject < Params > = new BehaviorSubject < Params > ( null ) ;
2019-12-20 12:48:35 +01:00
hasSidebar : boolean = false ;
hasHeader : boolean = false ;
2019-12-23 14:54:37 +01:00
hasAdminMenu : boolean = false ;
2020-07-06 11:19:01 +02:00
hasMiniMenu : boolean = false ;
isFrontPage : boolean = false ;
isViewPublic : boolean = false ;
sideBarItems : MenuItem [ ] = [ ] ;
2020-10-30 12:33:51 +01:00
specialSideBarMenuItem : MenuItem = null ;
2020-10-12 14:54:57 +02:00
menuItems : RootMenuItem [ ] = [ ] ;
2020-10-30 12:33:51 +01:00
menuHeader : Header = {
route : "/" ,
url : null ,
title : "Default menu header" ,
logoUrl : null ,
logoSmallUrl : null ,
position : 'center' ,
2020-10-30 14:59:37 +01:00
badge : false ,
2020-11-01 16:41:56 +01:00
stickyAnimation : false
2020-10-30 12:33:51 +01:00
} ;
2020-11-01 16:41:56 +01:00
2020-11-03 11:59:48 +01:00
userMenuItems : MenuItem [ ] = [ ] ;
2019-12-23 14:54:37 +01:00
adminMenuItems : MenuItem [ ] = [ ] ;
2020-07-06 11:19:01 +02:00
stakeholder : Stakeholder = null ;
activeTopic : Topic = null ;
2020-10-30 12:33:51 +01:00
loading : boolean = true ;
paramsResolved : boolean = false ;
2020-08-04 09:57:51 +02:00
innerWidth ;
2019-12-21 14:32:43 +01:00
private subscriptions : any [ ] = [ ] ;
constructor ( private route : ActivatedRoute ,
2019-12-20 12:48:35 +01:00
private propertiesService : EnvironmentSpecificService ,
private router : Router ,
private userManagementService : UserManagementService ,
private layoutService : LayoutService ,
private stakeholderService : StakeholderService ,
private cdr : ChangeDetectorRef ) {
2019-12-21 14:32:43 +01:00
this . subscriptions . push ( this . router . events . subscribe ( event = > {
if ( event instanceof NavigationEnd ) {
let r = this . route ;
while ( r . firstChild ) {
r = r . firstChild ;
}
let params = r . snapshot . params ;
2020-10-30 12:33:51 +01:00
this . paramsResolved = true ;
2019-12-21 14:32:43 +01:00
this . params . next ( params ) ;
}
} ) ) ;
2019-12-20 12:48:35 +01:00
}
2019-12-21 14:32:43 +01:00
2019-12-20 12:48:35 +01:00
ngOnInit() {
2020-10-30 12:33:51 +01:00
if ( window ) {
2020-08-04 09:57:51 +02:00
this . innerWidth = window . innerWidth ;
}
2019-12-20 12:48:35 +01:00
this . subscriptions . push ( this . layoutService . hasSidebar . subscribe ( hasSidebar = > {
this . hasSidebar = hasSidebar ;
2020-07-06 11:19:01 +02:00
if ( this . hasSidebar === false ) {
2019-12-24 15:48:27 +01:00
this . layoutService . setOpen ( false ) ;
}
2019-12-20 12:48:35 +01:00
this . cdr . detectChanges ( ) ;
} ) ) ;
this . subscriptions . push ( this . layoutService . hasHeader . subscribe ( hasHeader = > {
this . hasHeader = hasHeader ;
this . cdr . detectChanges ( ) ;
} ) ) ;
2019-12-23 14:54:37 +01:00
this . subscriptions . push ( this . layoutService . hasAdminMenu . subscribe ( hasAdminMenu = > {
this . hasAdminMenu = hasAdminMenu ;
this . cdr . detectChanges ( ) ;
} ) ) ;
2020-06-04 13:30:26 +02:00
this . subscriptions . push ( this . layoutService . hasMiniMenu . subscribe ( hasMiniMenu = > {
this . hasMiniMenu = hasMiniMenu ;
this . cdr . detectChanges ( ) ;
} ) ) ;
this . subscriptions . push ( this . layoutService . isFrontPage . subscribe ( isFrontPage = > {
this . isFrontPage = isFrontPage ;
this . cdr . detectChanges ( ) ;
} ) ) ;
2020-07-06 11:19:01 +02:00
this . route . queryParams . subscribe ( params = > {
this . isViewPublic = ( params [ 'view' ] == 'public' ) ;
} ) ;
2020-01-08 12:00:50 +01:00
this . layoutService . setOpen ( false ) ;
2020-10-30 12:33:51 +01:00
this . subscriptions . push ( this . params . subscribe ( params = > {
2020-11-01 16:41:56 +01:00
if ( this . paramsResolved ) {
2020-10-30 12:33:51 +01:00
this . loading = true ;
this . layoutService . setSmallScreen ( ( this . innerWidth && this . innerWidth < 1219 ) ) ;
this . layoutService . setOpen ( ! ( this . innerWidth && this . innerWidth < 1219 ) ) ;
let isSearch = this . router . url . includes ( 'search' ) ;
if ( params && params [ 'stakeholder' ] ) {
this . stakeholder = this . stakeholderService . stakeholder ;
if ( ! this . stakeholder || this . stakeholderService . stakeholder . alias !== params [ 'stakeholder' ] ) {
2020-11-13 17:42:12 +01:00
this . subscriptions . push ( this . stakeholderService . getStakeholder ( params [ 'stakeholder' ] ) . subscribe ( stakeholder = > {
2020-10-30 12:33:51 +01:00
if ( stakeholder ) {
this . stakeholder = stakeholder ;
LinksResolver . setProperties ( this . stakeholder . alias ) ;
this . buildMenu ( ) ;
if ( isSearch ) {
this . activeTopic = null ;
} else if ( params && params [ 'topic' ] && ! this . activeTopic ) {
this . activeTopic = this . stakeholder . topics . find ( topic = > topic . alias === decodeURIComponent ( params [ 'topic' ] ) && this . isPublicOrIsMember ( topic . visibility ) ) ;
2020-07-06 11:19:01 +02:00
} else {
2020-10-30 12:33:51 +01:00
this . activeTopic = this . stakeholder . topics . find ( topic = > this . isPublicOrIsMember ( topic . visibility ) ) ;
2020-07-06 11:19:01 +02:00
}
2020-11-05 17:50:06 +01:00
this . setSideBar ( ) ;
2020-10-30 12:33:51 +01:00
this . loading = false ;
} else {
2020-07-10 10:38:52 +02:00
LinksResolver . resetProperties ( ) ;
2020-06-10 16:03:28 +02:00
this . navigateToError ( ) ;
2020-10-12 14:54:57 +02:00
this . buildMenu ( ) ;
2020-10-30 12:33:51 +01:00
this . loading = false ;
2020-07-06 11:19:01 +02:00
}
2020-10-30 12:33:51 +01:00
} , error = > {
LinksResolver . resetProperties ( ) ;
this . navigateToError ( ) ;
this . buildMenu ( ) ;
this . loading = false ;
2020-11-13 17:42:12 +01:00
} ) ) ;
2019-12-21 14:32:43 +01:00
} else {
2020-10-23 15:58:16 +02:00
this . buildMenu ( ) ;
2020-10-30 12:33:51 +01:00
if ( isSearch ) {
this . activeTopic = null ;
} else if ( params && params [ 'topic' ] ) {
this . activeTopic = this . stakeholder . topics . find ( topic = > topic . alias === decodeURIComponent ( params [ 'topic' ] ) && this . isPublicOrIsMember ( topic . visibility ) ) ;
} else {
this . activeTopic = this . stakeholder . topics . find ( topic = > this . isPublicOrIsMember ( topic . visibility ) ) ;
}
2020-11-05 17:50:06 +01:00
this . setSideBar ( ) ;
2020-10-30 12:33:51 +01:00
this . loading = false ;
2020-10-23 15:58:16 +02:00
}
2020-10-30 12:33:51 +01:00
} else {
LinksResolver . resetProperties ( ) ;
this . stakeholderService . setStakeholder ( null ) ;
this . layoutService . setOpen ( ! ( this . innerWidth && this . innerWidth < 1219 ) ) ;
this . stakeholder = null ;
this . buildMenu ( ) ;
this . loading = false ;
}
}
} ) ) ;
this . subscriptions . push ( this . userManagementService . getUserInfo ( ) . subscribe ( user = > {
this . user = user ;
if ( user ) {
this . buildMenu ( ) ;
}
} , error = > {
console . log ( "App couldn't fetch properties" ) ;
console . log ( error ) ;
} ) ) ;
2019-12-20 12:48:35 +01:00
}
2020-07-06 11:19:01 +02:00
2019-12-20 12:48:35 +01:00
public ngOnDestroy() {
this . subscriptions . forEach ( value = > {
if ( value instanceof Subscriber ) {
value . unsubscribe ( ) ;
}
} ) ;
2020-11-13 17:42:12 +01:00
this . userManagementService . clearSubscriptions ( ) ;
this . layoutService . clearSubscriptions ( ) ;
this . stakeholderService . clearSubscriptions ( ) ;
2019-12-20 12:48:35 +01:00
}
2020-06-10 16:03:28 +02:00
private navigateToError() {
2020-09-18 11:19:47 +02:00
this . router . navigate ( [ '/error' ] , { queryParams : { 'page' : this . properties . baseLink + this . router . url } } ) ;
2020-06-10 16:03:28 +02:00
}
2020-07-06 11:19:01 +02:00
2019-12-20 12:48:35 +01:00
public get open() {
return this . layoutService . open ;
}
2020-07-06 11:19:01 +02:00
2019-12-24 15:48:27 +01:00
public toggleOpen ( event : MouseEvent ) {
event . preventDefault ( ) ;
this . layoutService . setOpen ( ! this . open ) ;
2019-12-20 12:48:35 +01:00
}
2020-07-06 11:19:01 +02:00
private setSideBar() {
let items : MenuItem [ ] = [ ] ;
2020-11-04 12:16:47 +01:00
if ( this . isPublicOrIsMember ( this . stakeholder . visibility ) ) {
this . stakeholder . topics . forEach ( ( topic ) = > {
if ( this . isPublicOrIsMember ( topic . visibility ) ) {
let topicItem : MenuItem = new MenuItem ( topic . alias , topic . name , "" , (
'/' + this . stakeholder . alias + '/' + topic . alias ) ,
null , [ ] , [ ] , { } ) ;
topicItem . icon = topic . icon ;
items . push ( topicItem ) ;
}
} ) ;
if ( items . length === 0 ) {
items . push ( new MenuItem ( 'noTopics' , 'No topics available yet' , "" , "" , false , [ ] , [ ] , { } ) ) ;
2020-07-06 11:19:01 +02:00
}
2020-11-04 12:16:47 +01:00
} else {
let topicItem : MenuItem = new MenuItem ( "private" , "Private Data" , "" , "" , null , [ ] , [ ] , { } ) ;
items . push ( topicItem ) ;
2020-07-06 11:19:01 +02:00
}
this . sideBarItems = items ;
}
2019-12-20 12:48:35 +01:00
buildMenu() {
2020-10-12 14:54:57 +02:00
this . menuItems = [ ] ;
this . adminMenuItems = [ ] ;
2019-12-20 12:48:35 +01:00
this . userMenuItems = [ ] ;
2020-11-03 10:42:47 +01:00
if ( this . user ) {
2020-11-03 11:59:48 +01:00
if ( this . isCurator ( ) ) {
this . userMenuItems . push ( new MenuItem ( "" , "Manage profiles" ,
"" , "/admin" , false , [ ] , [ ] , { } ) ) ;
}
2020-11-05 09:26:05 +01:00
this . userMenuItems . push ( new MenuItem ( "" , "User information" , "" , "/user-info" , false , [ ] , [ ] , { } ) ) ;
2020-11-03 10:42:47 +01:00
}
2020-10-30 12:33:51 +01:00
if ( this . stakeholder ) {
2020-10-12 14:54:57 +02:00
if ( this . isFrontPage ) {
this . menuHeader = {
route : "/" + this . stakeholder . alias ,
url : null ,
title : this.stakeholder.name ,
logoUrl : null ,
logoSmallUrl : null ,
position : 'center' ,
2020-10-30 14:59:37 +01:00
badge : false ,
stickyAnimation : false
2020-10-12 14:54:57 +02:00
} ;
2020-11-03 11:59:48 +01:00
if ( this . isCurator ( ) ) {
2020-10-12 14:54:57 +02:00
this . menuItems . push ( {
rootItem : new MenuItem ( "manage" , "Manage" ,
2020-10-30 12:33:51 +01:00
"" , "/admin" , false , [ ] , null , { } ) , items : [ ]
2020-10-12 14:54:57 +02:00
} ) ;
2020-10-30 12:33:51 +01:00
2020-10-29 10:17:39 +01:00
}
2020-11-04 12:16:47 +01:00
if ( this . isPublicOrIsMember ( this . stakeholder . visibility ) ) {
2020-11-19 17:16:36 +01:00
this . specialSideBarMenuItem = new MenuItem ( "search" , "Search research outcomes" , "" , this . properties . searchLinkToResults , false , [ ] , null , this . createSearchParameters ( ) ) ;
2020-11-04 12:16:47 +01:00
this . specialSideBarMenuItem . icon = '<span uk-icon="search"></span>' ;
} else {
this . specialSideBarMenuItem = null ;
}
2020-10-30 12:33:51 +01:00
2020-10-12 14:54:57 +02:00
} else {
this . menuHeader = {
route : "/admin/" + this . stakeholder . alias ,
url : null ,
title : 'Admin - ' + this . stakeholder . name ,
logoUrl : null ,
logoSmallUrl : null ,
position : 'center' ,
2020-10-30 14:59:37 +01:00
badge : false ,
stickyAnimation : false
2020-10-12 14:54:57 +02:00
} ;
this . menuItems . push ( {
rootItem : new MenuItem ( "" , "Dashboard" ,
2020-10-30 12:33:51 +01:00
"" , '/' + this . stakeholder . alias + '/' , false , [ ] , null , { } ) , items : [ ]
2020-10-12 14:54:57 +02:00
} ) ;
2020-10-30 12:33:51 +01:00
this . adminMenuItems . push ( new MenuItem ( "general" , "General" , "" , "/admin/" + this . stakeholder . alias , false , [ ] , [ ] , { } , "<i uk-icon=\"image\"></i>" ) ) ;
this . adminMenuItems . push ( new MenuItem ( "indicators" , "Indicators" , "" , "/admin/" + this . stakeholder . alias + '/indicators' , false , [ ] , [ ] , { } , "<i uk-icon=\"image\"></i>" ) ) ;
this . adminMenuItems . push ( new MenuItem ( "users" , "Users" , "" , "/admin/" + this . stakeholder . alias + "/users" , false , [ ] , [ ] , { } , "<i uk-icon=\"users\"></i>" ) ) ;
this . specialSideBarMenuItem = new MenuItem ( "back" , "Manage profiles" , "" , "/admin" , false , [ ] , null , { } ) ;
2020-10-31 17:02:30 +01:00
this . specialSideBarMenuItem . icon = '<span class="uk-icon-button small uk-icon uk-button-secondary">' + arrow_left . data + '</span>' ; // '<span class="uk-icon-button uk-icon portal-button " uk-icon="chevron-left"></span>';
this . specialSideBarMenuItem . customClass = 'uk-text-uppercase uk-text-bold uk-text-secondary' ;
2020-10-12 14:54:57 +02:00
}
2020-10-30 12:33:51 +01:00
} else {
if ( this . isFrontPage || ! this . hasAdminMenu ) {
this . menuHeader = {
route : null ,
url : "https://" + ( this . properties . environment == 'beta' ? 'beta.' : '' ) + 'monitor.openaire.eu' ,
title : "Monitor" ,
logoUrl : 'assets/common-assets/logo-large-monitor.png' ,
logoSmallUrl : "assets/common-assets/logo-small-monitor.png" ,
position : 'left' ,
2020-10-30 14:59:37 +01:00
badge : true ,
stickyAnimation : false
2020-10-30 12:33:51 +01:00
} ;
2020-11-03 11:59:48 +01:00
this . menuItems . push ( {
rootItem : new MenuItem ( "about" , "About" ,
"https://" + ( this . properties . environment == 'beta' ? 'beta.' : '' ) + 'monitor.openaire.eu/about/learn-how' , "" , false , [ ] , null , { } ) , items : [ ]
} ) ;
this . menuItems . push ( {
rootItem : new MenuItem ( "browse" , "Browse" ,
"https://" + ( this . properties . environment == 'beta' ? 'beta.' : '' ) + 'monitor.openaire.eu/browse' , "" , false , [ ] , null , { } ) , items : [ ]
} ) ;
this . menuItems . push ( {
rootItem : new MenuItem ( "contact" , "Contact us" ,
"https://" + ( this . properties . environment == 'beta' ? 'beta.' : '' ) + 'monitor.openaire.eu/contact-us' , "" , false , [ ] , null , { } ) , items : [ ]
} ) ;
2020-10-30 12:33:51 +01:00
} else {
this . menuHeader = {
route : "/" ,
url : null ,
title : "Monitor Dashboard" ,
logoUrl : null ,
logoSmallUrl : null ,
position : 'center' ,
2020-10-30 14:59:37 +01:00
badge : false ,
stickyAnimation : false
2020-10-30 12:33:51 +01:00
} ;
2020-10-12 14:54:57 +02:00
this . adminMenuItems = [ ] ;
this . specialSideBarMenuItem = null ;
this . adminMenuItems . push ( new MenuItem ( "stakeholders" , "Manage profiles" , "" , "/admin" , false , [ ] , [ ] , { } , "<i uk-icon=\"cog\"></i>" ) ) ;
2020-10-30 14:59:37 +01:00
/ * l e t a d m i n O p t i o n s = n e w M e n u I t e m ( " a d m i n O p t i o n s " , " A d m i n O p t i o n s " , " " , n u l l , f a l s e , [ ] , [ ] , { } )
2020-10-12 14:54:57 +02:00
adminOptions . items . push ( new MenuItem ( "pages" , "Pages" , "" , "/pages" , false , [ ] , [ ] , { } ) ) ;
adminOptions . items . push ( new MenuItem ( "portals" , "Portals" , "" , "/portals" , false , [ ] , [ ] , { } ) ) ;
adminOptions . items . push ( new MenuItem ( "entities" , "Entities" , "" , "/entities" , false , [ ] , [ ] , { } ) ) ;
adminOptions . items . push ( new MenuItem ( "classes" , "Class help texts" , "" , "/classes" , false , [ ] , [ ] , { } ) ) ;
this . adminMenuItems . push ( adminOptions ) ;
2020-10-30 14:59:37 +01:00
let monitorOptions = new MenuItem ( "monitorOptions" , "Monitor Options" , "" , null , false , [ ] , [ ] , { } )
monitorOptions . items . push ( new MenuItem ( "pages" , "Pages" , "" , "/pages" , false , [ ] , [ ] , { communityId : 'monitor' } ) ) ;
monitorOptions . items . push ( new MenuItem ( "entities" , "Entities" , "" , "/entities" , false , [ ] , [ ] , { communityId : 'monitor' } ) ) ;
monitorOptions . items . push ( new MenuItem ( "classes" , "Class help texts" , "" , "/classContents" , false , [ ] , [ ] , { communityId : 'monitor' } ) ) ;
monitorOptions . items . push ( new MenuItem ( "helptexts" , "Help texts" , "" , "/helptexts" , false , [ ] , [ ] , { communityId : 'monitor' } ) ) ;
2020-10-12 14:54:57 +02:00
this . adminMenuItems . push ( monitorOptions ) ; * /
2020-10-30 12:33:51 +01:00
2020-10-12 14:54:57 +02:00
}
2019-12-20 12:48:35 +01:00
}
2020-10-30 12:33:51 +01:00
2019-12-20 12:48:35 +01:00
}
2020-11-03 11:59:48 +01:00
public isCurator() {
return this . user && ( Session . isPortalAdministrator ( this . user ) || Session . isMonitorCurator ( this . user ) || Session . isKindOfMonitorManager ( this . user ) ) ;
}
public isManager ( stakeholder :Stakeholder ) {
return this . user && ( Session . isPortalAdministrator ( this . user ) || Session . isCurator ( stakeholder . type , this . user ) || Session . isManager ( stakeholder . type , stakeholder . alias , this . user ) ) ;
2020-07-06 11:19:01 +02:00
}
2020-10-30 12:33:51 +01:00
2020-11-01 16:41:56 +01:00
private resolvePageInner() {
if ( document !== undefined ) {
let header = document . getElementById ( 'pager_header_content' ) ;
let inner = document . getElementById ( 'page_content_inner' ) ;
if ( header ) {
inner . setAttribute ( 'style' , '{margin-top:' + header . offsetHeight + '}' ) ;
} else {
inner . setAttribute ( 'style' , '{margin-top:' + 0 + '}' ) ;
}
}
}
2020-10-23 15:58:16 +02:00
public isPublicOrIsMember ( visibility : Visibility ) : boolean {
2020-11-04 12:16:47 +01:00
if ( visibility == "PRIVATE" || ( this . isViewPublic && visibility != "PUBLIC" ) ) {
2020-07-06 11:19:01 +02:00
return false ;
}
2020-11-04 12:16:47 +01:00
return true ;
2020-07-06 11:19:01 +02:00
}
2020-10-30 12:33:51 +01:00
createSearchParameters() {
if ( ! this . stakeholder ) {
2020-07-13 11:23:04 +02:00
return { } ;
}
2020-10-30 12:33:51 +01:00
if ( this . stakeholder . type == "funder" ) {
return { "relfunder" : encodeURIComponent ( "\"" + this . stakeholder . index_id + "||" + this . stakeholder . index_name + "||" + this . stakeholder . index_shortName + "\"" ) } ;
} else if ( this . stakeholder . type == "ri" ) {
2020-07-13 11:23:04 +02:00
// https://beta.explore.openaire.eu/search/find/research-outcomes?f0=q&fv0=&resultbestaccessright=%22Open%20Access%22&community=%22mes%7C%7CEuropean%20Marine%20Science%22&qf=true
2020-10-30 12:33:51 +01:00
return { "community" : encodeURIComponent ( "\"" + this . stakeholder . index_id + "||" + this . stakeholder . index_name + "\"" ) } ;
} else if ( this . stakeholder . type == "organization" ) {
return { "cf" : true } ;
2020-07-13 11:23:04 +02:00
}
}
2019-10-24 09:44:29 +02:00
}