added customer administration

This commit is contained in:
Kevin Papst
2018-01-02 14:20:59 +01:00
parent 719920c7f4
commit efa3646cbe
11 changed files with 608 additions and 98 deletions

View File

@@ -0,0 +1,111 @@
<?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\Controller\Admin;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use TimesheetBundle\Entity\Customer;
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;
use TimesheetBundle\Form\CustomerEditForm;
use TimesheetBundle\Repository\CustomerRepository;
/**
* Controller used to manage activities in the admin part of the site.
*
* @Route("/admin/customer")
* @Security("has_role('ROLE_ADMIN')")
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class CustomerController extends Controller
{
/**
* @Route("/", defaults={"page": 1}, name="admin_customer")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated")
* @Method("GET")
* @Cache(smaxage="10")
*/
public function indexAction($page)
{
/* @var $entries Pagerfanta */
$entries = $this->getDoctrine()->getRepository(Customer::class)->findAll($page);
return $this->render('TimesheetBundle:admin:customer.html.twig', ['entries' => $entries]);
}
/**
* @Route("/{id}/edit", name="admin_customer_edit")
* @Method({"GET", "POST"})
*/
public function editAction($id, Request $request)
{
$entity = $this->getById($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($entity);
$entityManager->flush();
$this->addFlash('success', 'action.updated_successfully');
return $this->redirectToRoute(
'admin_customer', ['id' => $entity->getId()]
);
}
return $this->render(
'TimesheetBundle:admin:customer_edit.html.twig',
[
'customer' => $entity,
'form' => $editForm->createView()
]
);
}
/**
* @param $id
* @return null|Customer
*/
protected function getById($id)
{
/* @var $repo CustomerRepository */
$repo = $this->getDoctrine()->getRepository(Customer::class);
$activity = $repo->getById($id);
if (null === $activity) {
throw new NotFoundHttpException('Customer "'.$id.'" does not exist');
}
return $activity;
}
/**
* @param Customer $customer
* @return \Symfony\Component\Form\Form
*/
private function createEditForm(Customer $customer)
{
return $this->createForm(
CustomerEditForm::class,
$customer,
[
'action' => $this->generateUrl('admin_customer_edit', ['id' => $customer->getId()]),
'method' => 'POST'
]
);
}
}

View File

@@ -12,12 +12,13 @@
namespace TimesheetBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Customer
*
* @ORM\Table(name="customers")
* @ORM\Entity
* @ORM\Entity(repositoryClass="TimesheetBundle\Repository\CustomerRepository")
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
@@ -69,9 +70,11 @@ class Customer
private $company;
/**
* @var string
* @var integer
*
* @ORM\Column(name="vat", type="integer", length=2, nullable=true)
* @Assert\Range(min = 0, max = 99)
*
* @ORM\Column(name="vat", type="string", length=255, nullable=true)
*/
private $vat;
@@ -85,28 +88,15 @@ class Customer
/**
* @var string
*
* @ORM\Column(name="street", type="string", length=255, nullable=true)
* @ORM\Column(name="street", type="text", length=65535, nullable=true)
*/
private $street;
/**
* @var string
*
* @ORM\Column(name="zipcode", type="string", length=255, nullable=true)
*/
private $zipcode;
/**
* @var string
*
* @ORM\Column(name="city", type="string", length=255, nullable=true)
*/
private $city;
private $address;
/**
* @var string
*
* @ORM\Column(name="country", type="string", length=2, nullable=true)
* @Assert\NotBlank()
*/
private $country;
@@ -149,6 +139,7 @@ class Customer
* @var string
*
* @ORM\Column(name="timezone", type="string", length=255, nullable=false)
* @Assert\NotBlank()
*/
private $timezone;
@@ -259,7 +250,7 @@ class Customer
/**
* Set vat
*
* @param string $vat
* @param integer $vat
*
* @return Customer
*/
@@ -273,7 +264,7 @@ class Customer
/**
* Get vat
*
* @return string
* @return integer
*/
public function getVat()
{
@@ -305,77 +296,25 @@ class Customer
}
/**
* Set street
*
* @param string $street
* @return string
*/
public function getAddress()
{
return $this->address;
}
/**
* @param string $address
*
* @return Customer
*/
public function setStreet($street)
public function setAddress($address)
{
$this->street = $street;
$this->address = $address;
return $this;
}
/**
* Get street
*
* @return string
*/
public function getStreet()
{
return $this->street;
}
/**
* Set zipcode
*
* @param string $zipcode
*
* @return Customer
*/
public function setZipcode($zipcode)
{
$this->zipcode = $zipcode;
return $this;
}
/**
* Get zipcode
*
* @return string
*/
public function getZipcode()
{
return $this->zipcode;
}
/**
* Set city
*
* @param string $city
*
* @return Customer
*/
public function setCity($city)
{
$this->city = $city;
return $this;
}
/**
* Get city
*
* @return string
*/
public function getCity()
{
return $this->city;
}
/**
* Set country
*
@@ -543,4 +482,31 @@ class Customer
{
return $this->timezone;
}
/**
* @param Project[] $projects
* @return $this
*/
public function setProjects($projects)
{
$this->projects = $projects;
return $this;
}
/**
* @return Project[]
*/
public function getProjects()
{
return $this->projects;
}
/**
* @return string
*/
public function __toString()
{
return $this->getName();
}
}

View File

