From 8940b766b6a48bde22822cf4b4a8ed27f9f875b2 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Sun, 23 Oct 2016 21:27:11 +0200 Subject: [PATCH] improvements --- README.md | 4 +- .../views/Navbar/user.html.twig | 38 ++++++ .../translations/AvanzuAdminTheme.de.xliff | 19 +++ app/Resources/translations/messages.de.xliff | 118 +++++++++++------- app/Resources/views/base.html.twig | 67 ++++++++-- app/Resources/views/dashboard/index.html.twig | 46 +++++++ app/Resources/views/macros/widgets.html.twig | 94 ++++++++++++++ app/Resources/views/security/login.html.twig | 8 +- app/Resources/views/user/profile.html.twig | 10 ++ app/config/config.yml | 16 ++- app/config/config_dev.yml | 9 +- app/config/routing.yml | 6 +- app/config/routing_dev.yml | 83 +++++++++++- app/config/security.yml | 2 +- app/config/services.yml | 28 +++++ composer.json | 2 +- .../Controller/ProfileController.php | 71 +++++++++++ .../Controller/SecurityController.php | 1 + .../DataFixtures/ORM/LoadFixtures.php | 8 ++ src/AppBundle/Entity/User.php | 112 ++++++++++------- src/AppBundle/EventListener/MenuBuilder.php | 2 +- .../EventListener/NavbarShowUserListener.php | 59 +++++++++ .../Controller/Admin/TimesheetController.php | 2 +- .../Controller/TimesheetController.php | 5 +- .../Repository/TimesheetRepository.php | 46 +++++-- .../Resources/views/timesheet/index.html.twig | 10 +- web/avatars/default.png | Bin 0 -> 12519 bytes 27 files changed, 728 insertions(+), 138 deletions(-) create mode 100644 app/Resources/AvanzuAdminThemeBundle/views/Navbar/user.html.twig create mode 100644 app/Resources/translations/AvanzuAdminTheme.de.xliff create mode 100644 app/Resources/views/macros/widgets.html.twig create mode 100644 app/Resources/views/user/profile.html.twig create mode 100644 src/AppBundle/Controller/ProfileController.php create mode 100644 src/AppBundle/EventListener/NavbarShowUserListener.php create mode 100644 web/avatars/default.png diff --git a/README.md b/README.md index 9f776993..a9bd50d8 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ $ composer install Lets prepare the environment ```bash -$ php bin/console assets:install --symlink -$ php bin/console avanzu:admin:fetch-vendor +$ php bin/console assets:install --symlink --relative +$ php bin/console avanzu:admin:initialize --symlink --relative ``` diff --git a/app/Resources/AvanzuAdminThemeBundle/views/Navbar/user.html.twig b/app/Resources/AvanzuAdminThemeBundle/views/Navbar/user.html.twig new file mode 100644 index 00000000..e89d5475 --- /dev/null +++ b/app/Resources/AvanzuAdminThemeBundle/views/Navbar/user.html.twig @@ -0,0 +1,38 @@ + +{% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %} + diff --git a/app/Resources/translations/AvanzuAdminTheme.de.xliff b/app/Resources/translations/AvanzuAdminTheme.de.xliff new file mode 100644 index 00000000..b076b38b --- /dev/null +++ b/app/Resources/translations/AvanzuAdminTheme.de.xliff @@ -0,0 +1,19 @@ + + + + + + Member since %date% + Mitglied seit %date% + + + Profile + Profil + + + Sign out + Abmelden + + + + diff --git a/app/Resources/translations/messages.de.xliff b/app/Resources/translations/messages.de.xliff index 3b33e011..1a103c44 100644 --- a/app/Resources/translations/messages.de.xliff +++ b/app/Resources/translations/messages.de.xliff @@ -2,6 +2,9 @@ + browser.title Kimai - Time Tracking @@ -11,6 +14,57 @@ Kimai + + + security.title.login + Anmelden + + + security.label.username + Benutzername + + + security.label.password + Passwort + + + security.action.sign_in + Los geht's + + + + + menu.homepage + Dashboard + + + menu.admin + Administration + + + menu.logout + Abmelden + + + menu.timesheet + Zeiterfassung + + + + + footer.license + Alle Rechte vorbehalten. + + + http_error.name Error @@ -48,58 +102,34 @@ Error - - action.browse_admin - Zum Backend + + + label.date + Datum - - help.browse_admin - Zum Backend + + label.starttime + Begin - - action.browse_app - Zum Frontend + + label.endtime + Ende - - help.browse_app - Zum Frontend - - - title.login - Anmelden + + label.duration + Dauer label.username - Benutzername + Benutzer - - label.password - Passwort - - - action.sign_in - Los geht's - - - menu.logout - Abmelden - - - menu.admin - Backend - - - menu.timesheet - Zeiterfassung - - - kimai.license - Copyright Kevin Papst. Alle Rechte vorbehalten. - - - kimai.footer - Kimai Time-Tracking + + label.description + Beschreibung + diff --git a/app/Resources/views/base.html.twig b/app/Resources/views/base.html.twig index 05dda3e6..7c5e35eb 100644 --- a/app/Resources/views/base.html.twig +++ b/app/Resources/views/base.html.twig @@ -1,18 +1,68 @@ -{% extends 'AvanzuAdminThemeBundle:layout:base-layout.html.twig' %} +{% extends 'AvanzuAdminThemeBundle:layout:default-layout.html.twig' %} -{% block page_content %} - {% block main %} - {% endblock %} +{% block avanzu_page_content %} + {% block main %}{% endblock %} +{% endblock %} + +{% block avanzu_document_title %} + {% block title %}{{ 'browser.title'|trans }}{% endblock %} +{% endblock %} + +{% block avanzu_sidebar_search %} + +{% endblock %} + +{% block avanzu_page_title %} + {% block page_title %}{% endblock %} +{% endblock %} + +{% block avanzu_page_subtitle %} + {% block page_subtitle %}{% endblock %} +{% endblock %} + +{% block avanzu_logo_path %} + {{ path('dashboard') }} +{% endblock %} + +{% block avanzu_logo_mini %} + KTT +{% endblock %} + +{% block avanzu_logo_lg %} + Kimai - Time Tracking {% endblock %} {% block flash_messages %} {{ include('default/_flash_messages.html.twig') }} {% endblock %} +{% block avanzu_footer %} + {% block footer %} + + {% endblock %} +{% endblock %} {# - {% block title %}{{ 'browser.title'|trans }}{% endblock %} - + +avanzu_head +avanzu_navbar_toggle +avanzu_navbar_messages +avanzu_navbar_notifications +avanzu_navbar_tasks +avanzu_navbar_user +avanzu_navbar_control_sidebar_toggle +avanzu_sidebar_user +avanzu_sidebar_nav +avanzu_breadcrumb +avanzu_control_sidebar +avanzu_javascripts +avanzu_javascripts_inline - - -

© {{ 'now'|date('Y') }} - {{ 'kimai.footer'|trans }}

-

{{ 'kimai.license'|trans }}

- #} \ No newline at end of file diff --git a/app/Resources/views/dashboard/index.html.twig b/app/Resources/views/dashboard/index.html.twig index 4635af83..61d09c59 100644 --- a/app/Resources/views/dashboard/index.html.twig +++ b/app/Resources/views/dashboard/index.html.twig @@ -1,7 +1,53 @@ {% extends 'base.html.twig' %} +{% import "macros/widgets.html.twig" as widgets %} {% block page_title %}Dashboard{% endblock %} {% block page_subtitle %}Welcome!{% endblock %} {% block main %} + +
+
+ {{ widgets.info_box_counter('Bla blub', 300, 'bar-chart', 'blue') }} +
+
+ {{ widgets.info_box_counter('Steigerungsrate', 80, 'line-chart', 'red') }} +
+
+ {{ widgets.info_box_counter('Anzahl Tage', 27, 'calendar', 'yellow') }} +
+
+ {{ widgets.info_box_counter('Stunden total / Monat', 460, 'clock-o', 'green') }} +
+
+ +
+
+ {{ widgets.info_box_progress('Bewilligte Stunden', 'Stunden zur Abrechnung bewilligt', 120, 10, 'star-o') }} +
+
+ {{ widgets.info_box_progress('Umsatz / Monat', '70% Increase in 30 Days', 6830, 30, 'credit-card', 'black') }} +
+
+ {{ widgets.info_box_progress('Stunden persönlich', 'Das ist noch nicht genug', 135, 60, 'hourglass-o') }} +
+
+ {{ widgets.info_box_progress('Anzahl Benutzer', 'Mehr ist besser!', 5, 90, 'user') }} +
+
+ +
+
+ {{ widgets.info_box_more('Bewilligte Stunden', 120, 'h', 'star-o', 'purple') }} +
+
+ {{ widgets.info_box_more('Umsatz / Monat', 6830, '$', '', 'yellow') }} +
+
+ {{ widgets.info_box_more('Stunden persönlich', 135, '€', 'hourglass-o', 'red') }} +
+
+ {{ widgets.info_box_more('Anzahl Benutzer', 5, '', 'user') }} +
+
{% endblock %} diff --git a/app/Resources/views/macros/widgets.html.twig b/app/Resources/views/macros/widgets.html.twig new file mode 100644 index 00000000..4482f743 --- /dev/null +++ b/app/Resources/views/macros/widgets.html.twig @@ -0,0 +1,94 @@ + +{% macro profile_list(user, items, color) %} + {% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %} +
+ +
+
+ {{ macro.avatar(user.avatar, user.username) }} +
+ +

{{ user.alias }} ({{ user.username }})

+
{{ user.title }}
+
+ +
+{% endmacro %} + +{% macro profile_box(user, items) %} + {% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %} + +
+ +
+

{{ user.alias }} ({{ user.username }})

+
{{ user.title }}
+
+
+ {{ macro.avatar(user.avatar, user.username) }} +
+ +
+{% endmacro %} + +{% macro info_box_counter(title, amount, icon, color) %} +
+ + +
+ {{ title|trans }} + {{ amount }} +
+
+{% endmacro %} + +{% macro info_box_progress(title, description, amount, percentage, icon, color) %} +
+ + +
+ {{ title|trans }} + {{ amount }} + +
+
+
+ + {{ description }} + +
+ +
+{% endmacro %} + +{% macro info_box_more(title, amount, unit, icon, color) %} +
+
+

{{ amount }}{{ unit|default('%') }}

+

{{ title|trans }}

+
+
+ +
+ + {{ 'more.info.link'|trans }} + +
+{% endmacro %} diff --git a/app/Resources/views/security/login.html.twig b/app/Resources/views/security/login.html.twig index fdad8b35..3b8d47cf 100644 --- a/app/Resources/views/security/login.html.twig +++ b/app/Resources/views/security/login.html.twig @@ -15,18 +15,18 @@
- {{ 'title.login'|trans }} + {{ 'security.title.login'|trans }}
- +
- +
diff --git a/app/Resources/views/user/profile.html.twig b/app/Resources/views/user/profile.html.twig new file mode 100644 index 00000000..1bac09df --- /dev/null +++ b/app/Resources/views/user/profile.html.twig @@ -0,0 +1,10 @@ +{% extends 'base.html.twig' %} +{% import "macros/widgets.html.twig" as widgets %} + +{% block page_title %}Profile{% endblock %} +{% block page_subtitle %}manage your settings{% endblock %} + +{% block main %} + {{ widgets.profile_list(user, items, 'aqua-active') }} + {{ widgets.profile_box(user, items, 'aqua-active') }} +{% endblock %} \ No newline at end of file diff --git a/app/config/config.yml b/app/config/config.yml index b5442ff5..1a778cc6 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -58,7 +58,9 @@ twig: - "bootstrap_3_layout.html.twig" - "form/fields.html.twig" globals: - admin_skin: skin-blue + kimai_context: + date_1: "d.m.Y" # used for display in timesheets + box_color: "green" # Assetic Configuration (used for managing web assets: CSS, JavaScript, Sass, etc.) assetic: @@ -89,5 +91,13 @@ swiftmailer: password: "%mailer_password%" spool: { type: memory } -#avanzu_admin_theme: -# use_assetic: false +avanzu_admin_theme: + use_twig : true + theme: + default_avatar : avatars/default.png + skin : skin-blue + fixed_layout : false + boxed_layout : false + collapsed_sidebar: true + mini_sidebar : true + control_sidebar : false diff --git a/app/config/config_dev.yml b/app/config/config_dev.yml index cd62ce1f..19cd023c 100644 --- a/app/config/config_dev.yml +++ b/app/config/config_dev.yml @@ -7,9 +7,9 @@ framework: strict_requirements: true profiler: { only_exceptions: false } -web_profiler: - toolbar: '%kernel.debug%' - intercept_redirects: false +#web_profiler: +# toolbar: '%kernel.debug%' +# intercept_redirects: false monolog: handlers: @@ -40,3 +40,6 @@ services: class: Twig_Extensions_Extension_Text tags: - name: twig.extension + +avanzu_admin_theme: + enable_demo: false diff --git a/app/config/routing.yml b/app/config/routing.yml index 3c6fe41b..230e6565 100644 --- a/app/config/routing.yml +++ b/app/config/routing.yml @@ -34,6 +34,6 @@ homepage: permanent: true avanzu_admin_profile: - path: /profile/{userid}/ -avanzu_admin_logout: - path: /logout + path: /{_locale}/profile/{ident}/ + options: + avanzu_admin_route: profile diff --git a/app/config/routing_dev.yml b/app/config/routing_dev.yml index fbaae995..252828ee 100644 --- a/app/config/routing_dev.yml +++ b/app/config/routing_dev.yml @@ -1,17 +1,18 @@ # this imports the routes used to display the web debug toolbar at the bottom of each page -_wdt: - resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" - prefix: /_wdt +#_wdt: +# resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" +# prefix: /_wdt # this imports the routes needed to display the Symfony Profiler information -_profiler: - resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" - prefix: /_profiler +#_profiler: +# resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" +# prefix: /_profiler # this imports the route used to test error pages. Just browse the following URL: # /{_locale}/_error/{status_code}.{format} # (e.g. /en/_error/404, /en/_error/403.json, /fr/_error/500.xml) # See http://symfony.com/doc/current/cookbook/controller/error_pages.html#testing-error-pages-during-development + _errors: resource: "@TwigBundle/Resources/config/routing/errors.xml" prefix: /{_locale}/_error @@ -20,6 +21,76 @@ _errors: defaults: _locale: '%locale%' + +# ====================================================================== +# Template routes, that we need once we use those features +# ====================================================================== + +#avanzu_admin_login: +# path: /{_locale}/login +# options: +# avanzu_admin_route: login +# +#avanzu_admin_logout: +# path: /logout +# options: +# avanzu_admin_route: logout +# +#avanzu_admin_all_tasks: +# path: /tasks/ +# options: +# avanzu_admin_route: all_tasks +# +#avanzu_admin_show_task: +# path: /tasks/{ident}/ +# options: +# avanzu_admin_route: task +# +#avanzu_admin_all_notifications: +# path: /notifications/ +# options: +# avanzu_admin_route: all_notifications +# +#avanzu_admin_show_notification: +# path: /notifications/{ident}/ +# options: +# avanzu_admin_route: notification +# +#avanzu_admin_all_messages: +# path: /messages/ +# options: +# avanzu_admin_route: all_messages +# +#avanzu_admin_show_message: +# path: /messages/{ident}/ +# options: +# avanzu_admin_route: message + +# ====================================================================== +# Template demo screen +# Activate full demo mode by setting "enable_demo" in config_dev.xml +# ====================================================================== + +avanzu_admin_home: + path: /{_locale}/demo-admin/ + defaults: {_controller: AvanzuAdminThemeBundle:Default:index} + options: + avanzu_admin_route: welcome + +avanzu_admin_form_demo: + path: /{_locale}/demo-admin/form-demo/ + defaults: {_controller: AvanzuAdminThemeBundle:Default:form} + +avanzu_admin_login_demo: + path: /{_locale}/demo-admin/login/ + defaults: {_controller: AvanzuAdminThemeBundle:Default:login} + +avanzu_admin_dash_demo: + path: /{_locale}/demo-admin/dashboard/ + defaults: {_controller: AvanzuAdminThemeBundle:Default:dashboard} + +# ====================================================================== + # this loads the main routing file, which usually defines the routes available # in any environment (production, development, etc.) _main: diff --git a/app/config/security.yml b/app/config/security.yml index 10b2fe03..bdd29fc2 100644 --- a/app/config/security.yml +++ b/app/config/security.yml @@ -37,7 +37,7 @@ security: # The route name the user can go to in order to logout path: security_logout # The name of the route to redirect to after logging out - target: timesheet + target: homepage role_hierarchy: ROLE_USER: ROLE_CUSTOMER diff --git a/app/config/services.yml b/app/config/services.yml index f0295c81..c0b7dd1d 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -61,3 +61,31 @@ services: # # // same code using repository services # $posts = $this->get('app.post_repository')->findAll(); + + avanzu_admin_theme.navbar_user_listener: + class: AppBundle\EventListener\NavbarShowUserListener + arguments: ["@security.token_storage"] + tags: + - { name: kernel.event_listener, event: theme.navbar_user, method: onShowUser } + - { name: kernel.event_listener, event: theme.sidebar_user, method: onShowUser } + +# avanzu_admin_theme.navbar_task_listener: +# class: "%avanzu_admin_theme.navbar_task_listener.class%" +# tags: +# - { name: kernel.event_listener, event: theme.tasks, method: onListTasks } +# +# avanzu_admin_theme.navbar_notify_listener: +# class: "%avanzu_admin_theme.navbar_notify_listener.class%" +# tags: +# - { name: kernel.event_listener, event: theme.notifications, method: onListNotifications } +# +# avanzu_admin_theme.navbar_msg_listener: +# class: "%avanzu_admin_theme.navbar_msg_listener.class%" +# tags: +# - { name: kernel.event_listener, event: theme.messages, method: onListMessages } +# +# avanzu_admin_theme.setup_menu_listener: +# class: "%avanzu_admin_theme.setup_menu_listener.class%" +# tags: +# - { name: kernel.event_listener, event: theme.sidebar_setup_menu, method: onSetupMenu } +# - { name: kernel.event_listener, event: theme.breadcrumb, method: onSetupMenu } diff --git a/composer.json b/composer.json index e73d63fc..d2dd0062 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "symfony/symfony": "^3.1", "twig/extensions": "^1.3", "white-october/pagerfanta-bundle": "^1.0", - "avanzu/admin-theme-bundle": "^1.3" + "avanzu/admin-theme-bundle": "dev-feature/2.0" }, "require-dev": { "sensio/generator-bundle": "~3.0", diff --git a/src/AppBundle/Controller/ProfileController.php b/src/AppBundle/Controller/ProfileController.php new file mode 100644 index 00000000..5a076619 --- /dev/null +++ b/src/AppBundle/Controller/ProfileController.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AppBundle\Controller; + +use TimesheetBundle\Entity\Timesheet; +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; + +/** + * User profile controller + * + * @Route("/profile") + * @Security("has_role('ROLE_USER')") + * + * @author Kevin Papst + */ +class ProfileController extends Controller +{ + /** + * @Route("/", defaults={"ident": null}, name="user_profile") + * @Route("/{ident}/", requirements={"ident": "[a-zA-Z0-9\-].*"}, name="user_profile_ident") + * @Method("GET") + */ + public function indexAction($ident) + { + // FIXME fetch values dynamically and add trans filter to macros + $items = [ + [ + 'title' => 'Projects', + 'url' => '#', + 'color' => 'blue', + 'amount' => 12 + ], + [ + 'title' => 'Tasks', + 'url' => '#', + 'color' => 'aqua', + 'amount' => 5 + ], + [ + 'title' => 'Projects', + 'url' => '#', + 'color' => 'red', + 'amount' => 27 + ], + [ + 'title' => 'Einträge', + 'url' => '#', + 'color' => 'green', + 'amount' => 842 + ], + ]; + + return $this->render( + 'user/profile.html.twig', + ['user' => $this->getUser(), 'items' => $items] + ); + } +} diff --git a/src/AppBundle/Controller/SecurityController.php b/src/AppBundle/Controller/SecurityController.php index bf2b7dd3..74e513bd 100644 --- a/src/AppBundle/Controller/SecurityController.php +++ b/src/AppBundle/Controller/SecurityController.php @@ -31,6 +31,7 @@ class SecurityController extends Controller { $helper = $this->get('security.authentication_utils'); + //return $this->render('AvanzuAdminThemeBundle:Security:login.html.twig', [ return $this->render('security/login.html.twig', [ // last username entered by the user (if any) 'last_username' => $helper->getLastUsername(), diff --git a/src/AppBundle/DataFixtures/ORM/LoadFixtures.php b/src/AppBundle/DataFixtures/ORM/LoadFixtures.php index d86a1959..02053385 100644 --- a/src/AppBundle/DataFixtures/ORM/LoadFixtures.php +++ b/src/AppBundle/DataFixtures/ORM/LoadFixtures.php @@ -41,6 +41,8 @@ class LoadFixtures implements FixtureInterface, ContainerAwareInterface $passwordEncoder = $this->container->get('security.password_encoder'); $claraCustomer = new User(); + $claraCustomer->setAlias('Clara Haynes'); + $claraCustomer->setTitle('CFO'); $claraCustomer->setUsername('clara_customer'); $claraCustomer->setEmail('clara_customer@example.com'); $claraCustomer->setRoles(['ROLE_CUSTOMER']); @@ -49,6 +51,8 @@ class LoadFixtures implements FixtureInterface, ContainerAwareInterface $manager->persist($claraCustomer); $johnUser = new User(); + $johnUser->setAlias('John Doe'); + $johnUser->setTitle('Lead Developer'); $johnUser->setUsername('john_user'); $johnUser->setEmail('john_user@example.com'); $johnUser->setRoles(['ROLE_USER']); @@ -57,6 +61,8 @@ class LoadFixtures implements FixtureInterface, ContainerAwareInterface $manager->persist($johnUser); $tonyTeamlead = new User(); + $tonyTeamlead->setAlias('Tony Maier'); + $tonyTeamlead->setTitle('Head of Development'); $tonyTeamlead->setUsername('tony_teamlead'); $tonyTeamlead->setEmail('tony_teamlead@example.com'); $tonyTeamlead->setRoles(['ROLE_TEAMLEAD']); @@ -65,6 +71,8 @@ class LoadFixtures implements FixtureInterface, ContainerAwareInterface $manager->persist($tonyTeamlead); $annaAdmin = new User(); + $annaAdmin->setAlias('Anna Smith'); + $annaAdmin->setTitle('Administrator'); $annaAdmin->setUsername('anna_admin'); $annaAdmin->setEmail('anna_admin@example.com'); $annaAdmin->setRoles(['ROLE_ADMIN']); diff --git a/src/AppBundle/Entity/User.php b/src/AppBundle/Entity/User.php index c0fbd49a..19ad2fb1 100644 --- a/src/AppBundle/Entity/User.php +++ b/src/AppBundle/Entity/User.php @@ -7,7 +7,7 @@ use Symfony\Component\Security\Core\User\UserInterface; /** - * KimaiUsers + * User * * @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository") * @ORM\Table(name="users", uniqueConstraints={@ORM\UniqueConstraint(name="name", columns={"name"}), @ORM\UniqueConstraint(name="apikey", columns={"apikey"})}) @@ -46,6 +46,8 @@ class User implements UserInterface private $alias; /** + * FIXME remove me + * * @var boolean * * @ORM\Column(name="trash", type="boolean", nullable=false) @@ -67,6 +69,8 @@ class User implements UserInterface private $passwordresethash; /** + * FIXME remove me + * * @var integer * * @ORM\Column(name="ban", type="integer", nullable=false) @@ -74,6 +78,7 @@ class User implements UserInterface private $ban = '0'; /** + * FIXME remove me * @var integer * * @ORM\Column(name="banTime", type="integer", nullable=false) @@ -88,7 +93,7 @@ class User implements UserInterface private $secure = '0'; /** - * FIXME + * FIXME manytoone relation * * @var integer * @@ -97,7 +102,7 @@ class User implements UserInterface private $lastproject = '1'; /** - * FIXME + * FIXME manytoone * * @var integer * @@ -133,17 +138,32 @@ class User implements UserInterface */ private $apikey; + // ======= new columns for kimai 2 ======= /** * @ORM\Column(type="json_array") */ private $roles = []; + /** + * @var string + * + * @ORM\Column(name="title", type="string", length=50, nullable=true) + */ + private $title; + + /** + * @var string + * + * @ORM\Column(name="avatar", type="string", length=50, nullable=true) + */ + private $avatar; + /** * Set alias * * @param string $alias * - * @return KimaiUsers + * @return User */ public function setAlias($alias) { @@ -167,7 +187,7 @@ class User implements UserInterface * * @param boolean $trash * - * @return KimaiUsers + * @return User */ public function setTrash($trash) { @@ -191,7 +211,7 @@ class User implements UserInterface * * @param boolean $active * - * @return KimaiUsers + * @return User */ public function setActive($active) { @@ -215,7 +235,7 @@ class User implements UserInterface * * @param string $password * - * @return KimaiUsers + * @return User */ public function setPassword($password) { @@ -239,7 +259,7 @@ class User implements UserInterface * * @param string $passwordresethash * - * @return KimaiUsers + * @return User */ public function setPasswordresethash($passwordresethash) { @@ -263,7 +283,7 @@ class User implements UserInterface * * @param integer $ban * - * @return KimaiUsers + * @return User */ public function setBan($ban) { @@ -287,7 +307,7 @@ class User implements UserInterface * * @param integer $bantime * - * @return KimaiUsers + * @return User */ public function setBantime($bantime) { @@ -311,7 +331,7 @@ class User implements UserInterface * * @param string $secure * - * @return KimaiUsers + * @return User */ public function setSecure($secure) { @@ -335,7 +355,7 @@ class User implements UserInterface * * @param integer $lastproject * - * @return KimaiUsers + * @return User */ public function setLastproject($lastproject) { @@ -359,7 +379,7 @@ class User implements UserInterface * * @param integer $lastactivity * - * @return KimaiUsers + * @return User */ public function setLastactivity($lastactivity) { @@ -383,7 +403,7 @@ class User implements UserInterface * * @param integer $lastrecord * - * @return KimaiUsers + * @return User */ public function setLastrecord($lastrecord) { @@ -407,7 +427,7 @@ class User implements UserInterface * * @param string $timeframebegin * - * @return KimaiUsers + * @return User */ public function setTimeframebegin($timeframebegin) { @@ -431,7 +451,7 @@ class User implements UserInterface * * @param string $timeframeend * - * @return KimaiUsers + * @return User */ public function setTimeframeend($timeframeend) { @@ -455,7 +475,7 @@ class User implements UserInterface * * @param string $apikey * - * @return KimaiUsers + * @return User */ public function setApikey($apikey) { @@ -474,30 +494,6 @@ class User implements UserInterface return $this->apikey; } - /** - * Set globalroleid - * - * @param integer $globalroleid - * - * @return KimaiUsers - */ - public function setGlobalroleid($globalroleid) - { - $this->globalroleid = $globalroleid; - - return $this; - } - - /** - * Get globalroleid - * - * @return integer - */ - public function getGlobalroleid() - { - return $this->globalroleid; - } - public function getId() { return $this->id; @@ -565,8 +561,40 @@ class User implements UserInterface // $this->plainPassword = null; } + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @param string $title + */ + public function setTitle($title) + { + $this->title = $title; + } + + /** + * @return string + */ + public function getAvatar() + { + return $this->avatar; + } + + /** + * @param string $avatar + */ + public function setAvatar($avatar) + { + $this->avatar = $avatar; + } + public function __toString() { - return $this->getUsername(); + return $this->getAlias() ?: $this->getUsername(); } } \ No newline at end of file diff --git a/src/AppBundle/EventListener/MenuBuilder.php b/src/AppBundle/EventListener/MenuBuilder.php index 327be64f..33ad75f5 100644 --- a/src/AppBundle/EventListener/MenuBuilder.php +++ b/src/AppBundle/EventListener/MenuBuilder.php @@ -75,7 +75,7 @@ class MenuBuilder if ($isAdmin) { $event->addItem( - new MenuItemModel('admin', 'menu.admin_dashboard', '', [], 'fa fa-dashboard') + new MenuItemModel('admin', 'menu.admin', '', [], 'fa fa-dashboard') ); $this->eventDispatcher->dispatch( diff --git a/src/AppBundle/EventListener/NavbarShowUserListener.php b/src/AppBundle/EventListener/NavbarShowUserListener.php new file mode 100644 index 00000000..29b06c2c --- /dev/null +++ b/src/AppBundle/EventListener/NavbarShowUserListener.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AppBundle\EventListener; + +use AppBundle\Entity\User; +use Avanzu\AdminThemeBundle\Event\ShowUserEvent; +use Avanzu\AdminThemeBundle\Model\UserModel; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Class NavbarShowUserListener + * + * @author Kevin Papst + */ +class NavbarShowUserListener +{ + /** + * @var TokenStorageInterface + */ + protected $storage; + + public function __construct(TokenStorageInterface $tokenStorage) + { + $this->storage = $tokenStorage; + } + + public function onShowUser(ShowUserEvent $event) + { + /* @var $myUser User */ + $myUser = $this->storage->getToken()->getUser(); + + $titles = []; + $roles = $this->storage->getToken()->getRoles(); + foreach ($roles as $role) { + $titles[] = ucfirst(strtolower(str_replace('ROLE_', '', $role->getRole()))); + } + + $user = new UserModel(); + $user->setName($myUser->getAlias() ?: $myUser->getUsername()) + ->setUsername($myUser->getUsername()) + ->setIsOnline(true) + ->setTitle($myUser->getTitle()) + ->setAvatar($myUser->getAvatar()) + ->setMemberSince(new \DateTime()) // FIXME add column to entity + ; + + $event->setUser($user); + } +} \ No newline at end of file diff --git a/src/TimesheetBundle/Controller/Admin/TimesheetController.php b/src/TimesheetBundle/Controller/Admin/TimesheetController.php index 8d9ce632..30083c17 100644 --- a/src/TimesheetBundle/Controller/Admin/TimesheetController.php +++ b/src/TimesheetBundle/Controller/Admin/TimesheetController.php @@ -36,7 +36,7 @@ class TimesheetController extends Controller */ public function indexAction($page) { - $entries = $this->getDoctrine()->getRepository(Timesheet::class)->findLatest($page); + $entries = $this->getDoctrine()->getRepository(Timesheet::class)->findAll($page); return $this->render('TimesheetBundle:timesheet:index.html.twig', ['entries' => $entries]); } diff --git a/src/TimesheetBundle/Controller/TimesheetController.php b/src/TimesheetBundle/Controller/TimesheetController.php index 584f5c6e..2d3e98f6 100644 --- a/src/TimesheetBundle/Controller/TimesheetController.php +++ b/src/TimesheetBundle/Controller/TimesheetController.php @@ -30,13 +30,14 @@ class TimesheetController extends Controller { /** * @Route("/", defaults={"page": 1}, name="timesheet") - * @Route("/timesheet/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated") + * @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated") * @Method("GET") * @Cache(smaxage="10") */ public function indexAction($page) { - $entries = $this->getDoctrine()->getRepository(Timesheet::class)->findLatest($page); + $user = $this->getUser(); + $entries = $this->getDoctrine()->getRepository(Timesheet::class)->findLatest($user, $page); return $this->render('TimesheetBundle:timesheet:index.html.twig', ['entries' => $entries]); } diff --git a/src/TimesheetBundle/Repository/TimesheetRepository.php b/src/TimesheetBundle/Repository/TimesheetRepository.php index a7cfaebd..de405fdf 100644 --- a/src/TimesheetBundle/Repository/TimesheetRepository.php +++ b/src/TimesheetBundle/Repository/TimesheetRepository.php @@ -11,6 +11,7 @@ namespace TimesheetBundle\Repository; +use AppBundle\Entity\User; use TimesheetBundle\Entity\Timesheet; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query; @@ -26,16 +27,33 @@ class TimesheetRepository extends EntityRepository { /** + * @param User $user * @return Query */ - public function queryLatest() + public function queryLatest(User $user = null) { - return $this->getEntityManager() - ->createQuery(' - SELECT p - FROM TimesheetBundle:Timesheet p - ORDER BY p.start DESC - '); + $qb = $this->getEntityManager()->createQueryBuilder(); + + $qb->select('p') + ->from('TimesheetBundle:Timesheet', 'p') + ->orderBy('p.start', 'DESC'); + + if (null !== $user) { + $qb->where('p.user = :user') + ->setParameter('user', $user); + } + + return $qb->getQuery(); + } + + /** + * @param User $user + * @param int $page + * @return Pagerfanta + */ + public function findLatest(User $user, $page = 1) + { + return $this->getPager($this->queryLatest($user), $page); } /** @@ -43,9 +61,19 @@ class TimesheetRepository extends EntityRepository * * @return Pagerfanta */ - public function findLatest($page = 1) + public function findAll($page = 1) { - $paginator = new Pagerfanta(new DoctrineORMAdapter($this->queryLatest(), false)); + return $this->getPager($this->queryLatest(), $page); + } + + /** + * @param Query $query + * @param int $page + * @return Pagerfanta + */ + protected function getPager(Query $query, $page = 1) + { + $paginator = new Pagerfanta(new DoctrineORMAdapter($query, false)); $paginator->setMaxPerPage(25); $paginator->setCurrentPage($page); diff --git a/src/TimesheetBundle/Resources/views/timesheet/index.html.twig b/src/TimesheetBundle/Resources/views/timesheet/index.html.twig index f53dcf02..7f45b4ff 100644 --- a/src/TimesheetBundle/Resources/views/timesheet/index.html.twig +++ b/src/TimesheetBundle/Resources/views/timesheet/index.html.twig @@ -9,17 +9,17 @@ {{ 'label.date'|trans }} - {{ 'label.start'|trans }} - {{ 'label.end'|trans }} + {{ 'label.starttime'|trans }} + {{ 'label.endtime'|trans }} {{ 'label.duration'|trans }} - {{ 'label.user'|trans }} - {{ 'label.description'|trans }} + {{ 'label.username'|trans }} + {{ 'label.description'|trans }} {% for entry in entries %} - {{ entry.start|date("m/d/Y") }} + {{ entry.start|date(kimai_context.date_1) }} {{ entry.start|date("H:i") }} {{ entry.end|date("H:i") }} {{ entry.duration|gmdate }} diff --git a/web/avatars/default.png b/web/avatars/default.png new file mode 100644 index 0000000000000000000000000000000000000000..277c9f7811f434a965da923bde981897cd79bef9 GIT binary patch literal 12519 zcmeHtbySpZx9H3`NOvkVw9+6YIW!1T(%s$N%qU8SgoK2IAPg-a4bmw{cY~y~q?_X!unzi29_3UTA``PtOgqn&xF4jXV5D0{;s34;O0)gZ2z8Lob2%=SB z5Cp<`XeTYLrYJ29Q*(E*wsW)sffOQ=b*o)Fj&1k)xXw6p|Lfvnj2ulq9S$G$-n^%x@zi^T`zHy1rxwi8BqsCj|hWnU*uSHQL1pRqaIvp;zLp zs@K>15unuRYM=e#j9qr&84I&;7kX({GAK*8Zfb@U3VZVV4|~s_ck@)+BUH>4>Q9(f zDvvYF7QAhK$oBHA$MQvv=H=2&hF-jkFRVyen@bx#43)g5ah4<~d0=RRfY7iALCkfP zk{%DKU{G|bP(`$p&*rJOOL^bGR8rQv@Ve!rWS*+OedR`Gbvz^O`=k}d=-kUJP0{~` z;wRs!f}3?teOvK#g{JODJgt(Gt+qJ^Pb!594wF9q$+Fbs(+Wkz;nEhp z>2TOgUw!x+_Y1PP!_W~)UxhlkctU5S%%Lu!)rfs$h0O6i4>AgLOK$xwQ}1nV>+^VA zXRu!5DzXlL9lw4}*${tvg_AoGG>^ZziB+@oBni*sT6=)@N8;yO@oY)4g*QKid+CFG ztzNz<4UQ%{HHz`Ah`absWDrW-{mr7Vf~2FEnSyoOHg* zjA|u5Mmk3NOUI9p!EX;NVd^Dfn)}qHMCvs?yjEq z;I~0K!QzYDq)Seren;;)TQNbyyq`z{$J~^`LcgG(?nW2kv8lr|eZ00K?=nq|{PC4z zbgl?BWv^Q-ex?eLg5R`}aZ(-1AwC^yQd_-TxP8y87Dhtc{rQZjVq*oiJxYM9o z#J;KEG&&wqcmzWQ>9I^?i$2vxT_@PkhCPD4hzy2GDq!H6!5YE|&10wORl?C%qe5YC z71$GA=F>rBLKWVo^#%K;}R8GY#q`x1|?Ab9-nGSvJL>4X%n8n!3j z4)Xp7LR(Q`OwtdRwmc0WqsTI8)JU@}hd)+xsIj!w7IroMy;Roj&S)Ny!F$;4xs{si z_YzG%SMruXQ0>1ga9vBwO?gYuufvJm?tg3FU%tiZA$H}=iW}}uS*AQ(@Z|`;-}+Lb zE#Rme`cN|4ROyhu0<#YaHe=06Qj?rk6nZ9vy_Ct>E<*pdmrhqbm}04POedQtlrN?6*iOujYn!nsaa{HauFXXGHnTTpU9&3OP^t`e zK$i{kNytvP)*9Cu)N0bq)$(N>(|T95r)H;CUo!e}RQ3A@U5&s;M@j7cqW!}C!Toyu zM*WmpT3WJwd6_R z$5AXhCA*30vFht;e9!v}S>Fmywmi0yPxbv-et%4lOt05R%v#o%*I?Cj*r!bRk86!{ zjK3Oxy7PI0Z6dB{N$p_F)KA~GWRR$b6X!LTNuBiE+@Na|%UUEAZyN6!=o_Je^W0&~@qqyIPCXI3o3IkN}r#^qD zEEm*r`Mjvtob^_pp;_Kt{#}eH-*K&Yu}dB;NBVCZw zYW8l$zJvJ9f*cNm2}90z`@BTCVZGTBkT#wJ^)3!-N_RoMae44>VspA6X#I=@Gy2Bg}gm-9=w+%C3{()XF?$YypuQKD9= zwW_p=*)U>DPtMkA(Mq1sa>%U-^Qt{#-u0#&@19^DD;V9Z;;`SfW~*MXoa2e)?)ONt z)tvZ5>8;raYNT&`*ogj1`C8$c=vw$XC2vTsFxHso>Sn7PuMF=go(R=XA$#FiFLpOa z*T8+twN9^M&xRdk4?M4|HN%6O{;tW_2URm01#BH&8M{(b)>B;0Kb?ub{B~aWY5mjg zC*^{RudQFFFZ)s5vf?4mX~x>j*5&tsA=Joj{FKR}_u|ZAKvQ;1B~=IQXNy4wT4LJR zVFB7St^vUzA5{6^@O0do&c^x3(s=fd*0Dc!2rsyH+ae+biHA&R%R?T#YI zkn5Cn4ELq5BYqs=ND&rGDdId)*Qma!p8Ki6mS8B>m^6s1o0nN)^UG1^5ziW1k3@u5 zbmoHq@mNAZCTG?8#*4NKt0Nzx5~6&}p$Pj3Vqz5W5(^qjsd55SoJv-5;A2@X*;I|} zXV0E1FQm;1(pk2rV;T_~vAk1h67O>#tybhnJY)W$EG{nVgk{se!Y8G#F`HFAoMEd% zTF8}|*}~JDLfL6F5cKurN9m^5Nzet`rRtMFUTtep;tLFI%!VhwdD(aaLM7XA+jaC- z^bD#NZRBkZ`e}L<`=7=ueS1D2#ym=574>cX!RGUUU4pqtrLL;L@*Dok<^+tsZ;*Ub z4auM!GSQvtXQlZPinJt`WLjiFsp&#zhkP-1xpt*vP6b?9x79+`7by=eaDGN_rgcC0 zF%-znW;$6r)4O*%GvuNCgFSVOU(oUAProF;nUi2Hw?}$<$KJuCH*e7HV{dZ$p1zn? z<^Oo9p|~CMdJ+2zdpkKMc@2lRaZ~Wo6tD42mwVW8uhA^}bq1ZTx_w<6Hp0#t$vw;t9Hw4kIJg|D=xJmHBj7uW8=lb6Gj9(q@Gs!of z6M4x<7tJwf@84gf0Kx4+%Tbo5A4103+ES?F_c%c5$I0j7^#m&qLQv^cHPswx*un83 zV9^Gr=bTvAuYhI&(^Wyw0|X*ty8D6^H5d;;APCe>Q`b}Xxw5c@ixa1rrHi>0r>~PM zKn((k`U(S&PF9{~FkdG}XAfatG5Wt0!oc$#n2R3vm&DUSj9&M-8cf>7-3rFf$;Zh} zFOCI+!9?9Ht%WsYracGmw7wmZx}Y=8Il&vK%7!i3fAe61YyWbB-*oIL>5#QFI7 zME@@HKQRBT=znnP{s-q1f&b+E56pjX-f1GN>TYKR80pRw;yj{U{{!|fdQq-BJN<{< z{&^{Xp}<=a#}eiGuQx1?b&Xdm4g%3zD9T7``hxct#NH9>ETS(TX9&JV17W)+K9?f) zBNB|D1Jf%Y!WCh>DzJ7;Z)o3gr_h58(A!Lqx;!5?i0i`<#1tHSIi)MCfnrQ9+iA=a zE>u_gb=_ZK;Q4*Nq{NB5sZ~-5qEfKxWHdk6d@4Ch#}=W!7zCB9V3AP5dlom z(VG4a56;E?U)BCUTJ295qHP+w*c^}&bandp{mt#gk;Sbr6&^nRXM9?5k_0LdZ*~y? zHE&a>lG(p%V`IZ25`5!Z+t?^_UM~oJ!+zLhk{w3p^1+GnOWW74k~DY+ORd?rWd@c+ ze1zE+71Z;yU-ba7Elb&RyR(nf$5cumn&s-&e={6-yuK`w1e>69adS3ww!KmPKKKgoRUgE2pt9QPI5?UkPUj40%~bb zmsUr&QM_9Fq?Ex{A9U><9KO3{vq)@f07q0C-J3LdbhzG^xVO>~mbdLjLO&QC4ZBbQKeQe*NJHt3hfo9%NC?3 z2t&~et-2d_*dy!IkGf+hFP|n%ytqn&H$x!##wJQ!bObtD3#}nbE5$_GpA&^j_)#lf zlmHIbYj+SWcghVg+gIa+IZgf^#b0&i1Mm-kwMMJ!&Nd)!Wen{MH1@XB#xTz7pqmS~ zTX@fYiZv#*AJaD@;3>-stw#hpx<<|ZTE3dL4+XV@&n9#(*9=(=OJoUkAAdlvO6UpG z!V*$9{4hLsf14>g5{@regrX@>K4i1pV9q4W0JyEx~qS=Jxgehct^eiZb` zj5Z7p7rFyP7_U$I)|>=!`aV@n3*7{CL@ZNzKM$772sj*UmATrds*_T0fQR6+znhVcA2#Yhv;~-f+M~8|Fwt6?}FJ%9%V! zwM`t#UTwrmGm>)}ZkhIx*lEMEa?a*lTI+=!$hvkJ!D4rzjhZyousFc8>3o4WJiyH` zbE4UQ+adz0K3fdu+eihqg+qDvCBa;)0Zqqw$M$X+>4k zj5cDS_^*P?g=o}?e9|c2&=HsbdUsA6)%W5bhw zh~Y>NgA07;b>dup4-K0kSar=mq3bA_Yp8vO7-te0*$_V98HxNI@Od>b+{PNWF%~s1 z`~zM*PIXarclb|!3|||^A*Vb7;}qtLoM9CzaD2#LQz;=r|1FAN8ZDz_nm^T3;v>_1dfa&xmqZ9jiH|2o|s+YaEu7{uB_ z4%+)R!~Abp?YI-BX-ZUOIBfFnqXC{nOx4x|ybIBqWCNcdX>=&=S5CwchcVXAkG zB$o`OE)ay?^+g9ZE%x+Mw!Iyi!hQ-_b>f7<`9h_TKZdh+gGlhI2eFYB0N~Ut6`iSVd;}oYWAQmm0SK`=ouC2{suZXd26$Wra;LjuGm+C z(D(OMO@}TqPU{nJF5w_%=Y-CE2nt&LR!ey8N*NK2YSD3a%7Dd)Dc?hCK_HgN-|EDP zVQa+)$p*V=@@#&A7hm9kD@mwIzOcJ={8ba?O!kD}5re5sUi9c;At9`zd~MX=5&YSU zm|!Uh@b*Q6=Riz zQ!o}X*sBt4O4$#=;PaQ`F7s#n81ZFOYWFrg*c-v?;4-OlFGO;~3NvOr?$B1S77NUSTt44EFh35~iGi;3bQB7vXJ5D_?m>0Fee^>V;krBW? z7QGQ!i6CZq)96-PqK$k}s4~$Ih<#9t0Bw8!=F<@V1AS;lv;M3)Q&QVuJBA3LS5~-N z=`-L3+7|ic`?9gst8CMIi7cwx6(mtxD1gynNK4lltUb&M6i z^E{wl<`0g)o>M95)c{wkqtk-+JTOy0M79GA|eItWjcxIRZr2v+Iwx&4N>fWMYe4XHL_TyCxv?YeNJRoEe~ z)9urk7|f~H$0ZYzAK*2rAF-O{tO$Pm1qSZW8S-`ev|$gLdVz=?ff`zlXJzT$pXiGzzv zISZ54U;$cnOhH#;vp9Hk|H|&8C!R%3`{HG?-0=7c>YL7sJTwwmjQhETl^g#g;0IUg zx*r{tVg9suCj8wCQKXph#+olqUN^a|$(^v(bgN*`w*)ngZM>-41YGT8vsu@Is@t8n znOti^fo{$1Ps^Vs!X4vM8N_LELpqxyRk3Sxh{vLlN#dgXl*6aYW6XH(`GCqv41#7r zu2$*m=*6%?lJRHrbFZ=E7qyLkqfKEKE!E*Wj9hO?bJV<$1CPcAxJ(w4kG?N1{`kS4 zwx$BlItOCOt)(~oX6yC%+HA_louJc?1+$L}L3TaOIu0=*^gpq;wUV_orlZKYgwNF@ z)?LYu?@GiK=PVA?v*n@pf-@5+9h82?hNmHnGi!Qj$!E>9XSZm7bsGs(QDS-_hOgJu z55$T}up%EctN3KIZ_en$hU*>EKh+zygDuU&2L ztJ7^W=Zw-~!#28-e#NlXwm4b|Ym=a}WNE#7sGVt^i?b;c^ja-}LKUC{5cWcE*b}P1 zx;R=xIxh$bvXwPlZ5L*D*B{ji>j5?USWG~79V;824+W5+MWAI359N#m>PheX0a-e5 z8E>X(yNBVcyUM*&wO#6pmv483EamzZ7I-owIFlW6`?P|CvS+h%VH04KH=3$UhUDW- zyyZAqX!My$X4A>gJkV?I`Ay?CI-i5e(lQK-+$GQsW6dj|aYEx;DkrhpYWPu1aDQDpBd#ifZDU-_ zxStb!1kV`ZZ`7iw=kL>vWRbo3KXz!qgJJ1Jv-GM@~h~hE5Yi zHwxNpa5;_dPR<3_GPB>wY4+>l>`L*>Vv1@FW+(fC=#TJ+Mxx18Lw}z56HVID>LDS zcBHBkXQrL|R6|C_KqdBt*XlJyrA#OM(og-!O6@3!#+jgGa$-~)TL`^swx^IEv(xza z_O_+Gr?d?yVxKOlgY#t_*@1$WM=!)C9F+r@(>fJd!{Uz}&Xu#!cgm8WE zZ~uLk{yddc!wH}~8z6VK?tf$xY5-LV9dcYARNl0T1A;;v+kx$O3)LgwkX_Q=IJ&HE zc|Ja{7(NEN48L+ltJ|s*ndz=%{gw~Ky6l|8%{~XlEh#3Vviuf#Jx3?Q5<0>6wiXwS z1uorM3ep?wJsv~ypZ~_1G_tWJA?3Qg2)S56537lEwWpdIUn#~o-15?FzNaDc1T*1+ zLpd_EK|%%L*&2(bMDc?s_;Mp11j>dNO&!KuBdq zJxqr3z}VgL3iTT6Sm(A2Yh=Wv8J-8}sn^MaBgZEhkMOHp;%>q_IG>-%F`p6r!C4zIJ{`SsKhw^b3m*v=+b@t} zXv&=;3`4b}xTK*G^3|T7YoQA0yG*~z*5aBR!chGhT}-XtDeYFd2fy=nb)l`0g*Ds) zF)few%P(;VgXn6L`i(O$A2TJj2qVz2?>WVB;O%#Pi(so|=AXTF0gnb#4<>)W48 zc6Y86Pb*Q4j*R1mzfYA-?aax*cy~c9!Nd9H!lrSLdzz8LdWw{eP~s?#<5%?Dme8=94m?^nVoj-cTNgaSg^&y zqdPu4pC_1}ERYW$58~1=n43&F!-{|Bz}mjnpUn2^Ryv!YC(D9Z5T-II*uC^UYqo)| z0{5T}gYMgx2eUxqv|snbf%=8-AnzBygrHg~>41gv#Py;xIvI_P+H6&Xqf2U2=zBqz zUpwyo>$pa|78$0o9R$x8M5^*BnrW$1))!ktT8VuUzP#Rc)fPF1CTJh@rTKaQ472E5c)h|H^OHM2^Hl1JowcxWsbKjbs+J$#$94*V|sGF4j{vq+F z5C7HVmp5))dWdnuKS$iHD4p`jCbFn(3}qT)iq^G4CFxUMdhXSpr!m&{%y>cFmjq!| zJ4a-&vpSWOhhb%JZbWmp-*}&PIew5Ke2rCevD^6dr{3@87<_ae$TecBXcW+5 z{b@;I`cUK>v{R{C^r?F@i)sgX5a}q`NHxN^_t&aF%;4cpkRzt8H-rOg>$PK8IeJy5 z$uD49#gSs_4S~)Qg&Qjb(nireha>XqpZ|zFKnW1QMohpV9ot?k@X`HaG4dt17PJp~ zn4a+Ev)!pniwhC8q(xFmgrmCEFT>ilCu~SO%nJh*6b?9H^W8yZXI2zozgr*(R|}KC z?3~l?r(oC!K&VMl1J7L1qb9fMG2=UT+|IP8tH&9s!cX&G=;C>|c3zs9ID$qR&|F8(hy0qB#bNE*z}Z!16~@Yh`Rj$(xioRY&R zUjo#_(3_Z)a&EB3B5SK3cNw`Lpn)Mctg?Tq6!h3-qbQS7H!TTnf@aRb(^BxY0;niT z1k+W_hAp41pdlr(h43}X{LvJi76!6{tHAFRF;8{dmi+7ciD4TV^FhX=eU{KSuw@zl zdVLZA!W+QIl%p(X3WB&jJ!tefxL?6^=KK=$czf4@OGzp%x(yCJm73Ja2kE&asjeI^ zB&abna1#I?b4L0YK1sQea_SeTi1{dyfFd4|*?pFL2lT6!#AE;<>F_|Sj-EK$uv)!j z=Qz;guXd{W&;&&c9JRLJFcg~>qU2Yfgcp1DzhK|icAk;KP(jD4OD#bL=B$apzWIQa zCiq$5+ShetfiZD$m^gaC?8j>#D}F-U8D_Y)W&x2V3rBH@>ALyhVgp(BlI2@S3HX=? ziNqF)u4#IS*6aTMhS+GDA1=Y>z8+z}T1byPR1TP5z0;`%7}u~$NCm{{xtYN%qfrh92$IHii!-?PGQb!P z6(vU1RiP?{U7$6Kpu1wl)R@m|8DK|XdTJBk5#(sy& zF#|8e4QPDK$%otYSIzm5JXk^x)eUzc`O{ruFeSriW^Qfp4;gX?V+4Epx`n4+N*l#< z(|n-wu3Say%~ceav(STB@FCw~)g}4-8-!p-@}##Zpk<)zmH2|IY|U~0w)((WMkzip zW}k>64>MY`tr-?Y$^|ET9!&>K3SJ^SVAzalK*#Nd>4J<?aiQAf@c)SKeO(RApvTV?6;@ zxNmVhYTPZ;?gD{YqXBdiy}W=~-nM>DQAe1QfFWsl#3wQT7*8crSnHoR$L_i}&GQ2u z%Lf9xLm3xm2OhzBBHMb=sM2&DK@iLDjIjO&LWmY8wf zRnTr*2m|T#!UFa7JFX>1a8;+Aj~8Y%;M%OqBfreMuz?CLFq)?KuhXf)vQ$=x7P}(J z0WZ2twPiO6`F)n48p8}XRfYhq9?jV1yG3nq)k}L97niO=aeBBZJLHy}WUBh_q0AET zasCI2191a_tXUU72Rzc~xDwBz^zPKX#SjXtY0CY3^b>cd%FEN*d!QINTCktut)APX zYJx7i`$RAYj5YfbViuK>wEjD+I_z6e-IcxH5Ec+c##Eh`TEv3((ZG;3!ox4E4P|J( z_SrXMLiZmU>D{{nh7Vk1ikcWLK`d*APhSMkzrfYX^z8X?WHThtzb^W z6`K2FrQqw(4v?ER`7?#f`rXAX`+-zqj4kSW2s?SL{$3|XTKE0?p&`N_(s%WKz1t#? zk!g_zU!1Leo;(AhZ#>_ZZNt{vE7Y}g!9!z$trR$wECJ277xP`de!7-gL@-12eGjTn z5Po=Z)I)2ad*Z777|!}hiB1EYD@$Oj2K&iJPBZsP1ih+xWuB&VFscPzXFrh0Ek(!`r*1=oE5WjciyVTks%b%cfHqzf$L&c&X(1WRfY`Yef${dvPs&YN z7lX~-!A+r%?{m|0a|4`44eM3&WV;54>hKWH2{&Ihl{#7~s&(Y5VV^qPK@U5@4}sj2 z%Erd`e`r3->eLz`JU=pH^%I^aG==;TkK(F`q*eXR0q3=^xSOcndu8@OfL@5 z;(#~kuqY{!h$UepTu^@{k`5iT3_oFQvG>Vc@kv2jZR%Pdc)xniNR{iubKNp9_rXz* z8D0&!3z7;%t}Ooldu#u{e<%Q);XnR{(e~%wbH;08gm&sX5b#lyRgo!^GJW}f0256K A6aWAK literal 0 HcmV?d00001