diff --git a/composer.json b/composer.json index 1e77e75993..fa160efff5 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,8 @@ "knplabs/knp-gaufrette-bundle": "0.2.*", "fzaninotto/faker": "1.1.*", "knplabs/knp-menu-bundle": "2.0.*", - "liip/imagine-bundle": "0.9.*" + "liip/imagine-bundle": "0.9.*", + "athari/yalinqo": "*" }, "require-dev": { "behat/behat": "2.4.*", diff --git a/composer.lock b/composer.lock index 0f4c2a5ed0..107d91b533 100644 --- a/composer.lock +++ b/composer.lock @@ -3,8 +3,50 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "a4b77193c2dc308a7f9076b7f6aa8062", + "hash": "781f8e68f3cf0127a7fdd537f115fa84", "packages": [ + { + "name": "athari/yalinqo", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Athari/YaLinqo.git", + "reference": "6b3247909e037f218cca624c561d8a47ceeed6d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Athari/YaLinqo/zipball/6b3247909e037f218cca624c561d8a47ceeed6d2", + "reference": "6b3247909e037f218cca624c561d8a47ceeed6d2", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "autoload": { + "files": [ + "YaLinqo/Linq.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Simplified BSD" + ], + "authors": [ + { + "name": "Alexander Prokhorov" + } + ], + "description": "YaLinqo, a LINQ-to-objects library for PHP", + "homepage": "https://github.com/Athari/YaLinqo", + "keywords": [ + "linq", + "linqo", + "query", + "statistic" + ], + "time": "2013-05-08 12:59:02" + }, { "name": "doctrine/annotations", "version": "v1.1.1", @@ -1248,7 +1290,7 @@ "target-dir": "JMS/AopBundle", "source": { "type": "git", - "url": "https://github.com/schmittjoh/JMSAopBundle.git", + "url": "https://github.com/schmittjoh/JMSAopBundle", "reference": "7018357f5bedf204979a88867a9da05973744422" }, "dist": { @@ -1461,7 +1503,7 @@ "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/schmittjoh/parser-lib.git", + "url": "git://github.com/schmittjoh/parser-lib", "reference": "4d469a70c6dd03f921cbdeadafbcb261bb23e8b0" }, "dist": { @@ -2908,6 +2950,9 @@ "phpspec/phpspec2": "dev-develop", "twig/twig": "*" }, + "suggest": { + "friendsofsymfony/rest-bundle": "*" + }, "type": "symfony-bundle", "extra": { "branch-alias": { diff --git a/features/backend/dashboard.feature b/features/backend/dashboard.feature new file mode 100644 index 0000000000..44bb87d0ec --- /dev/null +++ b/features/backend/dashboard.feature @@ -0,0 +1,42 @@ +Feature: Store dashboard + In order to have an overview of my business + As a store owner + I need to be able to see sales info in backend dashboard + + Background: + Given I am logged in as administrator + And the following zones are defined: + | name | type | members | + | German lands | country | Germany, Austria, Switzerland | + And there are products: + | name | price | + | Mug | 5.99 | + | Sticker | 10.00 | + And the following orders were placed: + | user | address | + | klaus@example.com | Klaus Schmitt, Heine-Straße 12, 99734, Berlin, Germany | + | lars@example.com | Lars Meine, Fun-Straße 1, 90032, Vienna, Austria | + And order #000001 has following items: + | product | quantity | + | Mug | 2 | + And order #000002 has following items: + | product | quantity | + | Mug | 1 | + | Sticker | 4 | + + Scenario: Viewing the dashboard at website root + Given I am on the dashboard page + Then I should see "Administration dashboard" + + Scenario: Viewing sales info for last week + Given I am on the dashboard page + Then I should see last week revenue with value "€57.97" in the list + Then I should see last week orders with value "2" in the list + + Scenario: Viewing recent orders + Given I am on the dashboard page + Then I should see 2 orders in the list + + Scenario: Viewing recent users + Given I am on the dashboard page + Then I should see 3 users in the list diff --git a/features/backend/homepage.feature b/features/frontend/homepage.feature similarity index 100% rename from features/backend/homepage.feature rename to features/frontend/homepage.feature diff --git a/src/Sylius/Bundle/CoreBundle/DataFixtures/ORM/LoadOrdersData.php b/src/Sylius/Bundle/CoreBundle/DataFixtures/ORM/LoadOrdersData.php new file mode 100644 index 0000000000..65f81776c1 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/DataFixtures/ORM/LoadOrdersData.php @@ -0,0 +1,85 @@ +get('sylius.builder.order'); + + for ($i = 1; $i <= 50; $i++) { + $orderBuilder->create(); + + for ($j = 0; $j <= rand(3, 6); $j++) { + $variant = $this->getReference('Sylius.Variant-'.rand(1, SYLIUS_FIXTURES_TOTAL_VARIANTS - 1)); + + $orderBuilder->add($variant, $variant->getPrice(), rand(1, 5)); + } + + $order = $orderBuilder->getOrder(); + + $shipment = $this->getShipmentRepository()->createNew(); + $shipment->setMethod($this->getReference('Sylius.ShippingMethod.UPS Ground')); + + foreach ($order->getInventoryUnits() as $item) { + $shipment->addItem($item); + } + + $order->addShipment($shipment); + + $order->setNumber(str_pad((int) $i, 6, 0, STR_PAD_LEFT)); + $order->setUser($this->getReference('Sylius.User-'.rand(1, 15))); + $order->setShippingAddress($this->createAddress()); + $order->setBillingAddress($this->createAddress()); + $order->setCreatedAt($this->faker->dateTimeBetween('1 year ago', 'now')); + + $order->calculateTotal(); + + $this->setReference('Sylius.Order-'.$i, $order); + + $manager->persist($order); + } + + $manager->flush(); + } + + private function createAddress() + { + $address = $this->getAddressRepository()->createNew(); + + $address->setFirstname($this->faker->firstName); + $address->setLastname($this->faker->lastName); + $address->setCity($this->faker->city); + $address->setStreet($this->faker->streetAddress); + $address->setPostcode($this->faker->postcode); + + do { + $isoName = $this->faker->countryCode; + } while ('UK' === $isoName); + + $country = $this->getReference('Sylius.Country.'.$isoName); + $province = $country->hasProvinces() ? $this->faker->randomElement($country->getProvinces()) : null; + + $address->setCountry($country); + $address->setProvince($province); + + return $address; + } + + public function getOrder() + { + return 7; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Entity/Order.php b/src/Sylius/Bundle/CoreBundle/Entity/Order.php index 805a9ae48d..cfefde14ce 100644 --- a/src/Sylius/Bundle/CoreBundle/Entity/Order.php +++ b/src/Sylius/Bundle/CoreBundle/Entity/Order.php @@ -327,4 +327,9 @@ class Order extends BaseOrder implements OrderInterface { return $this->items->count(); } + + public function setCreatedAt(\DateTime $createdAt) + { + $this->createdAt = $createdAt; + } } diff --git a/src/Sylius/Bundle/CoreBundle/Repository/OrderRepository.php b/src/Sylius/Bundle/CoreBundle/Repository/OrderRepository.php index 5022e6ac16..bf7db18635 100644 --- a/src/Sylius/Bundle/CoreBundle/Repository/OrderRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Repository/OrderRepository.php @@ -12,6 +12,8 @@ namespace Sylius\Bundle\CoreBundle\Repository; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use YaLinqo\Enumerable; +use DateTime; class OrderRepository extends EntityRepository { @@ -62,4 +64,74 @@ class OrderRepository extends EntityRepository return $this->getPaginator($queryBuilder); } + + public function getTotalStatistics() + { + return Enumerable::from($this->findBetweenDates(new DateTime('1 year ago'), new DateTime())) + ->groupBy(function($order) { + return $order->getCreatedAt()->format('m'); + }, '$v->getTotal()', function($orders) { + return Enumerable::from($orders)->sum(); + }) + ->toValues() + ->toArray() + ; + } + + public function getCountStatistics() + { + return Enumerable::from($this->findBetweenDates(new DateTime('1 year ago'), new DateTime())) + ->groupBy(function($order) { + return $order->getCreatedAt()->format('m'); + }, null, function($orders) { + return Enumerable::from($orders)->count(); + }) + ->toValues() + ->toArray() + ; + } + + public function findBetweenDates(DateTime $from, DateTime $to) + { + $queryBuilder = $this->getCollectionQueryBuilderBetweenDates($from, $to); + + return $queryBuilder + ->getQuery() + ->getResult() + ; + } + + public function countBetweenDates(DateTime $from, DateTime $to) + { + $queryBuilder = $this->getCollectionQueryBuilderBetweenDates($from, $to); + + return $queryBuilder + ->select('count(o.id)') + ->getQuery() + ->getSingleScalarResult() + ; + } + + public function revenueBetweenDates(DateTime $from, DateTime $to) + { + $queryBuilder = $this->getCollectionQueryBuilderBetweenDates($from, $to); + + return $queryBuilder + ->select('sum(o.total)') + ->getQuery() + ->getSingleScalarResult() + ; + } + + protected function getCollectionQueryBuilderBetweenDates(DateTime $from, DateTime $to) + { + $queryBuilder = $this->getCollectionQueryBuilder(); + + return $queryBuilder + ->andWhere($queryBuilder->expr()->gte('o.createdAt', ':from')) + ->andWhere($queryBuilder->expr()->lte('o.createdAt', ':to')) + ->setParameter('from', $from) + ->setParameter('to', $to) + ; + } } diff --git a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml index 475ba2d1c1..d994547ade 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml @@ -6,6 +6,8 @@ sylius: checkout: shipping_method: Shipping Method payment_method: Payment Method + addressing: + different_billing_address: Use different address for billing? settings: general: title: Page title diff --git a/src/Sylius/Bundle/WebBundle/Controller/Backend/DashboardController.php b/src/Sylius/Bundle/WebBundle/Controller/Backend/DashboardController.php index 2947b6e75a..ecd6d570ec 100644 --- a/src/Sylius/Bundle/WebBundle/Controller/Backend/DashboardController.php +++ b/src/Sylius/Bundle/WebBundle/Controller/Backend/DashboardController.php @@ -12,6 +12,7 @@ namespace Sylius\Bundle\WebBundle\Controller\Backend; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use DateTime; /** * Backend dashboard controller. @@ -27,6 +28,49 @@ class DashboardController extends Controller */ public function mainAction() { - return $this->render('SyliusWebBundle:Backend/Dashboard:main.html.twig'); + $orderRepository = $this->get('sylius.repository.order'); + $months = array_map( + function($m) { + return date('F', mktime(0, 0, 0, $m, 10)); + }, + range(1, 12) + ); + + return $this->render('SyliusWebBundle:Backend/Dashboard:main.html.twig', array( + 'revenue' => $orderRepository->revenueBetweenDates(new DateTime('1 week ago'), new DateTime()), + 'ordersCount' => $orderRepository->countBetweenDates(new DateTime('1 week ago'), new DateTime()), + 'orders' => $orderRepository->findBy(array(), array('updatedAt' => 'desc'), 5), + 'users' => $this->get('sylius.repository.user')->findBy(array(), array('id' => 'desc'), 5), + 'charts' => array( + 'chart_order_total' => array( + 'label' => 'Order value (€)', + 'type' => 'Line', + 'data' => array( + 'labels' => $months, + 'datasets' => array( + array( + 'fillColor' => "rgba(151,187,205,0.5)", + 'strokeColor' => "rgba(151,187,205,1)", + 'data' => $orderRepository->getTotalStatistics() + ) + ) + ) + ), + 'chart_order_count' => array( + 'label' => 'Number of orders', + 'type' => 'Line', + 'data' => array( + 'labels' => $months, + 'datasets' => array( + array( + 'fillColor' => "rgba(151,187,205,0.5)", + 'strokeColor' => "rgba(151,187,205,1)", + 'data' => $orderRepository->getCountStatistics() + ) + ) + ) + ) + ) + )); } } diff --git a/src/Sylius/Bundle/WebBundle/Resources/translations/messages.en.xliff b/src/Sylius/Bundle/WebBundle/Resources/translations/messages.en.xliff index 6247d8c24e..955e8490ac 100644 --- a/src/Sylius/Bundle/WebBundle/Resources/translations/messages.en.xliff +++ b/src/Sylius/Bundle/WebBundle/Resources/translations/messages.en.xliff @@ -1,6 +1,6 @@ - +
The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message. @@ -10,6 +10,7 @@ Frontend/Product/_single.html.twig Frontend/Product/_singleBox.html.twig Frontend/Product/show.html.twig + Frontend/Product/_singleBox.html.twig sylius.add_to_cart Add to cart @@ -63,7 +64,6 @@ Backend/Dashboard/main.html.twig - Backend/Macros/alerts.html.twig Backend/User/macros.html.twig Backend/TaxCategory/macros.html.twig Backend/Stockable/macros.html.twig @@ -83,6 +83,31 @@ sylius.alert.warning Warning + + Backend/Dashboard/main.html.twig + sylius.backend.alert.info + Information + + + Backend/Dashboard/main.html.twig + sylius.backend.dashboard.header + Administration dashboard + + + Backend/Dashboard/main.html.twig + sylius.backend.dashboard.orders + Last week orders + + + Backend/Dashboard/main.html.twig + sylius.backend.dashboard.revenue + Last week revenue + + + Backend/Dashboard/main.html.twig + sylius.backend.dashboard.subheader + Overview of your store + Backend/Macros/actions.html.twig Backend/Macros/actions.html.twig @@ -152,10 +177,11 @@ Addressing step + Checkout/Step/shipping.html.twig + Checkout/Step/addressing.html.twig Checkout/Step/payment.html.twig Checkout/Step/shipping.html.twig Checkout/Step/finalize.html.twig - Checkout/Step/addressing.html.twig sylius.checkout.back Back @@ -224,12 +250,8 @@ Backend/Order/show.html.twig sylius.checkout.finalize.order.no_promotion - No promotion applied + No promotion applied - - sylius.form.checkout.addressing.different_billing_address - Use different address for billing? - Checkout/Step/finalize.html.twig sylius.checkout.finalize.order.no_shipping_charges @@ -244,13 +266,13 @@ Backend/Order/show.html.twig Checkout/Step/finalize.html.twig sylius.checkout.finalize.order.promotion_discount - Promotion discounts + Promotion discounts Backend/Order/show.html.twig Checkout/Step/finalize.html.twig sylius.checkout.finalize.order.promotion_total - Promotion total + Promotion total Checkout/Step/finalize.html.twig @@ -308,9 +330,9 @@ Name - Checkout/Step/payment.html.twig Checkout/Step/shipping.html.twig Checkout/Step/addressing.html.twig + Checkout/Step/payment.html.twig sylius.checkout.forward Continue @@ -413,40 +435,25 @@ sylius.create Create - - Backend/Dashboard/main.html.twig - sylius.dashboard.header - Administration dashboard - - - Backend/Dashboard/main.html.twig - sylius.dashboard.no_notifications - There are no new notifications. - - - Backend/Dashboard/main.html.twig - sylius.dashboard.subheader - Overview of your store - - Backend/Macros/buttons.html.twig Backend/Country/show.html.twig Backend/Country/show.html.twig - Backend/ShippingMethod/show.html.twig - Backend/TaxRate/show.html.twig Backend/Order/show.html.twig Backend/TaxCategory/macros.html.twig + Backend/TaxRate/show.html.twig + Backend/Macros/buttons.html.twig + Backend/ShippingMethod/show.html.twig sylius.delete delete - Backend/Macros/buttons.html.twig Backend/Country/show.html.twig - Backend/ShippingMethod/show.html.twig - Backend/TaxRate/show.html.twig Backend/Order/show.html.twig Backend/TaxCategory/macros.html.twig Backend/Stockable/macros.html.twig + Backend/TaxRate/show.html.twig + Backend/Macros/buttons.html.twig + Backend/ShippingMethod/show.html.twig sylius.edit edit @@ -479,7 +486,7 @@ Backend/Coupon/generate.html.twig sylius.generate - Generate + Generate Frontend/Homepage/main.html.twig @@ -574,17 +581,17 @@ Newest products - Backend/PaymentMethod/macros.html.twig + Backend/Variant/macros.html.twig Backend/User/macros.html.twig - Backend/User/show.html.twig - Backend/Variant/macros.html.twig - Backend/ShippingMethod/macros.html.twig - Backend/ShippingMethod/show.html.twig + Backend/User/show.html.twig + Backend/Product/show.html.twig + Backend/TaxRate/macros.html.twig Backend/Promotion/macros.html.twig Backend/Promotion/macros.html.twig - Backend/TaxRate/macros.html.twig Backend/Coupon/macros.html.twig - Backend/Product/show.html.twig + Backend/ShippingMethod/macros.html.twig + Backend/ShippingMethod/show.html.twig + Backend/PaymentMethod/macros.html.twig sylius.no no @@ -604,9 +611,9 @@ Product options + Backend/Product/index.html.twig Backend/Property/index.html.twig Backend/Prototype/index.html.twig - Backend/Product/index.html.twig sylius.option.manage Manage options @@ -736,7 +743,12 @@ Backend/Order/show.html.twig sylius.order.promotion_total - Promotion total + Promotion total + + + Backend/Dashboard/main.html.twig + sylius.order.recent_header + Recent orders Backend/Order/macros.html.twig @@ -986,8 +998,8 @@ Index of all products in your store]]> - Backend/Stockable/index.html.twig Backend/Product/show.html.twig + Backend/Stockable/index.html.twig sylius.product.manage Manage products @@ -1222,43 +1234,43 @@ Backend/Promotion/show.html.twig Backend/Coupon/index.html.twig sylius.promotion_coupon.create - Add coupon + Add coupon Backend/Coupon/create.html.twig sylius.promotion_coupon.create_header - Create new coupon + Create new coupon Backend/Promotion/show.html.twig Backend/Coupon/index.html.twig sylius.promotion_coupon.generate - Generate coupons + Generate coupons Backend/Coupon/generate.html.twig sylius.promotion_coupon.generate_header - Generating coupons + Generating coupons Backend/Promotion/show.html.twig sylius.promotion_coupon.index - List coupons + List coupons Backend/Coupon/macros.html.twig sylius.promotion_coupon.no_results - There are no coupon to display. + There are no coupon to display. Backend/Coupon/macros.html.twig sylius.promotion_coupon.total_results - Total: %total% + Total: %total% Backend/Coupon/update.html.twig sylius.promotion_coupon.update_header - Editing coupon + Editing coupon Backend/Promotion/show.html.twig @@ -1292,8 +1304,8 @@ Backend/Option/index.html.twig - Backend/Prototype/index.html.twig Backend/Product/index.html.twig + Backend/Prototype/index.html.twig sylius.property.manage Manage properties @@ -1750,8 +1762,8 @@ Tax rates - Backend/TaxRate/show.html.twig Backend/TaxCategory/index.html.twig + Backend/TaxRate/show.html.twig sylius.tax_rate.manage Manage tax rates @@ -1896,6 +1908,11 @@ sylius.user.no_results There are no users to display. + + Backend/Dashboard/main.html.twig + sylius.user.recent_header + Recent users + Backend/User/show.html.twig sylius.user.show_header @@ -1980,6 +1997,11 @@ Backend/Promotion/macros.html.twig Backend/TaxRate/macros.html.twig Backend/Product/show.html.twig + Backend/TaxRate/macros.html.twig + Backend/Promotion/macros.html.twig + Backend/ShippingMethod/macros.html.twig + Backend/ShippingMethod/show.html.twig + Backend/PaymentMethod/macros.html.twig sylius.yes yes diff --git a/src/Sylius/Bundle/WebBundle/Resources/views/Backend/Dashboard/main.html.twig b/src/Sylius/Bundle/WebBundle/Resources/views/Backend/Dashboard/main.html.twig index a12323a0d8..05c6ab3ce8 100644 --- a/src/Sylius/Bundle/WebBundle/Resources/views/Backend/Dashboard/main.html.twig +++ b/src/Sylius/Bundle/WebBundle/Resources/views/Backend/Dashboard/main.html.twig @@ -1,12 +1,63 @@ {% extends 'SyliusWebBundle:Backend:layout.html.twig' %} +{% from 'SyliusWebBundle:Backend/Order:macros.html.twig' import list as listOrders %} +{% from 'SyliusWebBundle:Backend/User:macros.html.twig' import list as listUsers %} + + +{% block javascripts %} +{{ parent() }} +{% javascripts + 'https://raw.github.com/nnnick/Chart.js/master/Chart.min.js' +%} + + +{% endjavascripts %} +{% endblock %} + {% block content %}
-

{{ 'sylius.alert.info'|trans }}

- {{ 'sylius.dashboard.no_notifications'|trans }} +

{{ 'sylius.backend.alert.info'|trans }}

+ There are no new notifications.
+ + + + + + + + + + + + + + +
{{ 'sylius.backend.dashboard.revenue'|trans }}{{ 'sylius.backend.dashboard.orders'|trans }}
{{ revenue|sylius_money }}{{ ordersCount }}
+ +
+ {% for id, chart in charts %} +
+

{{ chart.label|trans }}

+ +
+ {% endfor %} +
+ +

{{ 'sylius.order.recent_header'|trans }}

+{{ listOrders(orders) }} + +

{{ 'sylius.user.recent_header'|trans }}

+{{ listUsers(users) }} + {% endblock %} diff --git a/src/Sylius/Bundle/WebBundle/Resources/views/Backend/Order/macros.html.twig b/src/Sylius/Bundle/WebBundle/Resources/views/Backend/Order/macros.html.twig index 657de6cc49..a0230bcd75 100644 --- a/src/Sylius/Bundle/WebBundle/Resources/views/Backend/Order/macros.html.twig +++ b/src/Sylius/Bundle/WebBundle/Resources/views/Backend/Order/macros.html.twig @@ -4,7 +4,7 @@ {% import 'SyliusWebBundle:Backend/Macros:alerts.html.twig' as alerts %} {% if orders|length > 0 %} - +
diff --git a/src/Sylius/Bundle/WebBundle/Resources/views/Backend/User/macros.html.twig b/src/Sylius/Bundle/WebBundle/Resources/views/Backend/User/macros.html.twig index 983f493d39..7ffb768817 100644 --- a/src/Sylius/Bundle/WebBundle/Resources/views/Backend/User/macros.html.twig +++ b/src/Sylius/Bundle/WebBundle/Resources/views/Backend/User/macros.html.twig @@ -1,7 +1,7 @@ {% macro list(users) %} {% if users|length > 0 %} -
{{ sylius_resource_sort('createdAt', 'sylius.order.created_at'|trans) }}
+
{{ sylius_resource_sort('id', 'sylius.user.id'|trans) }}