user profile, editing and more statistics

This commit is contained in:
Kevin Papst
2016-11-09 21:58:00 +01:00
parent 1678129fe9
commit 8401797728
42 changed files with 1441 additions and 195 deletions

View File

@@ -26,19 +26,19 @@ class AppKernel extends Kernel
new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(), new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(), new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(), new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(), new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(),
new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(), new Avanzu\AdminThemeBundle\AvanzuAdminThemeBundle(),
new AppBundle\AppBundle(), new AppBundle\AppBundle(),
new TimesheetBundle\TimesheetBundle(), new TimesheetBundle\TimesheetBundle(),
new Avanzu\AdminThemeBundle\AvanzuAdminThemeBundle(),
]; ];
// Bundles only used for development or unit and functional tests // Bundles only used for development or unit and functional tests
if (in_array($this->getEnvironment(), ['dev', 'test'])) { if (in_array($this->getEnvironment(), ['dev', 'test'])) {
//$bundles[] = new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle();
//$bundles[] = new Symfony\Bundle\AsseticBundle\AsseticBundle();
$bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle();
$bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle(); $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();

View File

@@ -1,4 +1,4 @@
<!-- User Account: style can be found in dropdown.less --> {# User Account: style can be found in dropdown.less #}
{% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %} {% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %}
<li class="dropdown user user-menu"> <li class="dropdown user user-menu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">
@@ -10,8 +10,8 @@
<li class="user-header"> <li class="user-header">
{{ macro.avatar(user.avatar, user.username) }} {{ macro.avatar(user.avatar, user.username) }}
<p> <p>
{{user.name}} - {{user.title}} {{user.name}}
<small>{{ 'Member since %date%'|trans({'%date%': user.memberSince|date('m.Y') }, 'AvanzuAdminTheme') }}</small> <small>{{user.title}}</small>
</p> </p>
</li> </li>
{# {#
@@ -28,10 +28,10 @@
<!-- Menu Footer--> <!-- Menu Footer-->
<li class="user-footer"> <li class="user-footer">
<div class="pull-left"> <div class="pull-left">
<a href="{{ path('profile'|route_alias, {'ident' : user.identifier}) }}" class="btn btn-default btn-flat">{{'Profile'|trans({}, 'AvanzuAdminTheme')}}</a> <a href="{{ path('user_profile', {'username' : user.username}) }}" class="btn btn-default btn-flat">{{ 'label.user_profile'|trans }}</a>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<a href="{{ logout_path() }}" class="btn btn-default btn-flat">{{ 'Sign out'|trans({}, 'AvanzuAdminTheme') }}</a> <a href="{{ logout_path() }}" class="btn btn-default btn-flat">{{ 'label.logout'|trans }}</a>
</div> </div>
</li> </li>
</ul> </ul>

View File

@@ -0,0 +1,20 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
{# Tell the browser to be responsive to screen width #}
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
{# Bootstrap 3.3.6 #}
<link rel="stylesheet" href="{{ asset('theme/bootstrap/css/bootstrap.min.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
{# Ionicons #}
{#<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ionicons/2.0.1/css/ionicons.min.css">#}
<!-- Theme style -->
<link rel="stylesheet" href="{{ asset('theme/dist/css/AdminLTE.min.css') }}">
{# AdminLTE Skins. We have chosen the skin-blue for this starter page. However, you can choose
any other skin. Make sure you apply the skin class to the body tag so the changes take effect. #}
<link rel="stylesheet" href="{{ asset("theme/dist/css/skins/"~avanzu_admin_context.options.skin~".min.css") }}">
{# HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries.
WARNING: Respond.js doesn't work if you view the page via file:// #}
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="de" datatype="plaintext" original="not.available">
<body>
<trans-unit id="Member since %date%">
<source>Member since %date%</source>
<target>Mitglied seit %date%</target>
</trans-unit>
<trans-unit id="Profile">
<source>Profile</source>
<target>Profil</target>
</trans-unit>
<trans-unit id="Sign out">
<source>Sign out</source>
<target>Abmelden</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -25,10 +25,22 @@
<source>security.label.password</source> <source>security.label.password</source>
<target>Passwort</target> <target>Passwort</target>
</trans-unit> </trans-unit>
<trans-unit id="security.label.password_repeat">
<source>security.label.password_repeat</source>
<target>Passwort wiederholen</target>
</trans-unit>
<trans-unit id="security.action.sign_in"> <trans-unit id="security.action.sign_in">
<source>security.action.sign_in</source> <source>security.action.sign_in</source>
<target>Los geht's</target> <target>Los geht's</target>
</trans-unit> </trans-unit>
<trans-unit id="label.logout">
<source>label.logout</source>
<target>Abmelden</target>
</trans-unit>
<trans-unit id="label.user_profile">
<source>label.user_profile</source>
<target>Mein Profil</target>
</trans-unit>
<!-- <!--
Menu / Navbar items Menu / Navbar items
@@ -173,6 +185,46 @@
<source>label.project</source> <source>label.project</source>
<target>Projekt</target> <target>Projekt</target>
</trans-unit> </trans-unit>
<trans-unit id="label.hourly_rate">
<source>label.hourly_rate</source>
<target>Stundenlohn</target>
</trans-unit>
<trans-unit id="label.hours">
<source>label.hours</source>
<target>Stunden</target>
</trans-unit>
<trans-unit id="label.rate">
<source>label.rate</source>
<target>Umsatz</target>
</trans-unit>
<trans-unit id="label.language">
<source>label.language</source>
<target>Sprache</target>
</trans-unit>
<!--
Buttons & Actions
-->
<trans-unit id="label.actions">
<source>label.actions</source>
<target>Aktionen</target>
</trans-unit>
<trans-unit id="action.edit">
<source>action.edit</source>
<target>Bearbeiten</target>
</trans-unit>
<trans-unit id="action.delete">
<source>action.delete</source>
<target>Löschen</target>
</trans-unit>
<trans-unit id="action.save">
<source>action.save</source>
<target>Speichern</target>
</trans-unit>
<trans-unit id="action.updated_successfully">
<source>action.updated_successfully</source>
<target>Änderungen erfolgreich gespeichert</target>
</trans-unit>
<!-- <!--
Dashboard Dashboard
@@ -213,6 +265,10 @@
<source>badge.invisible</source> <source>badge.invisible</source>
<target>Nein</target> <target>Nein</target>
</trans-unit> </trans-unit>
<trans-unit id="label.toggle_dropdown">
<source>label.toggle_dropdown</source>
<target>Menü anzeigen</target>
</trans-unit>
<!-- <!--
Timesheet Timesheet
@@ -237,6 +293,30 @@
<source>profile.subtitle</source> <source>profile.subtitle</source>
<target>Verwalte Deine Einstellungen</target> <target>Verwalte Deine Einstellungen</target>
</trans-unit> </trans-unit>
<trans-unit id="profile.about_me">
<source>profile.about_me</source>
<target>Über mich</target>
</trans-unit>
<trans-unit id="profile.first_entry">
<source>profile.first_entry</source>
<target>Arbeitet seit</target>
</trans-unit>
<trans-unit id="profile.tab_monthly">
<source>profile.tab_monthly</source>
<target>Jahresstatistik</target>
</trans-unit>
<trans-unit id="profile.avatar">
<source>profile.avatar</source>
<target>Profilbild</target>
</trans-unit>
<trans-unit id="profile.settings">
<source>profile.settings</source>
<target>Einstellungen</target>
</trans-unit>
<trans-unit id="profile.password">
<source>profile.password</source>
<target>Passwort</target>
</trans-unit>
<!-- <!--
Admin: Timesheet Admin: Timesheet
@@ -285,6 +365,14 @@
<source>admin_user.subtitle</source> <source>admin_user.subtitle</source>
<target>Alle registrierten Nutzer</target> <target>Alle registrierten Nutzer</target>
</trans-unit> </trans-unit>
<trans-unit id="edit_profile.title">
<source>edit_profile.title</source>
<target>Profil editieren</target>
</trans-unit>
<trans-unit id="edit_profile.subtitle">
<source>edit_profile.subtitle</source>
<target>Einstellungen von %username% ändern</target>
</trans-unit>
<trans-unit id="label.alias"> <trans-unit id="label.alias">
<source>label.alias</source> <source>label.alias</source>
<target>Name</target> <target>Name</target>
@@ -346,6 +434,57 @@
<target>Umsatz total</target> <target>Umsatz total</target>
</trans-unit> </trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Januar</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Februar</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>März</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>April</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Mai</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Juni</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Juli</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>August</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>September</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Oktober</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>November</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Dezember</target>
</trans-unit>
</body> </body>
</file> </file>

View File

@@ -16,17 +16,24 @@
'label.title': 'graduation-cap', 'label.title': 'graduation-cap',
'label.active': 'lock', 'label.active': 'lock',
'label.roles': 'users', 'label.roles': 'users',
'label.actions': 'pencil-square-o',
}) }} }) }}
{% for entry in entries %} {% for entry in entries %}
<tr> <tr>
<td>{{ entry.id }}</td> <td>{{ entry.id }}</td>
<td>{{ entry.username }}</td> <td><a href="{{ path('user_profile', {'username' : entry.username}) }}">{{ entry.username }}</a></td>
<td>{{ entry.alias }}</td> <td>{{ entry.alias }}</td>
<td>{{ entry.email }}</td> <td>{{ entry.email }}</td>
<td>{{ entry.title }}</td> <td>{{ entry.title }}</td>
<td>{{ widgets.label_visible(entry.active) }}</td> <td>{{ widgets.label_visible(entry.active) }}</td>
<td>{{ entry.roles|join(',')|trans }}</td> <td>{{ entry.roles|join(',')|trans }}</td>
<td>
{{ widgets.button_group({
'edit': path('user_profile', {'username' : entry.username}),
'trash': path('user_profile_delete', {'username' : entry.username})
}) }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -0,0 +1,13 @@
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{{ user.username }}</h3>
</div>
{{ form_start(form) }}
<div class="box-body">
{{ form_widget(form) }}
</div>
<div class="box-footer">
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
</div>
{{ form_end(form) }}
</div>

View File

@@ -0,0 +1,13 @@
{% extends 'base.html.twig' %}
{% block page_title %}{{ 'edit_profile.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'edit_profile.subtitle'|trans({'%username%': user.username}) }}{% endblock %}
{% block main %}
{{ include('default/_flash_messages.html.twig') }}
{{ include('admin/user/_form.html.twig', {
user: user,
form: form,
}, with_context = false) }}
{% endblock %}

View File

@@ -38,11 +38,14 @@
{% block avanzu_head %} {% block avanzu_head %}
<link rel="stylesheet" href="{{ asset('css/kimai.css') }}"> <link rel="stylesheet" href="{{ asset('css/kimai.css') }}">
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script> <script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script> <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<script src="{{ asset('theme/bootstrap/js/bootstrap.min.js') }}"></script>
<script src="{{ asset('js/kimai.js') }}"></script> <script src="{{ asset('js/kimai.js') }}"></script>
<script src="{{ asset('js/Chart.min.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
$('.dropdown-toggle').dropdown();
$(document).kimai({imagePath: '{{ asset('images') }}'}); $(document).kimai({imagePath: '{{ asset('images') }}'});
$(document).kimai('ticktac', 'a#ticktac'); $(document).kimai('ticktac', 'a#ticktac');
}); });

View File

@@ -7,6 +7,7 @@
{% block main %} {% block main %}
{# FIXME Übersetzungen hinzufügen #} {# FIXME Übersetzungen hinzufügen #}
{# blue / yellow / purple / green / black #} {# blue / yellow / purple / green / black #}
{# bar-chart / line-chart / calendar / clock-o #} {# bar-chart / line-chart / calendar / clock-o #}
{# {#
@@ -30,16 +31,16 @@
{{ widgets.page_header('dashboard.you') }} {{ widgets.page_header('dashboard.you') }}
<div class="row"> <div class="row">
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-3 col-sm-6 col-xs-12">
{{ widgets.info_box_counter('stats.durationThisMonth', timesheetUser.durationThisMonth|duration(true), 'hourglass-o', 'blue') }} {{ widgets.info_box_counter('stats.durationThisMonth', timesheetUser.durationThisMonth|duration(true), 'hourglass-o', 'green') }}
</div> </div>
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-3 col-sm-6 col-xs-12">
{{ widgets.info_box_counter('stats.amountThisMonth', timesheetUser.amountThisMonth|money, 'money', 'green') }} {{ widgets.info_box_counter('stats.amountThisMonth', timesheetUser.amountThisMonth|money, 'money', 'blue') }}
</div> </div>
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-3 col-sm-6 col-xs-12">
{{ widgets.info_box_counter('stats.durationTotal', timesheetUser.durationTotal|duration(true), 'hourglass-o', 'yellow') }} {{ widgets.info_box_counter('stats.durationTotal', timesheetUser.durationTotal|duration(true), 'hourglass-o', 'red') }}
</div> </div>
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-3 col-sm-6 col-xs-12">
{{ widgets.info_box_counter('stats.amountTotal', timesheetUser.amountTotal|money, 'money', 'red') }} {{ widgets.info_box_counter('stats.amountTotal', timesheetUser.amountTotal|money, 'money', 'yellow') }}
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -65,13 +66,13 @@
{% if is_granted('ROLE_TEAMLEAD') %} {% if is_granted('ROLE_TEAMLEAD') %}
<div class="row"> <div class="row">
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-3 col-sm-6 col-xs-12">
{{ widgets.info_box_counter('Anzahl Benutzer', user.totalAmount, 'users', 'blue') }} {{ widgets.info_box_counter('Anzahl Benutzer', user.totalAmount, 'users', 'red') }}
</div> </div>
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-3 col-sm-6 col-xs-12">
{{ widgets.info_box_counter('Aktive Benutzer diesen Monat', timesheetGlobal.activeThisMonth, 'users', 'yellow') }} {{ widgets.info_box_counter('Aktive Benutzer diesen Monat', timesheetGlobal.activeThisMonth, 'users', 'yellow') }}
</div> </div>
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-3 col-sm-6 col-xs-12">
{{ widgets.info_box_counter('Aktive Benutzer jemals', timesheetGlobal.activeTotal, 'users', 'red') }} {{ widgets.info_box_counter('Aktive Benutzer jemals', timesheetGlobal.activeTotal, 'users', 'blue') }}
</div> </div>
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-3 col-sm-6 col-xs-12">
{{ widgets.info_box_counter('Momentan aktiv', timesheetGlobal.activeCurrently, 'users', 'green') }} {{ widgets.info_box_counter('Momentan aktiv', timesheetGlobal.activeCurrently, 'users', 'green') }}

View File

@@ -89,3 +89,33 @@
</a> </a>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro button_group_dropdown(title, actions) %}
<div class="btn-group">
<button type="button" class="btn btn-default">{{ title|trans }}</button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">{{ 'label.toggle_dropdown'|trans }}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% for url, entry in actions %}
<li><a href="{{ url }}">{{ entry|trans }}</a></li>
{% endfor %}
</ul>
</div>
{% endmacro %}
{% macro button_group(actions) %}
{% import _self as macro %}
<div class="btn-group">
{% for icon, url in actions %}
{{ macro.button_action(icon, url) }}
{% endfor %}
</div>
{% endmacro %}
{% macro button_action(icon, url) %}
<a href="{{ url }}" class="btn btn-default">
<i class="fa fa-{{ icon }}"></i>
</a>
{% endmacro %}

View File

@@ -5,17 +5,165 @@
{% block main %} {% block main %}
{% import _self as widgets %} {% import _self as widgets %}
{{ include('default/_flash_messages.html.twig') }}
<div class="row"> <div class="row">
<div class="col-md-6 col-sm-12 col-xs-12"> <div class="col-md-3">
{{ widgets.profile_list(user, settings, 'aqua-active') }} {{ widgets.profile_box(user, stats) }}
{{ widgets.profile_infos(user, stats) }}
</div>
<div class="col-md-9">
<div class="nav-tabs-custom">
<ul class="nav nav-tabs">
<li {% if tab == "charts" %}class="active"{% endif %}><a href="#charts" data-toggle="tab" aria-expanded="true">{{ 'profile.tab_monthly'|trans }}</a></li>
<li {% if tab == "profile" %}class="active"{% endif %}><a href="#profile" data-toggle="tab" aria-expanded="false">{{ 'profile.settings'|trans }}</a></li>
<li {% if tab == "password" %}class="active"{% endif %}><a href="#password" data-toggle="tab" aria-expanded="false">{{ 'profile.password'|trans }}</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane {% if tab == "charts" %}active{% endif %}" id="charts">
<script type="text/javascript">
var barChartOptions = {
scaleBeginAtZero: true,
scaleShowGridLines: true,
scaleGridLineColor: "rgba(0,0,0,.05)",
scaleGridLineWidth: 1,
scaleShowHorizontalLines: true,
scaleShowVerticalLines: true,
barShowStroke: true,
barStrokeWidth: 2,
barValueSpacing: 5,
barDatasetSpacing: 1,
responsive: true,
maintainAspectRatio: true,
tooltipTemplate: "{% raw %}<%= value %>{% endraw %} {{ 'label.hours'|trans }}",
};
var barChartLabels = [
{% for i in 1..11 %}
"{{ ('month.' ~i)|trans }}",
{% endfor %}
"{{ 'month.12'|trans }}"
];
</script>
{% for year,yearStat in years %}
<h2>{{ year }}</h2>
<div class="chart">
<canvas id="barChart{{ year }}" style="height: 230px;"></canvas>
</div>
<script type="text/javascript">
var barChartData{{ year }} = {
labels: barChartLabels,
datasets: [
{
{# label: '# hours',#}
fillColor: "#00a65a",
strokeColor: "#00a65a",
pointColor: "#00a65a",
pointStrokeColor: "#c1c7d1",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(220,220,220,1)",
data: [
{% for i in 1..11 %}
{{ (yearStat.month(i).totalDuration / 3600)|round }},
{% endfor %}
{{ (yearStat.month(12).totalDuration / 3600)|round }}
]
}
]
};
{% if tab == "charts" %}
$(function () {
var barChartCanvas{{ year }} = $("#barChart{{ year }}").get(0).getContext("2d");
var barChart = new Chart(barChartCanvas{{ year }});
barChart.Bar(barChartData{{ year }}, barChartOptions);
});
{% endif %}
$('a[href="#charts"]').on('shown.bs.tab', function(e){
var barChartCanvas{{ year }} = $("#barChart{{ year }}").get(0).getContext("2d");
var barChart = new Chart(barChartCanvas{{ year }});
barChart.Bar(barChartData{{ year }}, barChartOptions);
});
</script>
{% endfor %}
</div>
<div class="tab-pane {% if tab == "profile" %}active{% endif %}" id="profile">
{{ form_start(form) }}
{{ form_widget(form) }}
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
{{ form_end(form) }}
</div>
<div class="tab-pane {% if tab == "password" %}active{% endif %}" id="password">
{{ form_start(form_password) }}
{{ form_widget(form_password) }}
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
{{ form_end(form_password) }}
</div>
</div>
</div> </div>
<div class="col-md-6 col-sm-12 col-xs-12">
{{ widgets.profile_box(user, stats, 'aqua-active') }}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% macro profile_list(user, items, color) %} {% macro profile_infos(user, stats) %}
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{{ 'profile.about_me'|trans }}</h3>
</div>
<div class="box-body">
<ul class="nav nav-stacked">
{# colors = purple, blue, aqua, red, green #}
{# FIXME hourly rate must be dynamic <li><a href="#">{{ 'label.hourly_rate'|trans }} <span class="pull-right badge bg-blue">70</span></a></li> #}
<li><a href="#">{{ 'label.id'|trans }} <span class="pull-right badge bg-aqua">{{ user.id }}</span></a></li>
<li><a href="#">{{ 'label.username'|trans }} <span class="pull-right badge bg-blue">{{ user.username }}</span></a></li>
<li><a href="#">{{ 'label.language'|trans }} <span class="pull-right badge bg-green">{{ user.language }}</span></a></li>
<li><a href="#">{{ 'profile.first_entry'|trans }} <span class="pull-right badge bg-purple">{{ stats.firstEntry|date }}</span></a></li>
</ul>
</div>
</div>
{% endmacro %}
{% macro profile_box(user, stats) %}
{% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %}
<div class="box box-primary">
<div class="box-body box-profile">
{{ macro.avatar(user.avatar, user.username, 'profile-user-img img-responsive img-circle') }}
<h3 class="profile-username text-center">{{ user.alias }}</h3>
<p class="text-muted text-center">{{ user.title }}</p>
<ul class="list-group list-group-unbordered">
<li class="list-group-item">
<b>{{ 'stats.durationThisMonth'|trans }}</b> <a class="pull-right">{{ stats.durationThisMonth|duration }}</a>
</li>
<li class="list-group-item">
<b>{{ 'stats.amountThisMonth'|trans }}</b> <a class="pull-right">{{ stats.amountThisMonth|money }}</a>
</li>
<li class="list-group-item">
<b>{{ 'stats.durationTotal'|trans }}</b> <a class="pull-right">{{ stats.durationTotal|duration }}</a>
</li>
<li class="list-group-item">
<b>{{ 'stats.amountTotal'|trans }}</b> <a class="pull-right">{{ stats.amountTotal|money }}</a>
</li>
</ul>
{# <a href="#" class="btn btn-primary btn-block"><b>Follow</b></a> #}
</div>
<!-- /.box-body -->
</div>
{% endmacro %}
{# -------------------------------- UNUSED FOR NOW -------------------------------- #}
{% macro profile_list_unused(user, items, color) %}
{% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %} {% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %}
<div class="box box-widget widget-user-2"> <div class="box box-widget widget-user-2">
<!-- Add the bg color to the header using any of the bg-* classes --> <!-- Add the bg color to the header using any of the bg-* classes -->
@@ -37,11 +185,11 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% macro profile_box(user, stats, color) %} {# -------------------------------- UNUSED FOR NOW -------------------------------- #}
{% macro profile_box_unused(user, stats, color) %}
{% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %} {% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %}
<div class="box box-widget widget-user"> <div class="box box-widget widget-user">
<!-- Add the bg color to the header using any of the bg-* classes -->
<div class="widget-user-header bg-{{ color|default(kimai_context.box_color) }}"> <div class="widget-user-header bg-{{ color|default(kimai_context.box_color) }}">
<h3 class="widget-user-username">{{ user.alias }} ({{ user.username }})</h3> <h3 class="widget-user-username">{{ user.alias }} ({{ user.username }})</h3>
<h5 class="widget-user-desc">{{ user.title }}</h5> <h5 class="widget-user-desc">{{ user.title }}</h5>

View File

@@ -64,16 +64,16 @@ twig:
table_icons: false table_icons: false
# Assetic Configuration (used for managing web assets: CSS, JavaScript, Sass, etc.) # Assetic Configuration (used for managing web assets: CSS, JavaScript, Sass, etc.)
assetic: #assetic:
debug: "%kernel.debug%" # debug: "%kernel.debug%"
use_controller: false # use_controller: false
bundles: [ ] # bundles: [ ]
filters: # filters:
cssrewrite: ~ # cssrewrite: ~
jsqueeze: ~ # jsqueeze: ~
scssphp: # scssphp:
# the formatter must be the FQCN (don't use the 'compressed' value) # # the formatter must be the FQCN (don't use the 'compressed' value)
formatter: "Leafo\\ScssPhp\\Formatter\\Compressed" # formatter: "Leafo\\ScssPhp\\Formatter\\Compressed"
# Doctrine Configuration (used to access databases and manipulate their information) # Doctrine Configuration (used to access databases and manipulate their information)
doctrine: doctrine:
@@ -85,12 +85,12 @@ doctrine:
auto_mapping: true auto_mapping: true
# Swiftmailer Configuration (used to send emails) # Swiftmailer Configuration (used to send emails)
swiftmailer: #swiftmailer:
transport: "%mailer_transport%" # transport: "%mailer_transport%"
host: "%mailer_host%" # host: "%mailer_host%"
username: "%mailer_user%" # username: "%mailer_user%"
password: "%mailer_password%" # password: "%mailer_password%"
spool: { type: memory } # spool: { type: memory }
avanzu_admin_theme: avanzu_admin_theme:
use_twig : true use_twig : true

View File

@@ -29,8 +29,8 @@ monolog:
# type: chromephp # type: chromephp
# level: info # level: info
assetic: #assetic:
use_controller: true # use_controller: true
#swiftmailer: #swiftmailer:
# delivery_address: me@example.com # delivery_address: me@example.com

86
app/config/mysql.yml Normal file
View File

@@ -0,0 +1,86 @@
doctrine:
orm:
dql:
datetime_functions:
convert_tz: DoctrineExtensions\Query\Mysql\ConvertTz
date: DoctrineExtensions\Query\Mysql\Date
date_format: DoctrineExtensions\Query\Mysql\DateFormat
dateadd: DoctrineExtensions\Query\Mysql\DateAdd
datesub: DoctrineExtensions\Query\Mysql\DateSub
datediff: DoctrineExtensions\Query\Mysql\DateDiff
day: DoctrineExtensions\Query\Mysql\Day
dayname: DoctrineExtensions\Query\Mysql\DayName
dayofweek: DoctrineExtensions\Query\Mysql\DayOfWeek
dayofyear: DoctrineExtensions\Query\Mysql\DayOfYear
div: DoctrineExtensions\Query\Mysql\Div
from_unixtime: DoctrineExtensions\Query\Mysql\FromUnixtime
last_day: DoctrineExtensions\Query\Mysql\LastDay
minute: DoctrineExtensions\Query\Mysql\Minute
now: DoctrineExtensions\Query\Mysql\Now
month: DoctrineExtensions\Query\Mysql\Month
monthname: DoctrineExtensions\Query\Mysql\MonthName
second: DoctrineExtensions\Query\Mysql\Second
strtodate: DoctrineExtensions\Query\Mysql\StrToDate
time: DoctrineExtensions\Query\Mysql\Time
timediff: DoctrineExtensions\Query\Mysql\TimeDiff
timestampadd: DoctrineExtensions\Query\Mysql\TimestampAdd
timestampdiff: DoctrineExtensions\Query\Mysql\TimestampDiff
timetosec: DoctrineExtensions\Query\Mysql\TimeToSec
week: DoctrineExtensions\Query\Mysql\Week
weekday: DoctrineExtensions\Query\Mysql\WeekDay
year: DoctrineExtensions\Query\Mysql\Year
yearweek: DoctrineExtensions\Query\Mysql\YearWeek
unix_timestamp: DoctrineExtensions\Query\Mysql\UnixTimestamp
numeric_functions:
acos: DoctrineExtensions\Query\Mysql\Acos
asin: DoctrineExtensions\Query\Mysql\Asin
atan2: DoctrineExtensions\Query\Mysql\Atan2
atan: DoctrineExtensions\Query\Mysql\Atan
bit_count: DoctrineExtensions\Query\Mysql\BitCount
bit_xor: DoctrineExtensions\Query\Mysql\BitXor
ceil: DoctrineExtensions\Query\Mysql\Ceil
cos: DoctrineExtensions\Query\Mysql\Cos
cot: DoctrineExtensions\Query\Mysql\Cot
floor: DoctrineExtensions\Query\Mysql\Floor
hour: DoctrineExtensions\Query\Mysql\Hour
pi: DoctrineExtensions\Query\Mysql\Pi
power: DoctrineExtensions\Query\Mysql\Power
quarter: DoctrineExtensions\Query\Mysql\Quarter
rand: DoctrineExtensions\Query\Mysql\Rand
round: DoctrineExtensions\Query\Mysql\Round
stddev: DoctrineExtensions\Query\Mysql\StdDev
sin: DoctrineExtensions\Query\Mysql\Sin
std: DoctrineExtensions\Query\Mysql\Std
tan: DoctrineExtensions\Query\Mysql\Tan
string_functions:
ascii: DoctrineExtensions\Query\Mysql\Ascii
binary: DoctrineExtensions\Query\Mysql\Binary
char_length: DoctrineExtensions\Query\Mysql\CharLength
concat_ws: DoctrineExtensions\Query\Mysql\ConcatWs
countif: DoctrineExtensions\Query\Mysql\CountIf
crc32: DoctrineExtensions\Query\Mysql\Crc32
degrees: DoctrineExtensions\Query\Mysql\Degrees
field: DoctrineExtensions\Query\Mysql\Field
find_in_set: DoctrineExtensions\Query\Mysql\FindInSet
greatest: DoctrineExtensions\Query\Mysql\Greatest
group_concat: DoctrineExtensions\Query\Mysql\GroupConcat
ifelse: DoctrineExtensions\Query\Mysql\IfElse
ifnull: DoctrineExtensions\Query\Mysql\IfNull
least: DoctrineExtensions\Query\Mysql\Least
lpad: DoctrineExtensions\Query\Mysql\Lpad
match_against: DoctrineExtensions\Query\Mysql\MatchAgainst
md5: DoctrineExtensions\Query\Mysql\Md5
nullif: DoctrineExtensions\Query\Mysql\NullIf
radians: DoctrineExtensions\Query\Mysql\Radians
regexp: DoctrineExtensions\Query\Mysql\Regexp
replace: DoctrineExtensions\Query\Mysql\Replace
rpad: DoctrineExtensions\Query\Mysql\Rpad
sha1: DoctrineExtensions\Query\Mysql\Sha1
sha2: DoctrineExtensions\Query\Mysql\Sha2
soundex: DoctrineExtensions\Query\Mysql\Soundex
substring_index: DoctrineExtensions\Query\Mysql\SubstringIndex
uuid_short: DoctrineExtensions\Query\Mysql\UuidShort
hex: DoctrineExtensions\Query\Mysql\Hex
unhex: DoctrineExtensions\Query\Mysql\Unhex

View File

@@ -4,6 +4,9 @@
# See http://symfony.com/doc/current/best_practices/configuration.html#canonical-parameters # See http://symfony.com/doc/current/best_practices/configuration.html#canonical-parameters
parameters: parameters:
# used to load additional doctrine functions (possible values: mysql, sqlite)
database_engine: sqlite
# Define the database connection string # Define the database connection string
database_url: 'sqlite:///%kernel.root_dir%/data/timesheet.sqlite' database_url: 'sqlite:///%kernel.root_dir%/data/timesheet.sqlite'
# database_url: 'mysql://root:pass@127.0.0.1:3306/timesheet' # database_url: 'mysql://root:pass@127.0.0.1:3306/timesheet'

View File

@@ -34,6 +34,6 @@ homepage:
permanent: true permanent: true
avanzu_admin_profile: avanzu_admin_profile:
path: /{_locale}/profile/{ident}/ path: /{_locale}/profile/{username}
options: options:
avanzu_admin_route: profile avanzu_admin_route: profile

50
app/config/sqlite.yml Normal file
View File

@@ -0,0 +1,50 @@
doctrine:
orm:
dql:
datetime_functions:
date: DoctrineExtensions\Query\Sqlite\Date
date_format: DoctrineExtensions\Query\Sqlite\DateFormat
#dateadd: DoctrineExtensions\Query\Sqlite\DateAdd
#datediff: DoctrineExtensions\Query\Sqlite\DateDiff
day: DoctrineExtensions\Query\Sqlite\Day
dayname: DoctrineExtensions\Query\Sqlite\DayName
minute: DoctrineExtensions\Query\Sqlite\Minute
#month: DoctrineExtensions\Query\Sqlite\Month
#monthname: DoctrineExtensions\Query\Sqlite\MonthName
second: DoctrineExtensions\Query\Sqlite\Second
month: DoctrineExtensions\Query\Sqlite\Month
strftime: DoctrineExtensions\Query\Sqlite\StrfTime
#strtodate: DoctrineExtensions\Query\Sqlite\StrToDate
#time: DoctrineExtensions\Query\Sqlite\Time
#timestampadd: DoctrineExtensions\Query\Sqlite\TimestampAdd
#timestampdiff: DoctrineExtensions\Query\Sqlite\TimestampDiff
week: DoctrineExtensions\Query\Sqlite\Week
weekday: DoctrineExtensions\Query\Sqlite\WeekDay
year: DoctrineExtensions\Query\Sqlite\Year
string_functions:
# binary: DoctrineExtensions\Query\Sqlite\Binary
# char_length: DoctrineExtensions\Query\Sqlite\CharLength
concat_ws: DoctrineExtensions\Query\Sqlite\ConcatWs
# countif: DoctrineExtensions\Query\Sqlite\CountIf
# crc32: DoctrineExtensions\Query\Sqlite\Crc32
# degrees: DoctrineExtensions\Query\Sqlite\Degrees
# field: DoctrineExtensions\Query\Sqlite\Field
# find_in_set: DoctrineExtensions\Query\Sqlite\FindInSet
# group_concat: DoctrineExtensions\Query\Sqlite\GroupConcat
# ifelse: DoctrineExtensions\Query\Sqlite\IfElse
ifnull: DoctrineExtensions\Query\Sqlite\IfNull
# match_against: DoctrineExtensions\Query\Sqlite\MatchAgainst
# md5: DoctrineExtensions\Query\Sqlite\Md5
# nullif: DoctrineExtensions\Query\Sqlite\NullIf
# radians: DoctrineExtensions\Query\Sqlite\Radians
# regexp: DoctrineExtensions\Query\Sqlite\Regexp
replace: DoctrineExtensions\Query\Sqlite\Replace
# sha1: DoctrineExtensions\Query\Sqlite\Sha1
# sha2: DoctrineExtensions\Query\Sqlite\Sha2
# soundex: DoctrineExtensions\Query\Sqlite\Soundex
# uuid_short: DoctrineExtensions\Query\Sqlite\UuidShort
numeric_functions:
round: DoctrineExtensions\Query\Sqlite\Round

View File

@@ -31,7 +31,8 @@
"symfony/symfony": "^3.1", "symfony/symfony": "^3.1",
"twig/extensions": "^1.3", "twig/extensions": "^1.3",
"white-october/pagerfanta-bundle": "^1.0", "white-october/pagerfanta-bundle": "^1.0",
"avanzu/admin-theme-bundle": "dev-feature/2.0" "avanzu/admin-theme-bundle": "dev-feature/2.0",
"beberlei/DoctrineExtensions": "^1.0"
}, },
"require-dev": { "require-dev": {
"sensio/generator-bundle": "~3.0", "sensio/generator-bundle": "~3.0",

66
composer.lock generated
View File

@@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"hash": "4987a4da3f291b8e0bb1b3af06a5cfa6", "hash": "d5aa01779571420f9166199f60fa2a17",
"content-hash": "287d1663444888a10331136287c4269b", "content-hash": "20dda7356ab7c685ada168dd7e9b7593",
"packages": [ "packages": [
{ {
"name": "almasaeed2010/adminlte", "name": "almasaeed2010/adminlte",
@@ -93,6 +93,68 @@
"description": "Admin Theme based on the AdminLTE Template for easy integration into symfony", "description": "Admin Theme based on the AdminLTE Template for easy integration into symfony",
"time": "2016-08-26 08:28:19" "time": "2016-08-26 08:28:19"
}, },
{
"name": "beberlei/DoctrineExtensions",
"version": "v1.0.11",
"source": {
"type": "git",
"url": "https://github.com/beberlei/DoctrineExtensions.git",
"reference": "6dbe65e469e8bdcfe165ea16636b7049af479a24"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beberlei/DoctrineExtensions/zipball/6dbe65e469e8bdcfe165ea16636b7049af479a24",
"reference": "6dbe65e469e8bdcfe165ea16636b7049af479a24",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
},
"require-dev": {
"doctrine/orm": "~2.1",
"nesbot/carbon": "*",
"phpunit/phpunit": "~4.5",
"symfony/yaml": "~2.6",
"zf1/zend-date": "~1.12",
"zf1/zend-registry": "~1.12"
},
"suggest": {
"nesbot/carbon": "Alternative to DateTime",
"zf1/zend-date": "Alternative to DateTime"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"DoctrineExtensions\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Steve Lacey",
"email": "steve@stevelacey.net"
}
],
"description": "A set of extensions to Doctrine 2 that add support for additional query functions available in MySQL and Oracle.",
"keywords": [
"database",
"doctrine",
"orm"
],
"time": "2016-07-06 17:12:30"
},
{ {
"name": "doctrine/annotations", "name": "doctrine/annotations",
"version": "v1.2.7", "version": "v1.2.7",

View File

@@ -11,6 +11,10 @@
namespace AppBundle; namespace AppBundle;
use AppBundle\DependencyInjection\AppBundleExtension;
use AppBundle\DependencyInjection\CompilerPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
/** /**
@@ -18,7 +22,15 @@ use Symfony\Component\HttpKernel\Bundle\Bundle;
* *
* @see http://symfony.com/doc/current/cookbook/bundles/best_practices.html * @see http://symfony.com/doc/current/cookbook/bundles/best_practices.html
* @see http://symfony.com/doc/current/best_practices/business-logic.html * @see http://symfony.com/doc/current/best_practices/business-logic.html
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/ */
class AppBundle extends Bundle class AppBundle extends Bundle
{ {
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new CompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
}
} }

View File

@@ -11,12 +11,22 @@
namespace AppBundle\Controller; namespace AppBundle\Controller;
use AppBundle\Entity\User;
use AppBundle\Form\UserEditType;
use AppBundle\Form\UserPasswordType;
use AppBundle\Repository\UserRepository;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use TimesheetBundle\Entity\Timesheet; use TimesheetBundle\Entity\Timesheet;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use TimesheetBundle\Model\TimesheetStatistic;
use TimesheetBundle\Repository\TimesheetRepository;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
/** /**
* User profile controller * User profile controller
@@ -29,45 +39,189 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
class ProfileController extends Controller class ProfileController extends Controller
{ {
/** /**
* @Route("/", defaults={"ident": null}, name="user_profile") * @Route("/{username}", name="user_profile")
* @Route("/{ident}/", requirements={"ident": "[a-zA-Z0-9\-].*"}, name="user_profile_ident")
* @Method("GET") * @Method("GET")
*/ */
public function indexAction($ident) public function indexAction($username)
{ {
$user = $this->getUser(); $user = $this->getUserByUsername($username);
$isAdmin = false; return $this->getProfileView($user);
if ($isAdmin) {
} else {
} }
/**
* @param User $user
* @param Form $editForm
* @param Form $pwdForm
* @param string $tab
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function getProfileView(User $user, Form $editForm = null, Form $pwdForm = null, $tab = 'charts')
{
/* @var $timesheetRepo TimesheetRepository */
$timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class); $timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class);
$userStats = $timesheetRepo->getUserStatistics($user); $userStats = $timesheetRepo->getUserStatistics($user);
$monthlyStats = $timesheetRepo->getMonthlyStats($user);
// FIXME fetch values dynamically and add trans filter to macros $editForm = $editForm !== null ? $editForm : $this->createEditForm($user);
$items = [ $pwdForm = $pwdForm !== null ? $pwdForm : $this->createPasswordForm($user);
[
'title' => 'Stundenlohn',
'url' => '#',
'color' => 'blue', // aqua, red, green
'value' => 70
],
[
'title' => 'Sprache',
'url' => '#',
'color' => 'green', // aqua, red, green
'value' => 'deutsch'
],
];
return $this->render( return $this->render(
'user/profile.html.twig', 'user/profile.html.twig',
[ [
'user' => $this->getUser(), 'tab' => $tab,
'settings' => $items, 'user' => $user,
'stats' => $userStats 'stats' => $userStats,
'years' => $monthlyStats,
'form' => $editForm->createView(),
'form_password' => $pwdForm->createView(),
] ]
); );
} }
/**
* @Route("/{username}/edit", name="user_profile_edit")
* @Method({"GET", "POST"})
*/
public function editAction($username, Request $request)
{
$user = $this->getUserByUsername($username);
$editForm = $this->createEditForm($user);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash('success', 'action.updated_successfully');
return $this->redirectToRoute(
'user_profile', ['username' => $user->getUsername()]
);
}
return $this->getProfileView($user, $editForm, null, 'profile');
}
/**
* @Route("/{username}/password", name="user_profile_password")
* @Method({"GET", "POST"})
*/
public function passwordAction($username, Request $request)
{
$user = $this->getUserByUsername($username);
$pwdForm = $this->createPasswordForm($user);
$pwdForm->handleRequest($request);
if ($pwdForm->isSubmitted() && $pwdForm->isValid()) {
$password = $this->get('security.password_encoder')
->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash('success', 'action.updated_successfully');
return $this->redirectToRoute(
'user_profile', ['username' => $user->getUsername()]
);
}
return $this->getProfileView($user, null, $pwdForm, 'password');
}
/**
* FIXME
* @Route("/{username}/delete", name="user_profile_delete")
* @Method({"GET", "POST"})
*/
public function deleteAction($username, Request $request)
{
$user = $this->getUserByUsername($username);
$deleteForm = $this->createDeleteForm($user);
throw new \Exception('Delete not implemented yet');
}
/**
* @param $username
* @return User
* @throws NotFoundHttpException
*/
protected function getUserByUsername($username)
{
$user = $this->getUser();
// access to own profile always allowed
if (null === $username) {
$username = $user->getUsername();
}
if ($username !== $user->getUsername()) {
$this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page'); // TODO translation
}
if ($username !== $user->getUsername()) {
/* @var $userRepo UserRepository */
$userRepo = $this->getDoctrine()->getRepository(User::class);
$user = $userRepo->findByUsername($username);
if (null === $user) {
throw new NotFoundHttpException('User "'.$username.'" does not exist');
}
}
return $user;
}
/**
* @param User $user
* @return \Symfony\Component\Form\Form
*/
private function createEditForm(User $user)
{
return $this->createForm(
UserEditType::class,
$user,
[
'action' => $this->generateUrl('user_profile_edit', ['username' => $user->getUsername()]),
'method' => 'POST'
]
);
}
/**
* @param User $user
* @return \Symfony\Component\Form\Form
*/
private function createPasswordForm(User $user)
{
return $this->createForm(
UserPasswordType::class,
$user,
[
'validation_groups' => array('passwordUpdate'),
'action' => $this->generateUrl('user_profile_password', ['username' => $user->getUsername()]),
'method' => 'POST'
]
);
}
/**
* @param User $user
*
* @return \Symfony\Component\Form\Form The form
*/
private function createDeleteForm(User $user)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('user_profile_delete', ['username' => $user->getUsername()]))
->setMethod('DELETE')
->getForm()
;
}
} }

View File

@@ -83,47 +83,6 @@ class LoadFixtures implements FixtureInterface, ContainerAwareInterface
$manager->flush(); $manager->flush();
} }
/**
* FIXME CAN BE REMOVED
*
* @param ObjectManager $manager
*/
private function loadPosts(ObjectManager $manager)
{
$authors = [
'anna_admin@example.com',
'tony_teamlead@example.com',
'clara_customer@example.com',
'john_user@example.com'
];
foreach (range(1, 30) as $i) {
$post = new Post();
$post->setTitle($this->getRandomPostTitle());
$post->setSummary($this->getRandomPostSummary());
$post->setSlug($this->container->get('slugger')->slugify($post->getTitle()));
$post->setContent($this->getPostContent());
$post->setAuthorEmail($authors[array_rand($authors, 1)]);
$post->setPublishedAt(new \DateTime('now - '.$i.'days'));
foreach (range(1, 5) as $j) {
$comment = new Comment();
$comment->setAuthorEmail($authors[array_rand($authors, 1)]);
$comment->setPublishedAt(new \DateTime('now + '.($i + $j).'seconds'));
$comment->setContent($this->getRandomCommentContent());
$comment->setPost($post);
$manager->persist($comment);
$post->addComment($comment);
}
$manager->persist($post);
}
$manager->flush();
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Kimai package.
*
* (c) Kevin Papst <kevin@kevinpapst.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AppBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use \Doctrine\Common\ClassLoader;
/**
* Main extension class
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class AppExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$rootDir = realpath($container->getParameter('kernel.root_dir'));
$extensionsDir = realpath($rootDir . '/../vendor/beberlei/DoctrineExtensions/src/');
$classLoader = new ClassLoader('DoctrineExtensions', $extensionsDir);
$classLoader->register();
}
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Kimai package.
*
* (c) Kevin Papst <kevin@kevinpapst.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AppBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\Yaml\Yaml;
/**
* Class Configuration
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class CompilerPass implements CompilerPassInterface
{
/**
* @param ContainerBuilder $container
*/
public function process(ContainerBuilder $container)
{
$engine = $container->getParameter('database_engine');
if (null === $engine) {
throw new ParameterNotFoundException('database_engine');
}
$ormConfigDef = $container->getDefinition('doctrine.orm.default_configuration');
$configDir = realpath($container->getParameter('kernel.root_dir') . '/config/');
$config = Yaml::parse(file_get_contents($configDir . '/' . $engine . '.yml'));
if (!isset($config['doctrine']['orm']['dql'])) {
return;
}
$sql = $config['doctrine']['orm']['dql'];
if (!empty($sql)) {
foreach ($sql['string_functions'] as $name => $function) {
$ormConfigDef->addMethodCall('addCustomStringFunction', array($name, $function));
}
foreach ($sql['numeric_functions'] as $name => $function) {
$ormConfigDef->addMethodCall('addCustomNumericFunction', array($name, $function));
}
foreach ($sql['datetime_functions'] as $name => $function) {
$ormConfigDef->addMethodCall('addCustomDatetimeFunction', array($name, $function));
}
}
}
}