@@ -61,6 +61,8 @@ class Menu
$menu->addChild(
new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], 'fa fa-clock-o')
)->addChild(
new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fa fa-users')
)->addChild(
new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], 'fa fa-book')
)->addChild(

View File

@@ -0,0 +1,134 @@
<?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\Form;
use AppBundle\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CountryType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PercentType;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use TimesheetBundle\Entity\Customer;
/**
* Defines the form used to edit Customer entities.
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class CustomerEditForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// string - length 255
->add('name', TextType::class, [
'label' => 'label.name',
])
// text
->add('comment', TextareaType::class, [
'label' => 'label.comment',
'required' => false,
])
// do not allow project selection:
// 1. it is a bad UX
// 2. what should happen if they are detached?
/*
->add('projects', EntityType::class, [
'label' => 'label.project',
'class' => 'TimesheetBundle:Project',
'multiple' => true,
'expanded' => true
])
*/
// boolean
->add('visible', YesNoType::class, [
'label' => 'label.visible',
])
// string - length 255
->add('company', TextType::class, [
'label' => 'label.company',
'required' => false,
])
// string - length 255
->add('vat', PercentType::class, [
'label' => 'label.vat',
'type' => 'integer',
])
// string - length 255
->add('contact', TextType::class, [
'label' => 'label.contact',
'required' => false,
])
// string - length 255
->add('address', TextareaType::class, [
'label' => 'label.address',
'required' => false,
])
// TODO string - length 2
->add('country', CountryType::class, [
'label' => 'label.country',
])
// string - length 255
->add('phone', TelType::class, [
'label' => 'label.phone',
'required' => false,
])
// string - length 255
->add('fax', TelType::class, [
'label' => 'label.fax',
'required' => false,
])
// string - length 255
->add('mobile', TelType::class, [
'label' => 'label.mobile',
'required' => false,
])
// string - length 255
->add('mail', EmailType::class, [
'label' => 'label.email',
'required' => false,
])
// string - length 255
->add('homepage', UrlType::class, [
'label' => 'label.homepage',
'required' => false,
])
// string - length 255
->add('timezone', TimezoneType::class, [
'label' => 'label.timezone',
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Customer::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'admin_activity_edit',
]);
}
}

View File

@@ -0,0 +1,41 @@
<?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;
/**
* Customer statistics
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class CustomerStatistic
{
/**
* @var int
*/
protected $totalAmount = 0;
/**
* @return int
*/
public function getTotalAmount()
{
return $this->totalAmount;
}
/**
* @param int $totalAmount
*/
public function setTotalAmount($totalAmount)
{
$this->totalAmount = $totalAmount;
}
}

View File

@@ -0,0 +1,69 @@
<?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\Repository;
use AppBundle\Entity\User;
use TimesheetBundle\Entity\Activity;
use TimesheetBundle\Entity\Timesheet;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
use Pagerfanta\Adapter\DoctrineORMAdapter;
use Pagerfanta\Pagerfanta;
use TimesheetBundle\Model\ActivityStatistic;
/**
* Class AbstractRepository
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
abstract class AbstractRepository extends EntityRepository
{
/**
* @param string $orderBy
* @return Query
*/
protected function queryAll($orderBy = 'id')
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('a')
->from($this->getEntityName(), 'a')
->orderBy('a.' . $orderBy, 'ASC');
return $qb->getQuery();
}
/**
* @param int $page
*
* @return Pagerfanta
*/
public function findAll($page = 1)
{
return $this->getPager($this->queryAll(), $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);
return $paginator;
}
}

View File

@@ -0,0 +1,50 @@
<?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\Repository;
use TimesheetBundle\Entity\Customer;
use TimesheetBundle\Model\CustomerStatistic;
/**
* Class CustomerRepository
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class CustomerRepository extends AbstractRepository
{
/**
* @param $id
* @return null|Customer
*/
public function getById($id)
{
return $this->find($id);
}
/**
* Return statistic data for all customer.
*
* @return CustomerStatistic
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getGlobalStatistics()
{
$countAll = $this->getEntityManager()
->createQuery('SELECT COUNT(a.id) FROM TimesheetBundle:Customer c')
->getSingleScalarResult();
$stats = new CustomerStatistic();
$stats->setTotalAmount($countAll);
return $stats;
}
}

View File

@@ -0,0 +1,60 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'admin_customer.subtitle'|trans }} {{ 'subtitle.amount'|trans({'%count%': entries.count}) }}{% endblock %}
{% block main %}
{{ include('default/_flash_messages.html.twig') }}
{#
private $vat;
private $contact;
private $street;
private $zipcode;
private $city;
private $country;
private $phone;
private $fax;
private $mobile;
private $mail;
private $homepage;
private $timezone;
#}
{% if entries.count > 0 %}
{{ tables.data_table_header({
'label.id': 'hidden-xs',
'label.name': '',
'label.project': '',
'label.comment': 'hidden-xs',
'label.visible': '',
'label.actions': '',
}) }}
{% for entry in entries %}
<tr>
<td class="hidden-xs">{{ entry.id }}</td>
<td>{{ entry.name }} {% if entry.company is not empty %}({{ entry.company }}){% endif %}</td>
<td>
{% for project in entry.projects %}
<a href="{{ path('admin_project_edit', {'id' : project.id}) }}">{{ widgets.label_project(project) }}</a>
{% endfor %}
</td>
<td class="hidden-xs">{{ entry.comment }}</td>
<td>{{ widgets.label_visible(entry.visible) }}</td>
<td>
{{ widgets.button_group({
'edit': path('admin_customer_edit', {'id': entry.id}),
'trash': '#'
}) }}
</td>
</tr>
{% endfor %}
{{ tables.data_table_footer(entries, 'admin_customer_paginated') }}
{% else %}
{{ widgets.callout('warning', 'error.no_entries_found') }}
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'admin_customer.subtitle'|trans }}{% endblock %}
{% block main %}
{{ include('default/_flash_messages.html.twig') }}
{{ include('default/_form.html.twig', {
'title': customer.name,
'form': form,
'back': path('admin_customer')
}) }}
{% endblock %}