001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.petstore.console;
018
019import java.time.*;
020import java.util.*;
021import java.util.stream.*;
022
023import org.apache.juneau.marshall.collections.*;
024import org.apache.juneau.petstore.console.pages.*;
025import org.apache.juneau.petstore.console.pages.StorePageBuilders.*;
026import org.apache.juneau.petstore.dto.*;
027import org.apache.juneau.rest.server.*;
028import org.apache.juneau.rest.server.view.*;
029import org.apache.juneau.rest.server.view.freemarker.*;
030
031/**
032 * The Store page, the petstore dashboard, and the feeds its charts, tiles and fragments read.
033 *
034 * <p>
035 * The page is {@code store.ftlh}, with a builder twin at {@code ?builder}.  The feeds are read-only JSON over the
036 * live store, and the revenue tile counts days by the store's clock in UTC.
037 *
038 * <h5 class='section'>Example:</h5>
039 * <p class='bjava'>
040 *    <ja>@RestGet</ja>(path=<js>"/"</js>)
041 *    <jk>public</jk> View page() { <jk>return</jk> FreemarkerView.<jsm>of</jsm>(<js>"store.ftlh"</js>); }
042 * </p>
043 * <p class='bcode'>
044 *    &lt;@page tab="store" toolkit="views"&gt;
045 *      &lt;@card id="welcome" title="Welcome to the petstore"&gt;&lt;div class="jc-prose"&gt;...&lt;/div&gt;&lt;/@card&gt;
046 *      &lt;@card type="chart" id="by-status" title="Pets by status" src="/console/store/stats/status"&gt;
047 *      { kind: 'bar', x: { field: 'status' }, series: [ { field: 'count', label: 'Pets' } ] }
048 *      &lt;/@card&gt;
049 *    &lt;/@page&gt;
050 * </p>
051 * <p class='bcode'>
052 *    GET /console/store                   the page
053 *    GET /console/store/recent-orders     the ten newest orders
054 *    GET /console/store/stats/daily       {day, placed, delivered} for each recent day
055 *    GET /console/store/kpi/revenue       {label, value, unit} for the revenue tile
056 *    GET /console/store/stats/species     {species, count} for each species
057 *    GET /console/store/stats/status      {status, count} for each status
058 *    GET /console/store/stats/pets        {id, name, species, status} for each pet
059 *    GET /console/store/fragments/hours   the opening hours, as markup only
060 * </p>
061 * <p class='bcode'>
062 *    &lt;@badge id="pending" src="/console/pets/changes/pending" href="/console/pets/changes" refreshMs=60000/&gt;
063 * </p>
064 */
065@Rest(path="/store", title="Store")
066@SuppressWarnings({
067   "java:S110" // Inheritance depth comes from the BasicRestServlet hierarchy, not this page.
068})
069public class StoreRest extends PetstoreConsolePage {
070
071   private static final long serialVersionUID = 1L;
072
073   /**
074    * Renders the Store page, with the quick-stat tile values computed from the store.
075    *
076    * @param req The request; {@code ?builder} renders the builder twin.
077    * @return The page view.
078    */
079   @RestGet(path="/")
080   public View page(RestRequest req) {
081      var qs = QuickStats.of(store());
082      if (builder(req))
083         return twin(req, StorePageBuilders.store(qs));
084      return FreemarkerView.of("store.ftlh")
085         .attr("qsAvailable", qs.available())
086         .attr("qsPending", qs.pending())
087         .attr("qsSold", qs.sold())
088         .attr("qsOrdersPlaced", qs.ordersPlaced())
089         .attr("qsOrdersApproved", qs.ordersApproved())
090         .attr("qsOrdersDelivered", qs.ordersDelivered())
091         .attr("qsPetTotal", qs.petTotal());
092   }
093
094   /** @return The 10 most recent orders, by ship date then id, newest first. */
095   @RestGet(path="/recent-orders")
096   public List<Order> recentOrders() {
097      return store().getOrders().stream()
098         .sorted(Comparator.comparing(Order::getShipDate, Comparator.nullsFirst(Comparator.<String>naturalOrder())).thenComparingLong(Order::getId).reversed())
099         .limit(10).toList();
100   }
101
102   /** @return 30 rows {@code {day, placed, delivered}} ending today, oldest first; an order counts on its ship date. */
103   @RestGet(path="/stats/daily")
104   public JsonList daily() {
105      var today = LocalDate.ofInstant(store().clock().instant(), ZoneOffset.UTC);
106      var placed = new HashMap<String,Integer>();
107      var delivered = new HashMap<String,Integer>();
108      for (var o : store().getOrders()) {
109         if (o.getStatus() == OrderStatus.DELIVERED)
110            delivered.merge(o.getShipDate(), 1, Integer::sum);
111         else
112            placed.merge(o.getShipDate(), 1, Integer::sum);
113      }
114      var rows = new JsonList();
115      for (var i = 29; i >= 0; i--) {
116         var day = today.minusDays(i).toString();
117         rows.add(JsonMap.of("day", day, "placed", placed.getOrDefault(day, 0), "delivered", delivered.getOrDefault(day, 0)));
118      }
119      return rows;
120   }
121
122   /**
123    * The revenue tile: the summed pet prices of the orders delivered today, by the store clock in UTC.
124    *
125    * @return {@code {label, value, unit}}, with the value rounded to cents.
126    */
127   @RestGet(path="/kpi/revenue")
128   public JsonMap revenue() {
129      var today = LocalDate.ofInstant(store().clock().instant(), ZoneOffset.UTC).toString();
130      var total = 0d;
131      for (var o : store().getOrders()) {
132         if (o.getStatus() == OrderStatus.DELIVERED && today.equals(o.getShipDate())) {
133            var p = store().getPet(o.getPetId());
134            if (p != null)
135               total += p.getPrice();
136         }
137      }
138      return JsonMap.of("label", "Revenue today", "value", Math.round(total * 100) / 100d, "unit", "USD");
139   }
140
141   /** @return One row {@code {species, count}} per species, in enum order, zero counts included; pets with no species are not counted. */
142   @RestGet(path="/stats/species")
143   public JsonList species() {
144      var counts = store().getPets().stream().filter(p -> p.getSpecies() != null).collect(Collectors.groupingBy(Pet::getSpecies, Collectors.counting()));
145      var rows = new JsonList();
146      Arrays.stream(Species.values()).forEach(s -> rows.add(JsonMap.of("species", s.name(), "count", counts.getOrDefault(s, 0L))));
147      return rows;
148   }
149
150   /** @return One row {@code {status, count}} per pet status, in enum order. */
151   @RestGet(path="/stats/status")
152   public JsonList status() {
153      var counts = store().getPets().stream().filter(p -> p.getStatus() != null).collect(Collectors.groupingBy(Pet::getStatus, Collectors.counting()));
154      var rows = new JsonList();
155      Arrays.stream(PetStatus.values()).forEach(s -> rows.add(JsonMap.of("status", s.name(), "count", counts.getOrDefault(s, 0L))));
156      return rows;
157   }
158
159   /** @return One row {@code {id, name, species, status}} per pet, by id; the treemap groups them and opens the edit dialog from a tile. */
160   @RestGet(path="/stats/pets")
161   public JsonList pets() {
162      var rows = new JsonList();
163      store().getPets().stream().sorted(Comparator.comparingLong(Pet::getId)).forEach(p ->
164         rows.add(JsonMap.of("id", p.getId(), "name", p.getName(), "species", p.getSpecies() == null ? null : p.getSpecies().name(), "status", p.getStatus() == null ? null : p.getStatus().name())));
165      return rows;
166   }
167
168   /** @return The store hours as a chrome-free fragment. */
169   @RestGet(path="/fragments/hours")
170   public View hours() {
171      return FreemarkerView.of("fragments/store-hours.ftlh");
172   }
173}