View File

@@ -4,7 +4,7 @@ namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/** /**
* User * User
@@ -28,6 +28,8 @@ class User implements UserInterface
* @var string * @var string
* *
* @ORM\Column(name="name", type="string", length=160, nullable=false, unique=true) * @ORM\Column(name="name", type="string", length=160, nullable=false, unique=true)
* @Assert\NotBlank()
* @Assert\Length(min=6, max=160)
*/ */
private $username; private $username;
@@ -35,6 +37,8 @@ class User implements UserInterface
* @var string * @var string
* *
* @ORM\Column(name="mail", type="string", length=160, nullable=false, unique=true) * @ORM\Column(name="mail", type="string", length=160, nullable=false, unique=true)
* @Assert\NotBlank()
* @Assert\Email()
*/ */
private $email; private $email;
@@ -45,6 +49,14 @@ class User implements UserInterface
*/ */
private $password; private $password;
/**
* @var string
*
* @Assert\NotBlank(groups={"registration", "passwordUpdate"})
* @Assert\Length(min=6, max=4096, groups={"registration", "passwordUpdate"})
*/
private $plainPassword;
/** /**
* @var string * @var string
* *
@@ -52,6 +64,13 @@ class User implements UserInterface
*/ */
private $alias; private $alias;
/**
* @var string
*
* @ORM\Column(name="language", type="string", length=5, nullable=true)
*/
private $language = 'de';
/** /**
* @var boolean * @var boolean
* *
@@ -62,16 +81,9 @@ class User implements UserInterface
/** /**
* @var \DateTime * @var \DateTime
* *
* @ORM\Column(name="start_timeframe", type="datetime", nullable=true) * @ORM\Column(name="registration_date", type="datetime", nullable=true)
*/ */
private $timeframeBegin; private $registeredAt;
/**
* @var \DateTime
*
* @ORM\Column(name="end_timeframe", type="datetime", nullable=true)
*/
private $timeframeEnd;
/** /**
* @var string * @var string
@@ -94,6 +106,14 @@ class User implements UserInterface
*/ */
private $roles = []; private $roles = [];
/**
* User constructor.
*/
public function __construct()
{
$this->registeredAt = new \DateTime();
}
/** /**
* @return int * @return int
*/ */
@@ -103,35 +123,22 @@ class User implements UserInterface
} }
/** /**
* @return string * @return \DateTime
*/ */
public function getTimeframeBegin() public function getRegisteredAt()
{ {
return $this->timeframeBegin; return $this->registeredAt;
} }
/** /**
* @param string $timeframeBegin * @param $registeredAt
* @return $this
*/ */
public function setTimeframeBegin($timeframeBegin) public function setRegisteredAt($registeredAt)
{ {
$this->timeframeBegin = $timeframeBegin; $this->registeredAt = $registeredAt;
}
/** return $this;
* @return string
*/
public function getTimeframeEnd()
{
return $this->timeframeEnd;
}
/**
* @param string $timeframeEnd
*/
public function setTimeframeEnd($timeframeEnd)
{
$this->timeframeEnd = $timeframeEnd;
} }
/** /**
@@ -193,6 +200,28 @@ class User implements UserInterface
return $this->password; return $this->password;
} }
/**
* Only for form editing, you don't need this method!
*
* @return string
*/
public function getPlainPassword()
{
return $this->plainPassword;
}
/**
* Only for form editing, you don't need this method!
*
* @param string $password
* @return $this
*/
public function setPlainPassword($password)
{
$this->plainPassword = $password;
return $this;
}
/** /**
* @return string * @return string
*/ */
@@ -266,6 +295,22 @@ class User implements UserInterface
return $this; return $this;
} }
/**
* @return string
*/
public function getLanguage()
{
return $this->language;
}
/**
* @param string $language
*/
public function setLanguage($language)
{
$this->language = $language;
}
/** /**
* Returns the roles or permissions granted to the user for security. * Returns the roles or permissions granted to the user for security.
* *

View File

@@ -29,11 +29,18 @@ class NavbarShowUserListener
*/ */
protected $storage; protected $storage;
/**
* NavbarShowUserListener constructor.
* @param TokenStorageInterface $tokenStorage
*/
public function __construct(TokenStorageInterface $tokenStorage) public function __construct(TokenStorageInterface $tokenStorage)
{ {
$this->storage = $tokenStorage; $this->storage = $tokenStorage;
} }
/**
* @param ShowUserEvent $event
*/
public function onShowUser(ShowUserEvent $event) public function onShowUser(ShowUserEvent $event)
{ {
/* @var $myUser User */ /* @var $myUser User */
@@ -51,8 +58,7 @@ class NavbarShowUserListener
->setIsOnline(true) ->setIsOnline(true)
->setTitle($myUser->getTitle()) ->setTitle($myUser->getTitle())
->setAvatar($myUser->getAvatar()) ->setAvatar($myUser->getAvatar())
->setMemberSince(new \DateTime()) // FIXME add column to entity ->setMemberSince(new \DateTime());
;
$event->setUser($user); $event->setUser($user);
} }

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Kimai package.
*
* (c) Kevin Papst <kevin@kevinpapst.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AppBundle\Form;
use AppBundle\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\LanguageType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to create and manipulate Users.
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class UserEditType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// string - length 160
->add('alias', null, [
'label' => 'label.alias',
])
// string - length 50
->add('title', null, [
//'attr' => ['autofocus' => true],
'label' => 'label.title',
])
// string - length 160
->add('email', null, [
'label' => 'label.email',
])
// string - length 5
->add('language', LanguageType::class, [
'label' => 'label.language',
'choices' => array('Deutsch' => 'de') // FIXME translation
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'edit_user_profile',
]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Kimai package.
*
* (c) Kevin Papst <kevin@kevinpapst.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AppBundle\Form;
use AppBundle\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to create and manipulate Users.
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class UserPasswordType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('plainPassword', RepeatedType::class, [
'type' => PasswordType::class,
'first_options' => array('label' => 'security.label.password'),
'second_options' => array('label' => 'security.label.password_repeat'),
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'edit_user_password',
]);
}
}

