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 * <@page tab="store" toolkit="views"> 045 * <@card id="welcome" title="Welcome to the petstore"><div class="jc-prose">...</div></@card> 046 * <@card type="chart" id="by-status" title="Pets by status" src="/console/store/stats/status"> 047 * { kind: 'bar', x: { field: 'status' }, series: [ { field: 'count', label: 'Pets' } ] } 048 * </@card> 049 * </@page> 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 * <@badge id="pending" src="/console/pets/changes/pending" href="/console/pets/changes" refreshMs=60000/> 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}