Add graphs on stats page, suggestions improvements, only fetch on focus
This commit is contained in:
@@ -24,10 +24,4 @@ Then run the app in production mode:
|
|||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Todo
|
|
||||||
- Filtering
|
|
||||||
- Easy inventory updating for admin
|
|
||||||
- Mobile styling frontpage
|
|
||||||
- Undo checkout for admin
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function DrinkPage(arg:Arguments) {
|
|||||||
containers.map((container) => {
|
containers.map((container) => {
|
||||||
totalinventory += container.inventory;
|
totalinventory += container.inventory;
|
||||||
|
|
||||||
if((container.portions || 0) > 1){
|
if((container.portions || 0) > 1 && container.inventory > 0){
|
||||||
containerWithMultiplePortions = true;
|
containerWithMultiplePortions = true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -227,13 +227,13 @@ function DrinkPage(arg:Arguments) {
|
|||||||
) : ""}
|
) : ""}
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="mt-3 mb-3">
|
<Row className="mt-3 mb-3">
|
||||||
<Col col={12}>
|
<Col col={12}>
|
||||||
<h3>Checkouts</h3>
|
<h3>Checkouts</h3>
|
||||||
<ClientOnly fallback={<Fallback />}>
|
<ClientOnly fallback={<Fallback />}>
|
||||||
{() => <Chart options={chartOptions} data={arg.chartData} type={"bar"} />}
|
{() => <Chart options={chartOptions} data={arg.chartData} type={"bar"} />}
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{arg.recommendations.length > 0 ? (
|
{arg.recommendations.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Row className="mt-3 mb-3">
|
<Row className="mt-3 mb-3">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function generateDrinksChartData(id: number){
|
|||||||
// History of specific drink
|
// History of specific drink
|
||||||
const containersWithHistory = await dbe.container.findMany({
|
const containersWithHistory = await dbe.container.findMany({
|
||||||
where: {drink_id: id},
|
where: {drink_id: id},
|
||||||
include: {checkouts: {orderBy: {checkoutAt: "asc"}}}
|
include: {checkouts: {orderBy: {checkoutAt: "asc"}, where: {checkoutAt: {gte: dates[0]}}}}
|
||||||
})
|
})
|
||||||
|
|
||||||
var datasets : any = [];
|
var datasets : any = [];
|
||||||
@@ -88,21 +88,22 @@ export async function generateDrinksChartData(id: number){
|
|||||||
inventoryAfter[i] = totalInventory;
|
inventoryAfter[i] = totalInventory;
|
||||||
}
|
}
|
||||||
|
|
||||||
let color = random_rgba();
|
let [color1, color2] = random_rgba(2);
|
||||||
|
|
||||||
datasets.push({
|
datasets.push({
|
||||||
type: "line",
|
type: "line",
|
||||||
label: 'Inventory ' + volume(container.volume),
|
label: 'Inventory ' + volume(container.volume),
|
||||||
data: inventoryAfter,
|
data: inventoryAfter,
|
||||||
borderColor: color,
|
borderColor: color1,
|
||||||
backgroundColor: color,
|
backgroundColor: color1,
|
||||||
|
tension: 0.1
|
||||||
});
|
});
|
||||||
datasets.push({
|
datasets.push({
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
label: 'Checkouts ' + volume(container.volume),
|
label: 'Checkouts ' + volume(container.volume),
|
||||||
data: checkouts,
|
data: checkouts,
|
||||||
borderColor: color,
|
borderColor: color2,
|
||||||
backgroundColor: color,
|
backgroundColor: color2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useRouteError } from "@remix-run/react";
|
useRouteError } from "@remix-run/react";
|
||||||
|
|
||||||
import DrinkPage from "~/components/cards/drink.page";
|
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 { findIdFromSlug, findRecommendations, getMostCommonSection } from "~/models/drinks.server";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useRouteError } from "@remix-run/react";
|
useRouteError } from "@remix-run/react";
|
||||||
|
|
||||||
import DrinkPage from "~/components/cards/drink.page";
|
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 { findIdFromSlug, findRecommendations, getMostCommonSection } from "~/models/drinks.server";
|
||||||
import { enhance } from "~/utils/db.server";
|
import { enhance } from "~/utils/db.server";
|
||||||
|
|||||||
@@ -31,25 +31,50 @@ export default function InventoryLayout() {
|
|||||||
const [checkouts, setCheckouts] = useState(data.checkouts);
|
const [checkouts, setCheckouts] = useState(data.checkouts);
|
||||||
let fetcher = useFetcher();
|
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(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
|
|
||||||
fetcher.load("/resource/checkouts");
|
if(shouldFetch){
|
||||||
|
fetcher.load("/resource/checkouts");
|
||||||
|
|
||||||
if(fetcher.state === "idle"){
|
if(fetcher.state === "idle"){
|
||||||
if(!fetcher.data) return;
|
if(!fetcher.data) return;
|
||||||
|
|
||||||
let newCheckouts = Number(fetcher.data.checkouts);
|
let newCheckouts = Number(fetcher.data.checkouts);
|
||||||
if(newCheckouts != checkouts){
|
if(newCheckouts != checkouts){
|
||||||
|
|
||||||
setCheckouts(newCheckouts);
|
setCheckouts(newCheckouts);
|
||||||
|
|
||||||
if (revalidator.state === "idle") {
|
if (revalidator.state === "idle") {
|
||||||
revalidator.revalidate();
|
revalidator.revalidate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LoaderFunctionArgs, json } from "@remix-run/node";
|
import { LoaderFunctionArgs, json, redirect } from "@remix-run/node";
|
||||||
import { useLoaderData, useSearchParams } from "@remix-run/react";
|
import { Link, useLoaderData, useNavigate, useSearchParams } from "@remix-run/react";
|
||||||
import { enhance } from "@zenstackhq/runtime";
|
import { enhance } from "@zenstackhq/runtime";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Accordion, Button, Col, Container, Offcanvas, Row, useAccordionButton } from "react-bootstrap";
|
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 { sortDrinksFromSearch } from "~/models/drinks.sort.server";
|
||||||
|
|
||||||
import { db } from "~/utils/db.server";
|
import { db } from "~/utils/db.server";
|
||||||
|
import { randomRange } from "~/utils/random";
|
||||||
|
|
||||||
export const loader = async ({request} : LoaderFunctionArgs) => {
|
export const loader = async ({request} : LoaderFunctionArgs) => {
|
||||||
const dbe = enhance(db);
|
const dbe = enhance(db);
|
||||||
@@ -30,6 +31,8 @@ export const loader = async ({request} : LoaderFunctionArgs) => {
|
|||||||
export default function BeersRoute() {
|
export default function BeersRoute() {
|
||||||
const loadData = useLoaderData<typeof loader>();
|
const loadData = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
@@ -39,6 +42,12 @@ export default function BeersRoute() {
|
|||||||
|
|
||||||
const numResults = loadData.drinkResults.length;
|
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 (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Row className="mt-3">
|
<Row className="mt-3">
|
||||||
@@ -77,6 +86,15 @@ export default function BeersRoute() {
|
|||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</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>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useRouteError } from "@remix-run/react";
|
useRouteError } from "@remix-run/react";
|
||||||
|
|
||||||
import DrinkPage from "~/components/cards/drink.page";
|
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 { findIdFromSlug, findRecommendations, getMostCommonSection } from "~/models/drinks.server";
|
||||||
import { enhance } from "~/utils/db.server";
|
import { enhance } from "~/utils/db.server";
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ import { LoaderFunctionArgs, json } from "@remix-run/node";
|
|||||||
import { useLoaderData } from "@remix-run/react";
|
import { useLoaderData } from "@remix-run/react";
|
||||||
import { enhance } from "@zenstackhq/runtime";
|
import { enhance } from "@zenstackhq/runtime";
|
||||||
import { Badge, Button, Col, Container, Nav, Row, Tab, Table, Tabs } from "react-bootstrap";
|
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 { LinkContainer } from "react-router-bootstrap";
|
||||||
import { ClientOnly } from "remix-utils/client-only";
|
import { ClientOnly } from "remix-utils/client-only";
|
||||||
|
import { generate24HChartData, generateMonthChartData } from "~/models/history.chart.server";
|
||||||
import { ContainerWithDrink, containerTypeToString } from "~/models/types";
|
import { ContainerWithDrink, containerTypeToString } from "~/models/types";
|
||||||
import { volume } from "~/utils/conversions";
|
import { volume } from "~/utils/conversions";
|
||||||
import timeAgo from "~/utils/datetime";
|
import timeAgo from "~/utils/datetime";
|
||||||
|
import 'chart.js/auto';
|
||||||
|
|
||||||
|
|
||||||
import { db } from "~/utils/db.server";
|
import { db } from "~/utils/db.server";
|
||||||
|
import { generateBeerHistoryChartData, generateBeerInventoryChartData, generateTypeHistoryChartData, generateTypeInventoryChartData } from "~/models/type.chart.server";
|
||||||
|
|
||||||
function statsForContainers(containers : ContainerWithDrink[]){
|
function statsForContainers(containers : ContainerWithDrink[]){
|
||||||
var drinksInventory = 0;
|
var drinksInventory = 0;
|
||||||
@@ -56,7 +60,6 @@ function statsForContainers(containers : ContainerWithDrink[]){
|
|||||||
totalPrice = Math.round(totalPrice * 100) / 100;
|
totalPrice = Math.round(totalPrice * 100) / 100;
|
||||||
averageAlcoholPercentage = Math.round(averageAlcoholPercentage * 100) / 100;
|
averageAlcoholPercentage = Math.round(averageAlcoholPercentage * 100) / 100;
|
||||||
|
|
||||||
|
|
||||||
return {drinksInventory, beersInventory, wineInventory, sodaInventory, cocktailInventory, totalLiterAlcohol, averageAlcoholPercentage, totalPrice};
|
return {drinksInventory, beersInventory, wineInventory, sodaInventory, cocktailInventory, totalLiterAlcohol, averageAlcoholPercentage, totalPrice};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +83,7 @@ export const loader = async ({request} : LoaderFunctionArgs) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const statsLast24H = statsForContainers(containersLast24);
|
const statsLast24H = statsForContainers(containersLast24);
|
||||||
|
const chartData24H = await generate24HChartData();
|
||||||
|
|
||||||
// Checkout 24H statistics
|
// Checkout 24H statistics
|
||||||
let lastMonthDate = Date.now() - (31 * 24 * 60 * 60 * 1000);
|
let lastMonthDate = Date.now() - (31 * 24 * 60 * 60 * 1000);
|
||||||
@@ -93,10 +97,12 @@ export const loader = async ({request} : LoaderFunctionArgs) => {
|
|||||||
containersLastMonth.push(entry.container);
|
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 historyTotal = await dbe.history.findMany({include: {container: {include: {drink: true}}}});
|
||||||
|
const chartDataHistoryTotal = await generateTypeHistoryChartData();
|
||||||
|
const chartDataBeerHistoryTotal = await generateBeerHistoryChartData();
|
||||||
|
|
||||||
let containersTotal : ContainerWithDrink[] = [];
|
let containersTotal : ContainerWithDrink[] = [];
|
||||||
historyTotal.forEach((entry) => {
|
historyTotal.forEach((entry) => {
|
||||||
@@ -105,15 +111,83 @@ export const loader = async ({request} : LoaderFunctionArgs) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const statsTotal = statsForContainers(containersTotal);
|
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"}});
|
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() {
|
export default function StatsRoute() {
|
||||||
const loadData = useLoaderData<typeof loader>();
|
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 (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Row className="mt-3">
|
<Row className="mt-3">
|
||||||
@@ -171,6 +245,18 @@ export default function StatsRoute() {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</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>
|
||||||
<Tab eventKey="totalh" title="Total history">
|
<Tab eventKey="totalh" title="Total history">
|
||||||
<h2 className="mt-3">Total checkouts</h2>
|
<h2 className="mt-3">Total checkouts</h2>
|
||||||
@@ -224,6 +310,18 @@ export default function StatsRoute() {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</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>
|
||||||
<Tab eventKey="month" title="Last Month">
|
<Tab eventKey="month" title="Last Month">
|
||||||
<h2 className="mt-3">Last Month checkouts</h2>
|
<h2 className="mt-3">Last Month checkouts</h2>
|
||||||
@@ -277,6 +375,9 @@ export default function StatsRoute() {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
<ClientOnly fallback={<Fallback />}>
|
||||||
|
{() => <Chart options={barChartOptions} data={loadData.chartDataMonth} type={"bar"} />}
|
||||||
|
</ClientOnly>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="24h" title="Last 24H">
|
<Tab eventKey="24h" title="Last 24H">
|
||||||
<h2 className="mt-3">Last 24H checkouts</h2>
|
<h2 className="mt-3">Last 24H checkouts</h2>
|
||||||
@@ -330,6 +431,9 @@ export default function StatsRoute() {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
<ClientOnly fallback={<Fallback />}>
|
||||||
|
{() => <Chart options={barChartOptions} data={loadData.chartData24H} type={"bar"} />}
|
||||||
|
</ClientOnly>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -361,4 +465,8 @@ export default function StatsRoute() {
|
|||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Fallback() {
|
||||||
|
return <div>Generating Chart</div>;
|
||||||
}
|
}
|
||||||
@@ -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 { useLoaderData } from "@remix-run/react";
|
||||||
import { Button, Col, Container, Form, Row, Table } from "react-bootstrap";
|
import { Button, Col, Container, Form, Row, Table } from "react-bootstrap";
|
||||||
import { LinkContainer } from "react-router-bootstrap";
|
import { LinkContainer } from "react-router-bootstrap";
|
||||||
@@ -94,7 +94,10 @@ export default function SuggestionsRoute() {
|
|||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th>Suggestions</th>
|
<th>Suggestions</th>
|
||||||
{ loadData.isAdmin ? (
|
{ loadData.isAdmin ? (
|
||||||
<th>Delete</th>
|
<>
|
||||||
|
<th>Resolve</th>
|
||||||
|
<th>Delete</th>
|
||||||
|
</>
|
||||||
) : ""}
|
) : ""}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -103,15 +106,26 @@ export default function SuggestionsRoute() {
|
|||||||
<tr key={entry.id}>
|
<tr key={entry.id}>
|
||||||
<td>{entry.name ? (<>{entry.name}</>) : ( <><i>No name</i></> ) }</td>
|
<td>{entry.name ? (<>{entry.name}</>) : ( <><i>No name</i></> ) }</td>
|
||||||
<td>{timeAgo(new Date(entry.createdAt))}</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 ? (
|
{ loadData.isAdmin ? (
|
||||||
|
<>
|
||||||
|
<td>
|
||||||
|
{!entry.resolved ? (
|
||||||
|
<LinkContainer to={"/admin/resolve/suggestion/" + entry.id}>
|
||||||
|
<Button variant="success">
|
||||||
|
Resolve
|
||||||
|
</Button>
|
||||||
|
</LinkContainer>
|
||||||
|
): ""}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<LinkContainer to={"/admin/remove/suggestion/" + entry.id}>
|
<LinkContainer to={"/admin/remove/suggestion/" + entry.id}>
|
||||||
<Button variant="danger">
|
<Button variant="danger">
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
</td>
|
</td>
|
||||||
|
</>
|
||||||
) : ""}
|
) : ""}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useRouteError } from "@remix-run/react";
|
useRouteError } from "@remix-run/react";
|
||||||
|
|
||||||
import DrinkPage from "~/components/cards/drink.page";
|
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 { findIdFromSlug, findRecommendations, getMostCommonSection } from "~/models/drinks.server";
|
||||||
import { enhance } from "~/utils/db.server";
|
import { enhance } from "~/utils/db.server";
|
||||||
|
|||||||
+26
-3
@@ -1,4 +1,27 @@
|
|||||||
export function random_rgba() {
|
export function random_rgba(numShades : number) {
|
||||||
var o = Math.round, r = Math.random, s = 205, m=50;
|
let o = Math.round, r = Math.random, s = 155, m = 100;
|
||||||
return 'rgba(' + o(r()*s + m) + ',' + o(r()*s + m) + ',' + o(r()*s + m) + ',' + 1 + ')';
|
|
||||||
|
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 + ")";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,10 @@ export default function timeAgo(previous: Date) {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const elapsed : number = (now.getTime() - previous.getTime());
|
const elapsed : number = (now.getTime() - previous.getTime());
|
||||||
|
|
||||||
|
if(elapsed < 0){
|
||||||
|
return 'recently';
|
||||||
|
}
|
||||||
|
|
||||||
if (elapsed < msPerMinute) {
|
if (elapsed < msPerMinute) {
|
||||||
return Math.round(elapsed/1000) + ' seconds ago';
|
return Math.round(elapsed/1000) + ' seconds ago';
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -1,4 +1,3 @@
|
|||||||
export function randomRange(min:number, max:number) {
|
export function randomRange(min:number, max:number) {
|
||||||
return Math.round(Math.random() * (max - min) + min);
|
return Math.round(Math.random() * (max - min) + min);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,4 +212,5 @@ model Suggestion {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
name String?
|
name String?
|
||||||
content String
|
content String
|
||||||
|
resolved Boolean @default(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ model Suggestion {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
name String?
|
name String?
|
||||||
content String
|
content String
|
||||||
|
resolved Boolean @default(false)
|
||||||
|
|
||||||
@@allow('all', true)
|
@@allow('all', true)
|
||||||
}
|
}
|
||||||
@@ -110,6 +110,7 @@ export default defineConfig({
|
|||||||
route("remove/checkouts/:containerid", "routes/admin/edit/remove/checkouts.$containerid.tsx");
|
route("remove/checkouts/:containerid", "routes/admin/edit/remove/checkouts.$containerid.tsx");
|
||||||
|
|
||||||
route("remove/suggestion/:id", "routes/admin/edit/remove/suggestion.$id.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/inventory", "routes/admin/manage/inventory.tsx");
|
||||||
route("manage/checkout", "routes/admin/manage/checkout.tsx");
|
route("manage/checkout", "routes/admin/manage/checkout.tsx");
|
||||||
|
|||||||
Reference in New Issue
Block a user