View File

@@ -57,7 +57,7 @@ class UserRepository extends EntityRepository
public function findByUsername($username) public function findByUsername($username)
{ {
return $this->findOneBy(['']); return $this->findOneBy(['username' => $username]);
} }
/** /**

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Kimai package.
*
* (c) Kevin Papst <kevin@kevinpapst.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace TimesheetBundle\Model\Statistic;
/**
* Yearly statistics
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class Month
{
/**
* @var string
*/
protected $month;
/**
* @var int
*/
protected $totalDuration = 0;
/**
* @var int
*/
protected $totalRate = 0;
/**
* Month constructor.
* @param string $month
*/
public function __construct($month)
{
$this->month = $month;
}
/**
* @return string
*/
public function getMonth()
{
return $this->month;
}
/**
* @return int
*/
public function getTotalDuration()
{
return $this->totalDuration;
}
/**
* @param $totalDuration
* @return $this
*/
public function setTotalDuration($totalDuration)
{
$this->totalDuration = $totalDuration;
return $this;
}
/**
* @return int
*/
public function getTotalRate()
{
return $this->totalRate;
}
/**
* @param $totalRate
* @return $this
*/
public function setTotalRate($totalRate)
{
$this->totalRate = $totalRate;
return $this;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Kimai package.
*
* (c) Kevin Papst <kevin@kevinpapst.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace TimesheetBundle\Model\Statistic;
/**
* Yearly statistics
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class Year
{
/**
* @var string
*/
protected $year;
/**
* @var Month[]
*/
protected $months;
/**
* Year constructor.
* @param string $year
*/
public function __construct($year)
{
$this->year = $year;
}
/**
* @return string
*/
public function getYear()
{
return $this->year;
}
/**
* @param Month $month
* @return $this
*/
public function setMonth(Month $month)
{
$this->months[(int)$month->getMonth()] = $month;
return $this;
}
/**
* @param $month
* @return null|Month
*/
public function getMonth($month)
{
if (isset($this->months[$month])) {
return $this->months[$month];
}
return null;
}
/**
* @return Month[]
*/
public function getMonths()
{
return array_values($this->months);
}
}

