Add graphs on stats page, suggestions improvements, only fetch on focus

This commit is contained in:
2024-06-24 20:52:35 +02:00
parent 2a58e8f2ea
commit 38ca48b6b5
20 changed files with 615 additions and 49 deletions
-6
View File
@@ -24,10 +24,4 @@ Then run the app in production mode:
npm start
```
## Todo
- Filtering
- Easy inventory updating for admin
- Mobile styling frontpage
- Undo checkout for admin
+8 -8
View File
@@ -48,7 +48,7 @@ function DrinkPage(arg:Arguments) {
containers.map((container) => {
totalinventory += container.inventory;
if((container.portions || 0) > 1){
if((container.portions || 0) > 1 && container.inventory > 0){
containerWithMultiplePortions = true;
}
})
@@ -227,13 +227,13 @@ function DrinkPage(arg:Arguments) {
) : ""}
</Row>
<Row className="mt-3 mb-3">
<Col col={12}>
<h3>Checkouts</h3>
<ClientOnly fallback={<Fallback />}>
{() => <Chart options={chartOptions} data={arg.chartData} type={"bar"} />}
</ClientOnly>
</Col>
</Row>
<Col col={12}>
<h3>Checkouts</h3>
<ClientOnly fallback={<Fallback />}>
{() => <Chart options={chartOptions} data={arg.chartData} type={"bar"} />}
</ClientOnly>
</Col>
</Row>
{arg.recommendations.length > 0 ? (
<>
<Row className="mt-3 mb-3">
@@ -27,7 +27,7 @@ export async function generateDrinksChartData(id: number){
// History of specific drink
const containersWithHistory = await dbe.container.findMany({
where: {drink_id: id},
include: {checkouts: {orderBy: {checkoutAt: "asc"}}}
include: {checkouts: {orderBy: {checkoutAt: "asc"}, where: {checkoutAt: {gte: dates[0]}}}}
})
var datasets : any = [];
@@ -88,21 +88,22 @@ export async function generateDrinksChartData(id: number){
inventoryAfter[i] = totalInventory;
}
let color = random_rgba();
let [color1, color2] = random_rgba(2);
datasets.push({
type: "line",
label: 'Inventory ' + volume(container.volume),
data: inventoryAfter,
borderColor: color,
backgroundColor: color,
borderColor: color1,
backgroundColor: color1,
tension: 0.1
});
datasets.push({
type: 'bar',
label: 'Checkouts ' + volume(container.volume),
data: checkouts,
borderColor: color,
backgroundColor: color,
borderColor: color2,
backgroundColor: color2,
});
}
+207
View File
@@ -0,0 +1,207 @@
import { enhance } from "@zenstackhq/runtime";
import { random_rgba } from "~/utils/color";
import timeAgo from "~/utils/datetime";
import { db } from "~/utils/db.server";
import { randomRange } from "~/utils/random";
export async function generateMonthChartData(){
const dbe = enhance(db);
// Get the previous month in dates
var dates = [];
const now = Date.now();
const subtracter = (1000 * 60 * 60 * 24);
for(let i = 0; i <= 30; i++){
let date = now - (subtracter * i);
dates.push(new Date(date));
}
dates.reverse();
// Generate labels
const labels = dates.map((date)=> timeAgo(date));
// History of specific drink
const containersWithHistory = await dbe.container.findMany({
include: {drink: true, checkouts: {orderBy: {checkoutAt: "asc"}, where: {checkoutAt: {gte: dates[0]}}}}
})
var datasets : any = [];
for(let container of containersWithHistory){
if(container.checkouts.length == 0) continue;
var history = container.checkouts;
var totalInventory = container.inventory;
// Generate arrays of data
var checkouts = [];
var inventoryAfter = [];
var lastInventoryAfter = history.length > 0 ? history[0].inventoryAfter + 1 : 0;
var previousCheckoutInventoryAfter = history.length > 0 ? history[0].inventoryAfter : 0;
var lastChange = 0;
var previousLastChange = 0;
for(let i = 0; i <= dates.length - 1; i++){
let startDate = dates[i];
let endDate = dates[i+1];
let totalCheckouts = 0;
for(let entry of history){
if(entry.checkoutAt > startDate && entry.checkoutAt < endDate){
totalCheckouts++;
lastInventoryAfter = entry.inventoryAfter;
lastChange = i;
}
}
// There was inventory added
if((lastInventoryAfter + totalCheckouts) > previousCheckoutInventoryAfter){
if(i > 0 && checkouts[i-1] > 0){
// Special case, there were also checkouts the previous day, do nothing
} else if(i > 0) {
let index = randomRange(previousLastChange + 1, i - 1);
for(let j = index; j <= i - 1; j++){
inventoryAfter[j] = lastInventoryAfter + totalCheckouts;
}
}
}
previousCheckoutInventoryAfter = lastInventoryAfter;
previousLastChange = lastChange;
checkouts[i] = totalCheckouts;
inventoryAfter[i] = lastInventoryAfter;
}
// Force the remaining inventory left values after the last checkout to be equal to
// the actual remaining inventory
let index = randomRange(lastChange + 1, dates.length - 1);
for(let i = index; i <= dates.length - 1; i++){
inventoryAfter[i] = totalInventory;
}
let [color1] = random_rgba(1);
datasets.push({
type: 'bar',
label: 'Checkouts ' + container.drink.name,
data: checkouts,
borderColor: color1,
backgroundColor: color1,
});
}
const data = {
labels,
datasets: datasets
};
return data;
}
export async function generate24HChartData(){
const dbe = enhance(db);
// Get the previous month in hours
var hours = [];
const now = Date.now();
const subtracter = (1000 * 60 * 60);
for(let i = 0; i <= 24; i++){
let date = now - (subtracter * i);
hours.push(new Date(date));
}
hours.reverse();
// Generate labels
const labels = hours.map((date)=> timeAgo(date));
// History of specific drink
const containersWithHistory = await dbe.container.findMany({
include: {drink: true, checkouts: {orderBy: {checkoutAt: "asc"}, where: {checkoutAt: {gte: hours[0]}}}}
})
var datasets : any = [];
for(let container of containersWithHistory){
if(container.checkouts.length == 0) continue;
var history = container.checkouts;
var totalInventory = container.inventory;
// Generate arrays of data
var checkouts = [];
var inventoryAfter = [];
var lastInventoryAfter = history.length > 0 ? history[0].inventoryAfter + 1 : 0;
var previousCheckoutInventoryAfter = history.length > 0 ? history[0].inventoryAfter : 0;
var lastChange = 0;
var previousLastChange = 0;
for(let i = 0; i <= hours.length - 1; i++){
let startDate = hours[i];
let endDate = hours[i+1];
let totalCheckouts = 0;
for(let entry of history){
if(entry.checkoutAt > startDate && entry.checkoutAt < endDate){
totalCheckouts++;
lastInventoryAfter = entry.inventoryAfter;
lastChange = i;
}
}
// There was inventory added
if((lastInventoryAfter + totalCheckouts) > previousCheckoutInventoryAfter){
if(i > 0 && checkouts[i-1] > 0){
// Special case, there were also checkouts the previous day, do nothing
} else if(i > 0) {
let index = randomRange(previousLastChange + 1, i - 1);
for(let j = index; j <= i - 1; j++){
inventoryAfter[j] = lastInventoryAfter + totalCheckouts;
}
}
}
previousCheckoutInventoryAfter = lastInventoryAfter;
previousLastChange = lastChange;
checkouts[i] = totalCheckouts;
inventoryAfter[i] = lastInventoryAfter;
}
// Force the remaining inventory left values after the last checkout to be equal to
// the actual remaining inventory
let index = randomRange(lastChange + 1, hours.length - 1);
for(let i = index; i <= hours.length - 1; i++){
inventoryAfter[i] = totalInventory;
}
let [color1] = random_rgba(1);
datasets.push({
type: 'bar',
label: 'Checkouts ' + container.drink.name,
data: checkouts,
borderColor: color1,
backgroundColor: color1,
});
}
const data = {
labels,
datasets: datasets
};
return data;
}
+155
View File
@@ -0,0 +1,155 @@
import { DrinkType } from "@prisma/client";
import { enhance } from "@zenstackhq/runtime";
import { hexToRGB, random_rgba } from "~/utils/color";
import timeAgo from "~/utils/datetime";
import { db } from "~/utils/db.server";
import { randomRange } from "~/utils/random";
export async function generateTypeInventoryChartData(){
const dbe = enhance(db);
let labels : string[] = [];
let colors : string[] = [];
let numset : number[] = [];
let priceset : number[] = [];
for(let type in DrinkType){
if(type == DrinkType.Drink) continue;
let typeT = DrinkType[type];
labels.push(type);
colors.push(random_rgba(1)[0]);
const aggregate = await dbe.container.aggregate({_sum: {inventory: true}, where: {drink: {type: typeT}}});
numset.push(aggregate._sum.inventory ?? 0);
const containers = await dbe.container.findMany({select: {price: true, inventory: true}, where: {drink: {type: typeT}}});
let reduced = containers.reduce((sum, val) => {return {inventory: sum.inventory + val.inventory, price: sum.price + (val.price * val.inventory)}});
priceset.push(reduced.price);
}
const data = {
labels,
datasets: [{
type: 'doughnut',
label: 'Inventory',
data: numset,
backgroundColor: colors,
},
{
type: 'doughnut',
label: 'Price',
data: priceset,
backgroundColor: colors,
}]
};
return data;
}
export async function generateBeerInventoryChartData(){
const dbe = enhance(db);
let labels : string[] = [];
let colors : string[] = [];
let numset : number[] = [];
const beerstyle = await dbe.beerStyle.findMany({});
for(let style of beerstyle){
labels.push(style.name);
colors.push(hexToRGB("#"+style.color, 1));
const containers = await dbe.beer.findMany({where: {style_id: style.id}, select: {containers: {select: {inventory: true}}}});
const reduced = containers.reduce((sum, val) => {return sum + val.containers.reduce((sum2, val2)=>{return sum2 + val2.inventory}, 0)}, 0);
numset.push(reduced);
}
const data = {
labels,
datasets: [{
type: 'doughnut',
label: 'Inventory',
data: numset,
backgroundColor: colors,
}]
};
return data;
}
export async function generateTypeHistoryChartData(){
const dbe = enhance(db);
let labels : string[] = [];
let colors : string[] = [];
let numset : number[] = [];
let priceset : number[] = [];
for(let type in DrinkType){
if(type == DrinkType.Drink) continue;
let typeT = DrinkType[type];
labels.push(type);
colors.push(random_rgba(1)[0]);
const aggregate = await dbe.history.aggregate({_count: {_all: true}, where: {container: {drink: {type: typeT}}}});
numset.push(aggregate._count._all ?? 0);
const history = await dbe.history.findMany({select: {container: {select: {price: true}}}, where: {container: {drink: {type: typeT}}}});
let reduced = history.reduce((sum, val) => {return {container:{ price: sum.container.price + val.container.price}}}, {container: {price: 0}});
priceset.push(reduced.container.price);
}
const data = {
labels,
datasets: [{
type: 'doughnut',
label: 'Checked out',
data: numset,
backgroundColor: colors,
},
{
type: 'doughnut',
label: 'Price',
data: priceset,
backgroundColor: colors,
}]
};
return data;
}
export async function generateBeerHistoryChartData(){
const dbe = enhance(db);
let labels : string[] = [];
let colors : string[] = [];
let numset : number[] = [];
const beerstyle = await dbe.beerStyle.findMany({});
for(let style of beerstyle){
labels.push(style.name);
colors.push(hexToRGB("#"+style.color, 1));
const containers = await dbe.beer.findMany({where: {style_id: style.id}, select: {containers: {select: {checkouts: true}}}});
const reduced = containers.reduce((sum, val) => {return sum + val.containers.reduce((sum2, val2)=>{return sum2 + val2.checkouts.length}, 0)}, 0);
numset.push(reduced);
}
const data = {
labels,
datasets: [{
type: 'doughnut',
label: 'Checked out',
data: numset,
backgroundColor: colors,
}]
};
return data;
}
+15
View File
@@ -0,0 +1,15 @@
import { LoaderFunctionArgs, redirect } from "@remix-run/node";
import { enhance } from "~/utils/db.server";
export const loader = async ({
request,
params
} : LoaderFunctionArgs) => {
const { dbe } = await enhance(request);
const id = Number(params.id);
await dbe.suggestion.update({where: {id: id}, data: {resolved: true}});
return redirect("/inventory/suggestions");
}
+1 -1
View File
@@ -9,7 +9,7 @@ import {
useRouteError } from "@remix-run/react";
import DrinkPage from "~/components/cards/drink.page";
import { generateDrinksChartData } from "~/models/charts.server";
import { generateDrinksChartData } from "~/models/drinks.charts.server";
import { findIdFromSlug, findRecommendations, getMostCommonSection } from "~/models/drinks.server";
@@ -9,7 +9,7 @@ import {
useRouteError } from "@remix-run/react";
import DrinkPage from "~/components/cards/drink.page";
import { generateDrinksChartData } from "~/models/charts.server";
import { generateDrinksChartData } from "~/models/drinks.charts.server";
import { findIdFromSlug, findRecommendations, getMostCommonSection } from "~/models/drinks.server";
import { enhance } from "~/utils/db.server";
+34 -9
View File
@@ -31,25 +31,50 @@ export default function InventoryLayout() {
const [checkouts, setCheckouts] = useState(data.checkouts);
let fetcher = useFetcher();
let shouldFetch = true;
// User has switched back to the tab
const onFocus = () => {
shouldFetch = true;
};
// User has switched away from the tab (AKA tab is hidden)
const onBlur = () => {
shouldFetch = false;
};
useEffect(() => {
window.addEventListener("focus", onFocus);
window.addEventListener("blur", onBlur);
// Calls onFocus when the window first loads
onFocus();
// Specify how to clean up after this effect:
return () => {
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
};
}, []);
useEffect(() => {
const timer = setTimeout(() => {
fetcher.load("/resource/checkouts");
if(shouldFetch){
fetcher.load("/resource/checkouts");
if(fetcher.state === "idle"){
if(!fetcher.data) return;
if(fetcher.state === "idle"){
if(!fetcher.data) return;
let newCheckouts = Number(fetcher.data.checkouts);
if(newCheckouts != checkouts){
let newCheckouts = Number(fetcher.data.checkouts);
if(newCheckouts != checkouts){
setCheckouts(newCheckouts);
setCheckouts(newCheckouts);
if (revalidator.state === "idle") {
revalidator.revalidate();
if (revalidator.state === "idle") {
revalidator.revalidate();
}
}
}
}
}, 5000);
+20 -2
View File
@@ -1,5 +1,5 @@
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { useLoaderData, useSearchParams } from "@remix-run/react";
import { LoaderFunctionArgs, json, redirect } from "@remix-run/node";
import { Link, useLoaderData, useNavigate, useSearchParams } from "@remix-run/react";
import { enhance } from "@zenstackhq/runtime";
import { useState } from "react";
import { Accordion, Button, Col, Container, Offcanvas, Row, useAccordionButton } from "react-bootstrap";
@@ -11,6 +11,7 @@ import { findManufacturersFromSearch } from "~/models/drinks.server";
import { sortDrinksFromSearch } from "~/models/drinks.sort.server";
import { db } from "~/utils/db.server";
import { randomRange } from "~/utils/random";
export const loader = async ({request} : LoaderFunctionArgs) => {
const dbe = enhance(db);
@@ -30,6 +31,8 @@ export const loader = async ({request} : LoaderFunctionArgs) => {
export default function BeersRoute() {
const loadData = useLoaderData<typeof loader>();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [show, setShow] = useState(false);
@@ -39,6 +42,12 @@ export default function BeersRoute() {
const numResults = loadData.drinkResults.length;
let randomDrink = function(){
let index = randomRange(0, numResults - 1);
let drink_id = loadData.drinkResults[index].id;
navigate("/inventory/drink/" + drink_id);
}
return (
<Container>
<Row className="mt-3">
@@ -77,6 +86,15 @@ export default function BeersRoute() {
</Row>
</Col>
</Row>
<Row>
<Col key="pick">
<Row className="my-5">
<Col className="text-center">
<Button size="lg" onClick={randomDrink} >Pick one for me!</Button>
</Col>
</Row>
</Col>
</Row>
</Container>
);
}
+1 -1
View File
@@ -9,7 +9,7 @@ import {
useRouteError } from "@remix-run/react";
import DrinkPage from "~/components/cards/drink.page";
import { generateDrinksChartData } from "~/models/charts.server";
import { generateDrinksChartData } from "~/models/drinks.charts.server";
import { findIdFromSlug, findRecommendations, getMostCommonSection } from "~/models/drinks.server";
import { enhance } from "~/utils/db.server";
+113 -5
View File
@@ -2,14 +2,18 @@ import { LoaderFunctionArgs, json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { enhance } from "@zenstackhq/runtime";
import { Badge, Button, Col, Container, Nav, Row, Tab, Table, Tabs } from "react-bootstrap";
import { Line } from "react-chartjs-2";
import { Chart, Line } from "react-chartjs-2";
import { LinkContainer } from "react-router-bootstrap";
import { ClientOnly } from "remix-utils/client-only";
import { generate24HChartData, generateMonthChartData } from "~/models/history.chart.server";
import { ContainerWithDrink, containerTypeToString } from "~/models/types";
import { volume } from "~/utils/conversions";
import timeAgo from "~/utils/datetime";
import 'chart.js/auto';
import { db } from "~/utils/db.server";
import { generateBeerHistoryChartData, generateBeerInventoryChartData, generateTypeHistoryChartData, generateTypeInventoryChartData } from "~/models/type.chart.server";
function statsForContainers(containers : ContainerWithDrink[]){
var drinksInventory = 0;
@@ -56,7 +60,6 @@ function statsForContainers(containers : ContainerWithDrink[]){
totalPrice = Math.round(totalPrice * 100) / 100;
averageAlcoholPercentage = Math.round(averageAlcoholPercentage * 100) / 100;
return {drinksInventory, beersInventory, wineInventory, sodaInventory, cocktailInventory, totalLiterAlcohol, averageAlcoholPercentage, totalPrice};
}
@@ -80,6 +83,7 @@ export const loader = async ({request} : LoaderFunctionArgs) => {
});
const statsLast24H = statsForContainers(containersLast24);
const chartData24H = await generate24HChartData();
// Checkout 24H statistics
let lastMonthDate = Date.now() - (31 * 24 * 60 * 60 * 1000);
@@ -93,10 +97,12 @@ export const loader = async ({request} : LoaderFunctionArgs) => {
containersLastMonth.push(entry.container);
});
const statsLastMonth = statsForContainers(containersLastMonth);
let statsLastMonth = statsForContainers(containersLastMonth);
const chartDataMonth = await generateMonthChartData();
const historyTotal = await dbe.history.findMany({include: {container: {include: {drink: true}}}});
const chartDataHistoryTotal = await generateTypeHistoryChartData();
const chartDataBeerHistoryTotal = await generateBeerHistoryChartData();
let containersTotal : ContainerWithDrink[] = [];
historyTotal.forEach((entry) => {
@@ -105,15 +111,83 @@ export const loader = async ({request} : LoaderFunctionArgs) => {
});
const statsTotal = statsForContainers(containersTotal);
const chartDataTotal = await generateTypeInventoryChartData();
const chartDataBeerTotal = await generateBeerInventoryChartData();
const history = await dbe.history.findMany({select: {checkoutAt: true, inventoryAfter: true, container: {select: {volume: true, type: true, drink: true}}}, take: 10, orderBy: {checkoutAt: "desc"}});
return json({total, statsLast24H, statsLastMonth, statsTotal, history});
return json({total, chartDataTotal, chartDataBeerTotal, statsLast24H, chartData24H, statsLastMonth, chartDataMonth, statsTotal, chartDataHistoryTotal, chartDataBeerHistoryTotal, history});
};
export default function StatsRoute() {
const loadData = useLoaderData<typeof loader>();
const barChartOptions = {
responsive: true,
plugins: {
legend: {
display: false
},
title: {
display: false
},
},
scales: {
x: {
title: {
display: true,
text: 'History'
},
stacked: true
},
y: {
stacked: true,
title: {
display: true,
text: 'Amount'
},
min: 0,
ticks: {
// forces step size to be 50 units
stepSize: 1
}
}
}
};
const doughnutChartOptions = {
responsive: true,
plugins: {
legend: {
display: false
},
title: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if(label != "Price: ") return label + context.parsed;
if (context.parsed !== null) {
label += new Intl.NumberFormat('nl-NL', {
style: 'currency',
currency: 'EUR'
}).format(context.parsed);
}
return label;
}
}
}
}
};
return (
<Container>
<Row className="mt-3">
@@ -171,6 +245,18 @@ export default function StatsRoute() {
</tr>
</tbody>
</Table>
<Row>
<Col xs={12} md={4}>
<ClientOnly fallback={<Fallback />}>
{() => <Chart options={doughnutChartOptions} data={loadData.chartDataTotal} type={"doughnut"} />}
</ClientOnly>
</Col>
<Col xs={12} md={4}>
<ClientOnly fallback={<Fallback />}>
{() => <Chart options={doughnutChartOptions} data={loadData.chartDataBeerTotal} type={"doughnut"} />}
</ClientOnly>
</Col>
</Row>
</Tab>
<Tab eventKey="totalh" title="Total history">
<h2 className="mt-3">Total checkouts</h2>
@@ -224,6 +310,18 @@ export default function StatsRoute() {
</tr>
</tbody>
</Table>
<Row>
<Col xs={12} md={4}>
<ClientOnly fallback={<Fallback />}>
{() => <Chart options={doughnutChartOptions} data={loadData.chartDataHistoryTotal} type={"doughnut"} />}
</ClientOnly>
</Col>
<Col xs={12} md={4}>
<ClientOnly fallback={<Fallback />}>
{() => <Chart options={doughnutChartOptions} data={loadData.chartDataBeerHistoryTotal} type={"doughnut"} />}
</ClientOnly>
</Col>
</Row>
</Tab>
<Tab eventKey="month" title="Last Month">
<h2 className="mt-3">Last Month checkouts</h2>
@@ -277,6 +375,9 @@ export default function StatsRoute() {
</tr>
</tbody>
</Table>
<ClientOnly fallback={<Fallback />}>
{() => <Chart options={barChartOptions} data={loadData.chartDataMonth} type={"bar"} />}
</ClientOnly>
</Tab>
<Tab eventKey="24h" title="Last 24H">
<h2 className="mt-3">Last 24H checkouts</h2>
@@ -330,6 +431,9 @@ export default function StatsRoute() {
</tr>
</tbody>
</Table>
<ClientOnly fallback={<Fallback />}>
{() => <Chart options={barChartOptions} data={loadData.chartData24H} type={"bar"} />}
</ClientOnly>
</Tab>
</Tabs>
</Col>
@@ -361,4 +465,8 @@ export default function StatsRoute() {
</Row>
</Container>
);
}
function Fallback() {
return <div>Generating Chart</div>;
}
+18 -4
View File
@@ -1,4 +1,4 @@
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/node";
import { ActionFunctionArgs, EntryContext, LoaderFunctionArgs, json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { Button, Col, Container, Form, Row, Table } from "react-bootstrap";
import { LinkContainer } from "react-router-bootstrap";
@@ -94,7 +94,10 @@ export default function SuggestionsRoute() {
<th>Date</th>
<th>Suggestions</th>
{ loadData.isAdmin ? (
<th>Delete</th>
<>
<th>Resolve</th>
<th>Delete</th>
</>
) : ""}
</tr>
</thead>
@@ -103,15 +106,26 @@ export default function SuggestionsRoute() {
<tr key={entry.id}>
<td>{entry.name ? (<>{entry.name}</>) : ( <><i>No name</i></> ) }</td>
<td>{timeAgo(new Date(entry.createdAt))}</td>
<td>{entry.content}</td>
<td>{entry.resolved ? (<><s className="text-muted">{entry.content}</s></>) : (<>{entry.content}</>)}</td>
{ loadData.isAdmin ? (
<>
<td>
{!entry.resolved ? (
<LinkContainer to={"/admin/resolve/suggestion/" + entry.id}>
<Button variant="success">
Resolve
</Button>
</LinkContainer>
): ""}
</td>
<td>
<LinkContainer to={"/admin/remove/suggestion/" + entry.id}>
<Button variant="danger">
Remove
</Button>
</LinkContainer>
</td>
</td>
</>
) : ""}
</tr>
))}
+1 -1
View File
@@ -9,7 +9,7 @@ import {
useRouteError } from "@remix-run/react";
import DrinkPage from "~/components/cards/drink.page";
import { generateDrinksChartData } from "~/models/charts.server";
import { generateDrinksChartData } from "~/models/drinks.charts.server";
import { findIdFromSlug, findRecommendations, getMostCommonSection } from "~/models/drinks.server";
import { enhance } from "~/utils/db.server";
+26 -3
View File
@@ -1,4 +1,27 @@
export function random_rgba() {
var o = Math.round, r = Math.random, s = 205, m=50;
return 'rgba(' + o(r()*s + m) + ',' + o(r()*s + m) + ',' + o(r()*s + m) + ',' + 1 + ')';
export function random_rgba(numShades : number) {
let o = Math.round, r = Math.random, s = 155, m = 100;
let red = o(r()*s + m), green = o(r()*s + m), blue = o(r()*s + m);
let result = [];
numShades = Math.min(numShades, 3);
for(let i = 0; i < numShades; ++i){
result.push('rgba(' + (red - i*50) + ',' + (green - i*50) + ',' + (blue - i*50) + ',' + 0.8 + ')');
}
return result;
}
export function hexToRGB(hex : string, alpha : number) {
var r = parseInt(hex.slice(1, 3), 16),
g = parseInt(hex.slice(3, 5), 16),
b = parseInt(hex.slice(5, 7), 16);
if (alpha) {
return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
} else {
return "rgb(" + r + ", " + g + ", " + b + ")";
}
}
+4
View File
@@ -9,6 +9,10 @@ export default function timeAgo(previous: Date) {
const now = new Date();
const elapsed : number = (now.getTime() - previous.getTime());
if(elapsed < 0){
return 'recently';
}
if (elapsed < msPerMinute) {
return Math.round(elapsed/1000) + ' seconds ago';
}
+1 -2
View File
@@ -1,4 +1,3 @@
export function randomRange(min:number, max:number) {
return Math.round(Math.random() * (max - min) + min);
}
}
+1
View File
@@ -212,4 +212,5 @@ model Suggestion {
createdAt DateTime @default(now())
name String?
content String
resolved Boolean @default(false)
}
+1
View File
@@ -228,6 +228,7 @@ model Suggestion {
createdAt DateTime @default(now())
name String?
content String
resolved Boolean @default(false)
@@allow('all', true)
}
+1
View File
@@ -110,6 +110,7 @@ export default defineConfig({
route("remove/checkouts/:containerid", "routes/admin/edit/remove/checkouts.$containerid.tsx");
route("remove/suggestion/:id", "routes/admin/edit/remove/suggestion.$id.tsx");
route("resolve/suggestion/:id", "routes/admin/edit/suggestion.$id.tsx");
route("manage/inventory", "routes/admin/manage/inventory.tsx");
route("manage/checkout", "routes/admin/manage/checkout.tsx");