View File

@@ -11,6 +11,8 @@
namespace TimesheetBundle\Model; namespace TimesheetBundle\Model;
use \DateTime;
/** /**
* Timesheet statistics * Timesheet statistics
* *
@@ -34,6 +36,10 @@ class TimesheetStatistic
* @var int * @var int
*/ */
protected $amountTotal = 0; protected $amountTotal = 0;
/**
* @var \DateTime
*/
protected $firstEntry;
/** /**
* @return int * @return int
@@ -98,4 +104,20 @@ class TimesheetStatistic
{ {
$this->amountThisMonth = $amountThisMonth; $this->amountThisMonth = $amountThisMonth;
} }
/**
* @return DateTime
*/
public function getFirstEntry()
{
return $this->firstEntry;
}
/**
* @param DateTime $firstEntry
*/
public function setFirstEntry(DateTime $firstEntry)
{
$this->firstEntry = $firstEntry;
}
} }

View File

@@ -18,6 +18,8 @@ use Doctrine\ORM\Query;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Type;
use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Adapter\DoctrineORMAdapter;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
use TimesheetBundle\Model\Statistic\Month;
use TimesheetBundle\Model\Statistic\Year;
use TimesheetBundle\Model\TimesheetGlobalStatistic; use TimesheetBundle\Model\TimesheetGlobalStatistic;
use TimesheetBundle\Model\TimesheetStatistic; use TimesheetBundle\Model\TimesheetStatistic;
use DateTime; use DateTime;
@@ -30,13 +32,29 @@ use DateTime;
class TimesheetRepository extends EntityRepository class TimesheetRepository extends EntityRepository
{ {
/**
* @param $select
* @param User|null $user
* @return \Doctrine\ORM\QueryBuilder
*/
protected function queryThisMonth($select, User $user = null) protected function queryThisMonth($select, User $user = null)
{ {
$qb = $this->getEntityManager()->createQueryBuilder();
// FIXME
$end = new DateTime(); $end = new DateTime();
$begin = $end->sub(new \DateInterval("P30D")); $begin = $end->sub(new \DateInterval("P30D")); // FIXME last month is not last 30 days
return $this->queryTimeRange($select, $begin, $end, $user);
}
/**
* @param $select
* @param DateTime $begin
* @param DateTime $end
* @param User|null $user
* @return \Doctrine\ORM\QueryBuilder
*/
protected function queryTimeRange($select, DateTime $begin, DateTime $end, User $user = null)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select($select) $qb->select($select)
->from('TimesheetBundle:Timesheet', 't') ->from('TimesheetBundle:Timesheet', 't')
@@ -52,6 +70,7 @@ class TimesheetRepository extends EntityRepository
return $qb; return $qb;
} }
/** /**
* Fetch statistic data for one user. * Fetch statistic data for one user.
* *
@@ -74,16 +93,67 @@ class TimesheetRepository extends EntityRepository
$durationMonth = $this->queryThisMonth('SUM(t.duration)', $user) $durationMonth = $this->queryThisMonth('SUM(t.duration)', $user)
->getQuery() ->getQuery()
->getSingleScalarResult(); ->getSingleScalarResult();
$firstEntry = $this->getEntityManager()
->createQuery('SELECT MIN(t.begin) FROM TimesheetBundle:Timesheet t WHERE t.user = :user')
->setParameter('user', $user)
->getSingleScalarResult();
$stats = new TimesheetStatistic(); $stats = new TimesheetStatistic();
$stats->setAmountTotal($rateTotal); $stats->setAmountTotal($rateTotal);
$stats->setDurationTotal($durationTotal); $stats->setDurationTotal($durationTotal);
$stats->setAmountThisMonth($amountMonth); $stats->setAmountThisMonth($amountMonth);
$stats->setDurationThisMonth($durationMonth); $stats->setDurationThisMonth($durationMonth);
$stats->setFirstEntry(new DateTime($firstEntry));
return $stats; return $stats;
} }
/**
* Returns an array of Year statistics.
*
* @param User|null $user
* @return Year[]
*/
public function getMonthlyStats(User $user = null)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('SUM(t.rate) as totalRate, SUM(t.duration) as totalDuration, MONTH(t.begin) as month, YEAR(t.begin) as year')
->from('TimesheetBundle:Timesheet', 't')
->where($qb->expr()->gt('t.begin', '0'))
->andWhere($qb->expr()->isNotNull('t.end'))
->orderBy('year', 'DESC')
->addOrderBy('month', 'ASC')
->groupBy('year')
->addGroupBy('month');
if (null !== $user) {
$qb->where('t.user = :user')
->setParameter('user', $user);
}
$years = [];
foreach($qb->getQuery()->execute() as $statRow) {
$curYear = $statRow['year'];
if (!isset($years[$curYear])) {
$year = new Year($curYear);
for ($i = 1; $i < 13; $i++) {
$month = $i < 10 ? '0' . $i : (string)$i;
$year->setMonth(new Month($month));
}
$years[$curYear] = $year;
}
$month = new Month($statRow['month']);
$month->setTotalDuration($statRow['totalDuration'])
->setTotalRate($statRow['totalRate']);
$years[$curYear]->setMonth($month);
}
return $years;
}
/** /**
* Fetch statistic data for all user. * Fetch statistic data for all user.
* *
@@ -133,8 +203,8 @@ class TimesheetRepository extends EntityRepository
$qb->select('t') $qb->select('t')
->from('TimesheetBundle:Timesheet', 't') ->from('TimesheetBundle:Timesheet', 't')
->where('t.begin > 0') ->where($qb->expr()->gt('t.begin', '0'))
->andWhere('t.end is NULL'); ->andWhere($qb->expr()->isNull('t.end'));
$params = []; $params = [];

View File

@@ -13,6 +13,7 @@
'label.project': 'book', 'label.project': 'book',
'label.comment': 'comment-o', 'label.comment': 'comment-o',
'label.visible': 'eye', 'label.visible': 'eye',
'label.actions': 'pencil-square-o',
}) }} }) }}
{% for entry in entries %} {% for entry in entries %}
@@ -22,6 +23,9 @@
<td>{{ entry.project }}</td> <td>{{ entry.project }}</td>
<td>{{ entry.comment }}</td> <td>{{ entry.comment }}</td>
<td>{{ widgets.label_visible(entry.visible) }}</td> <td>{{ widgets.label_visible(entry.visible) }}</td>
<td>
{{ widgets.button_group({'edit': '#', 'trash': '#'}) }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -14,6 +14,7 @@
'label.activity': 'tasks', 'label.activity': 'tasks',
'label.budget': 'credit-card', 'label.budget': 'credit-card',
'label.visible': 'eye', 'label.visible': 'eye',
'label.actions': 'pencil-square-o',
}) }} }) }}
{% for entry in entries %} {% for entry in entries %}
@@ -24,6 +25,9 @@
<td>{{ widgets.badge_counter(entry.activities.count) }}</td> <td>{{ widgets.badge_counter(entry.activities.count) }}</td>
<td>{{ entry.budget|money}}</td> <td>{{ entry.budget|money}}</td>
<td>{{ widgets.label_visible(entry.visible) }}</td> <td>{{ widgets.label_visible(entry.visible) }}</td>
<td>
{{ widgets.button_group({'edit': '#', 'trash': '#'}) }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -1,32 +1,45 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% import "macros/datatables.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as datatables %}
{% block page_title %}{{ 'admin_timesheet.title'|trans }}{% endblock %} {% block page_title %}{{ 'admin_timesheet.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'admin_timesheet.subtitle'|trans }}{% endblock %} {% block page_subtitle %}{{ 'admin_timesheet.subtitle'|trans }}{% endblock %}
{% block main %} {% block main %}
{% if entries.count > 0 %} {% if entries.count > 0 %}
{{ widgets.data_table_header({ {{ datatables.data_table_header({
'label.date': 'calendar', 'label.date': 'calendar',
'label.starttime': 'hourglass-start', 'label.starttime': 'hourglass-start',
'label.endtime': 'hourglass-end', 'label.endtime': 'hourglass-end',
'label.duration': 'clock-o', 'label.duration': 'clock-o',
'label.rate': 'money',
'label.username': 'user', 'label.username': 'user',
'label.description': 'pencil-square-o', 'label.description': 'comment-o',
'label.actions': 'pencil-square-o',
}) }} }) }}
{% for entry in entries %} {% for entry in entries %}
<tr> <tr>
<td>{{ entry.begin|date(kimai_context.date_1) }}</td> <td>{{ entry.begin|date(kimai_context.date_1) }}</td>
<td>{{ entry.begin|date("H:i") }}</td> <td>{{ entry.begin|date("H:i") }}</td>
{% if entry.end %}
<td>{{ entry.end|date("H:i") }}</td> <td>{{ entry.end|date("H:i") }}</td>
<td>{{ entry.duration|duration }}</td> <td>{{ entry.duration|duration }}</td>
<td>{{ entry.user.username }}</td> <td>{{ entry.rate|money }}</td>
{% else %}
<td>&dash;</td>
<td>&dash;</td>
<td>&dash;</td>
{% endif %}
<td><a href="{{ path('profile'|route_alias, {'username' : entry.user.username}) }}">{{ entry.user.username }}</a></td>
<td>{{ entry.description }}</td> <td>{{ entry.description }}</td>
<td>
{{ widgets.button_group({'edit': '#', 'trash': '#'}) }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
{{ widgets.data_table_footer(entries, 'admin_timesheet_paginated') }} {{ datatables.data_table_footer(entries, 'admin_timesheet_paginated') }}
{% else %} {% else %}
{{ widgets.callout('warning', 'error.no_entries_found') }} {{ widgets.callout('warning', 'error.no_entries_found') }}
{% endif %} {% endif %}

View File

@@ -1,30 +1,43 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% import "macros/datatables.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as datatables %}
{% block page_title %}{{ 'timesheet.title'|trans }}{% endblock %} {% block page_title %}{{ 'timesheet.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'timesheet.subtitle'|trans }}{% endblock %} {% block page_subtitle %}{{ 'timesheet.subtitle'|trans }}{% endblock %}
{% block main %} {% block main %}
{% if entries.count > 0 %} {% if entries.count > 0 %}
{{ widgets.data_table_header({ {{ datatables.data_table_header({
'label.date': 'calendar', 'label.date': 'calendar',
'label.starttime': 'hourglass-start', 'label.starttime': 'hourglass-start',
'label.endtime': 'hourglass-end', 'label.endtime': 'hourglass-end',
'label.duration': 'clock-o', 'label.duration': 'clock-o',
'label.description': 'pencil-square-o', 'label.rate': 'money',
'label.description': 'comment-o',
'label.actions': 'pencil-square-o',
}) }} }) }}
{% for entry in entries %} {% for entry in entries %}
<tr> <tr>
<td>{{ entry.begin|date(kimai_context.date_1) }}</td> <td>{{ entry.begin|date(kimai_context.date_1) }}</td>
<td>{{ entry.begin|date("H:i") }}</td> <td>{{ entry.begin|date("H:i") }}</td>
{% if entry.end %}
<td>{{ entry.end|date("H:i") }}</td> <td>{{ entry.end|date("H:i") }}</td>
<td>{{ entry.duration|duration }}</td> <td>{{ entry.duration|duration }}</td>
<td>{{ entry.rate|money }}</td>
{% else %}
<td>&dash;</td>
<td>&dash;</td>
<td>&dash;</td>
{% endif %}
<td>{{ entry.description }}</td> <td>{{ entry.description }}</td>
<td>
{{ widgets.button_group({'repeat': '#', 'edit': '#', 'trash': '#'}) }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
{{ widgets.data_table_footer(entries, 'timesheet_paginated') }} {{ datatables.data_table_footer(entries, 'timesheet_paginated') }}
{% else %} {% else %}
{{ widgets.callout('warning', 'error.no_entries_found') }} {{ widgets.callout('warning', 'error.no_entries_found') }}
{% endif %} {% endif %}

View File

@@ -12,7 +12,6 @@
namespace TimesheetBundle; namespace TimesheetBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
use TimesheetBundle\EventListener\Menu as MenuListener;
/** /**
* This class defines the Bundle for all Timesheet related topics * This class defines the Bundle for all Timesheet related topics
@@ -27,8 +26,5 @@ class TimesheetBundle extends Bundle
*/ */
public function boot() public function boot()
{ {
$listener = new MenuListener();
/* @var $dispatcher \Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher */
$dispatcher = $this->container->get('event_dispatcher');
} }
} }

View File

@@ -2,6 +2,10 @@
width: 30px; width: 30px;
} }
.user-panel>.info {
left: 40px;
padding-top: 0px;
}
/* /*
#ticktac img { #ticktac img {
width: 100%; width: 100%;

11
web/js/Chart.min.js vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,6 @@ $(document).ready(function() {
}, },
hook: function(name) { hook: function(name) {
alert('Hook: ' + name); alert('Hook: ' + name);
console.log($.fn.kimai.settings);
}, },
subscribe: function() { subscribe: function() {
pubSub.on.apply(pubSub, arguments); pubSub.on.apply(pubSub, arguments);
@@ -31,9 +30,6 @@ $(document).ready(function() {
}; };
$.fn.kimai = function(options) { $.fn.kimai = function(options) {
console.log('#');
console.log(options);
console.log('#');
if (methods[options]) { if (methods[options]) {
return methods[options].apply( this, Array.prototype.slice.call(arguments, 1)); return methods[options].apply( this, Array.prototype.slice.call(arguments, 1));
} else if (typeof options === 'object') { } else if (typeof options === 'object') {