Add project files.

This commit is contained in:
Tea 2024-03-05 11:44:30 +01:00
commit 86a57f81f8
52 changed files with 4823 additions and 0 deletions

8
.dockerignore Normal file
View file

@ -0,0 +1,8 @@
# directories
**/bin/
**/obj/
**/out/
# files
Dockerfile*
**/*.md

63
.gitattributes vendored Normal file
View file

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

363
.gitignore vendored Normal file
View file

@ -0,0 +1,363 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd

22
Dockerfile Normal file
View file

@ -0,0 +1,22 @@
# Learn about building .NET container images:
# https://github.com/dotnet/dotnet-docker/blob/main/samples/README.md
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG TARGETARCH
WORKDIR /source
# copy csproj and restore as distinct layers
COPY PizzaBot/*.csproj .
RUN dotnet restore -a $TARGETARCH
# copy and publish app and libraries
COPY PizzaBot/. .
RUN dotnet publish -a $TARGETARCH --no-restore -o /app
# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
EXPOSE 8080
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
ENTRYPOINT ["./PizzaBot"]

27
PizzaBot.sln Normal file
View file

@ -0,0 +1,27 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34408.163
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PizzaBot", "PizzaBot\PizzaBot.csproj", "{74F20D7A-6724-4A49-9EC2-61461D5193BC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BF3D050-0F18-4862-95CD-24C01C035F63}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{74F20D7A-6724-4A49-9EC2-61461D5193BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{74F20D7A-6724-4A49-9EC2-61461D5193BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{74F20D7A-6724-4A49-9EC2-61461D5193BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{74F20D7A-6724-4A49-9EC2-61461D5193BC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E7F8AF82-C1E7-4F8A-98D1-5C5B3A486F04}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="PizzaBot.styles.css" />
<link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css">
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body style="min-height:100svh">
<Routes />
<script src="_framework/blazor.web.js"></script>
<script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>
</body>
</html>

View file

@ -0,0 +1,11 @@
@inject GlobalStuffService GlobalStuffService
@if (GlobalStuffService.OrdersLocked)
{
<p style="font-size: 1.2em; color: red;"> @GlobalStuffService.LOCKED_ORDERS_MESSAGE </p>
}
@if (GlobalStuffService.Message.Length >= 2)
{
<p style="font-size: 1.5em; color: limegreen;"> @GlobalStuffService.Message </p>
}

View file

@ -0,0 +1,15 @@
<div style="margin:5px">
<span>Help</span>
<a href="/help" class="btn btn-primary" style="height:2rem; aspect-ratio:1; padding: 0.25rem;">
<div class="help-button" />
</a>
</div>
<style>
.help-button {
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="whitesmoke" class="bi bi-question-lg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 0 1-.5.5h-.77a.5.5 0 0 1-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777ZM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14"/></svg>');
background-size: contain;
height: 100%;
aspect-ratio: 1;
}
</style>

View file

@ -0,0 +1,36 @@
@using System.Text.RegularExpressions
@inherits InputBase<float>
<input type="range" @attributes=AdditionalAttributes class=@CssClass @bind=CurrentValueAsString />
@code{
protected override string FormatValueAsString(float value)
=> value.ToString("F5");
static Regex decimalNumberRegex = new Regex(@"^-?\d+(\.\d+)?$", RegexOptions.Compiled);
protected override bool TryParseValueFromString(string value, out float result, out string validationErrorMessage)
{
Match match = decimalNumberRegex.Match(value);
if (!match.Success)
{
validationErrorMessage = "Not a valid float";
result = 0.0f;
return false;
}
if(float.TryParse(value, out float parsingResult))
{
result = parsingResult;
validationErrorMessage = null;
return true;
}
validationErrorMessage = "Not a valid float";
result = 0.0f;
return false;
}
}

View file

@ -0,0 +1,21 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<article class="content px-4">
<GlobalMessages />
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

View file

@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View file

@ -0,0 +1,30 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">🍕 PizzaBot 2000</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="orderlist">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Pizza Order List
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="order">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> New Pizza Order
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="help">
<span class="bi bi-help-nav-menu" aria-hidden="true"></span> How does this work?
</NavLink>
</div>
</nav>
</div>

View file

@ -0,0 +1,109 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.bi-help-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-question-lg' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 0 1-.5.5h-.77a.5.5 0 0 1-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777ZM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View file

@ -0,0 +1,15 @@
<div style="margin:5px">
<span>Order</span>
<a href="/order" class="btn btn-primary" style="height:2rem; aspect-ratio:1; padding: 0.25rem;">
<div class=" order-button" />
</a>
</div>
<style>
.order-button {
background-image: url('data:image/svg+xml, <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="whitesmoke" class="bi bi-plus-square-fill" viewBox="0 0 16 16"><path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0" /></svg>');
background-size: contain;
height: 100%;
aspect-ratio: 1;
}
</style>

View file

@ -0,0 +1,164 @@
@page "/admin{secretpath}"
@rendermode InteractiveServer
@inject GlobalStuffService GlobalStuffService
@inject PizzaDBService PizzaDBService
@inject NavigationManager NavigationManager
<PageTitle>Pizza Admin</PageTitle>
<TotalPizzasDisplay />
<h3>Hello, you are my GOD!</h3>
<h4>Order Locking</h4>
<button @onclick="ToggleOrdersLocked" class="btn btn-danger">
@if (GlobalStuffService.OrdersLocked)
{
<span>Unlock Orders</span>
}
else
{
<span>Lock Orders</span>
}
</button>
<h4>Global Message</h4>
<InputText @bind-Value=GlobalMessage id="Message" />
<button @onclick="SetGlobalMessage">
Confirm Message
</button>
@if (GlobalStuffService.OrdersLocked)
{
<h4>Payment</h4>
<a href="/admin@(secretpath)/pay"><div class="btn btn-primary">Go to payment screen</div></a>
}
<h4>Delete Orders</h4>
@if (HasDeletedEverything)
{
<p style="color: lime">Deleted everything</p>
}
@if (WrongDeletionPasscode)
{
<p style="color: red">Wrong passphrase</p>
}
<label for="Passphrase">Deletion Passphrase</label>
<InputText @bind-Value=DeletionPasscode id="Passphrase" class="form-control" />
<button @onclick="() => {DeleteOrderConfirmation = true;}" class="btn btn-danger">
DELETE ALL ORDERS
</button>
@if (DeleteOrderConfirmation)
{
<button @onclick="DeleteAllOrders" class="btn btn-danger">
ARE YOU SURE?
</button>
}
<h4>Configuration</h4>
<EditForm Model="@PConfig" OnValidSubmit="@SaveNewPizzaConfig" FormName="PizzaConfigForm">
<div class="form-group">
<label for="SizeX">Width of Pizza</label>
<InputNumber @bind-Value=PConfig.SizeX id="SizeX" class="form-control" ParsingErrorMessage="Must be float value" />
<ValidationMessage For="() => PConfig.SizeX " />
</div>
<div class="form-group">
<label for="SizeY">Height of Pizza</label>
<InputNumber @bind-Value=PConfig.SizeY id="SizeY" class="form-control" ParsingErrorMessage="Must be float value" />
<ValidationMessage For="() => PConfig.SizeY " />
</div>
<div class="form-group">
<label for="Price">Price of Pizza in Cents</label>
<InputNumber @bind-Value=PConfig.Price id="Price" class="form-control" ParsingErrorMessage="Must be integer value" />
<ValidationMessage For="() => PConfig.Price " />
</div>
<div class="form-group">
<label for="Pieces">Pieces per Pizza</label>
<InputNumber @bind-Value=PConfig.Fragments id="Pieces" class="form-control" ParsingErrorMessage="Must be integer value" />
<ValidationMessage For="() => PConfig.Fragments " />
</div>
<div class="form-group">
<label for="Toppings">Number of allowed toppings per pizza</label>
<InputNumber @bind-Value=PConfig.Toppings id="Toppings" class="form-control" ParsingErrorMessage="Must be integer value" />
<ValidationMessage For="() => PConfig.Toppings " />
</div>
<div class="form-group">
<label for"MaxNameLength">Maximum length of names</label>
<InputNumber @bind-Value=PConfig.NameLength id="MaxNameLength" class="form-control" ParsingErrorMessage="Must be integer value" />
<ValidationMessage For="() => PConfig.NameLength " />
</div>
<div class="form-group">
<label for="PenaltyType">Penalty Type used for balancing</label>
<InputSelect @bind-Value=PConfig.PenaltyType id="PenaltyType" class="form-control">
<option value="@Models.PenaltyType.Tuxic">Tuxic Penalty (Original)</option>
<option value="@Models.PenaltyType.PfeifferTreimer">Pfeiffer-Treimer Penalty (Default)</option>
<option value="@Models.PenaltyType.PfeifferTreimerLockedDown">Pfeiffer-Treimer Penalty (Clamped)</option>
</InputSelect>
</div>
<input type="submit" value="Submit" class="btn btn-primary" />
</EditForm>
<style>
h4 {
margin-top: 2rem !important;
}
</style>
@code {
PizzaConfig PConfig = new PizzaConfig();
string GlobalMessage = "";
[Parameter]
public string secretpath { get; set; }
protected override void OnInitialized()
{
if (secretpath != Environment.GetEnvironmentVariable("ADMIN_PATH"))
{
NavigationManager.NavigateTo("/"); return;
}
GlobalMessage = GlobalStuffService.Message;
}
bool DeleteOrderConfirmation = false;
bool HasDeletedEverything = false;
bool WrongDeletionPasscode = false;
string DeletionPasscode = "";
void SaveNewPizzaConfig(EditContext editContext)
{
GlobalStuffService.SetConfig(PConfig);
GlobalStuffService.ShouldBalance = true;
}
void ToggleOrdersLocked()
{
GlobalStuffService.SetOrdersLocked(!GlobalStuffService.OrdersLocked);
}
void SetGlobalMessage()
{
GlobalStuffService.SetMessage(GlobalMessage);
}
void DeleteAllOrders()
{
HasDeletedEverything = PizzaDBService.DeleteAllOrders(DeletionPasscode);
WrongDeletionPasscode = !HasDeletedEverything;
DeleteOrderConfirmation = false;
}
protected override async Task OnInitializedAsync()
{
PConfig = GlobalStuffService.GetConfig() ?? new PizzaConfig();
}
}

View file

@ -0,0 +1,125 @@
@page "/admin{secretpath}/pay"
@using System.Globalization
@rendermode InteractiveServer
@inject PizzaDBService PizzaDBService
@inject NavigationManager NavigationManager
<PageTitle>PizzaPay</PageTitle>
<h3>AdminPay</h3>
<div class="pizza-order-grid">
<div class="right-border">Paid?</div>
<div class="right-border">Name</div>
<div class="right-border">Price</div>
<div style="text-align:center; grid-column-start: span 3;">Pieces</div>
<div class="right-border"></div>
<div class="right-border"></div>
<div class="right-border"></div>
<div class="right-border">🍖</div>
<div class="right-border">🍄+🧀</div>
<div class="right-border">🌽</div>
@foreach (PizzaRequest request in PizzaDBService.GetAllRequests())
{
PizzaResult? result = PizzaDBService.GetResultById(request.Id);
if (result == null)
{
<p class="request error" style="color: red; grid-column-start: span @gridwidth;">ERROR: Result of request with name @request.Name not found!</p>
}
else
{
<div class="request">
@if (result.hasPaid)
{
<button @onclick="() => MarkAsUnpaid(result.Id)">
</button>
}
else
{
<button @onclick="() => MarkAsPaid(result.Id)">
</button>
}
</div>
<div class="request">@request.Name</div>
<div class="request">@totalCostToString(result.totalCost)</div>
<div class="request">@result.resPiecesMeat / @request.reqPiecesMeat</div>
<div class="request">@result.resPiecesVegetarian / @request.reqPiecesVegetarian</div>
<div class="request">@result.resPiecesVegan / @request.reqPiecesVegan</div>
}
}
</div>
<style>
.pizza-order-grid {
display: grid;
grid-template-columns: 1fr 25vw repeat(@(gridwidth - 2), 1fr);
grid-auto-rows: auto;
}
.pizza-order-grid > * {
word-break: break-all;
overflow: auto;
padding: 5px;
}
.pizza-order-grid > .request {
border-top: solid 1px gray;
font-size: 0.8em;
}
.pizza-order-grid > .request:not(.price):not(.error),
.pizza-order-grid > .right-border {
border-right: solid 1px gray;
}
.request button {
width: 2.5em;
aspect-ratio: 1;
text-align: center;
font-size: 1em;
padding: 0;
background: none;
outline: none;
appearance:none;
-webkit-appearance: none;
-moz-appearance: none;
border: none;
}
</style>
@code {
[Parameter]
public string secretpath { get; set; }
protected override void OnInitialized()
{
if (secretpath != Environment.GetEnvironmentVariable("ADMIN_PATH"))
{
NavigationManager.NavigateTo("/"); return;
}
}
int gridwidth = 6;
string totalCostToString(float totalCost)
{
return totalCost.ToString("C2", CultureInfo.CreateSpecificCulture("de-DE"));
}
public void MarkAsPaid(int id)
{
PizzaDBService.MarkAsPaid(id);
}
public void MarkAsUnpaid(int id)
{
PizzaDBService.MarkAsNotPaid(id);
}
}

View file

@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View file

@ -0,0 +1,33 @@
@page "/help"
@inject GlobalStuffService GlobalStuffService
<PageTitle>Pizza Help</PageTitle>
<h3>How To Pizza</h3>
<p>You can choose any number of slices from any of the three categories of pizzas - meat, vegetarian and vegan.</p>
<p>Each pizza of size @((_config.SizeX * 100).ToString("F0"))cm X @(_config.SizeY * 100)cm is cut into @(_config.Fragments) pieces of approximately @GlobalStuffService.GetSizeOfSliceInCM2().ToString("F0")cm<sup>2</sup>.</p>
<p>
The system calculates how many pizzas should be ordered and distributes slices according to the pieces you request and the priority you set.
This means, you might get more or less slices than you requested, or slices from a different category than you requested.
The system tries to respect your wishes. If you are not happy with the outcome, try changing your priority.
</p>
<p>In the list, you can see your order: <i> Name, Assigned pieces / Requested pieces (for each category), Price </i></p>
<p>
🍖 = Meat <br />
🍄🧀 = Vegetarian <br />
🌽 = Vegan
</p>
@code {
PizzaConfig _config;
protected override void OnInitialized()
{
_config = GlobalStuffService.GetConfig();
if(_config == null)
{
_config = new PizzaConfig();
}
}
}

View file

@ -0,0 +1,118 @@
@page "/order"
@rendermode InteractiveServer
@inherits LayoutComponentBase
@inject PizzaDBService PizzaDBService
@inject GlobalStuffService GlobalStuffService
@inject Microsoft.AspNetCore.Components.NavigationManager NavigationManager
<PageTitle> Order Pizza </PageTitle>
@if (errorEncountered)
{
<p style="color: red; font-weight: 700;"> @ErrorMessage </p>
}
@if (submissionSuccessful)
{
<p style="color: lawngreen; font-weight: 700;"> @SuccessfulSubmissionMessage </p>
}
<EditForm Model=@Order OnValidSubmit="@FormSubmitted" FormName="PizzaOrderForm">
<DataAnnotationsValidator />
<div class="form-group">
<label for="Name">Name</label><span style="font-size:0.7em; color:dimgrey; margin:0;">must be unique</span>
<InputText @bind-Value=Order.Name id="Name" class="form-control" />
<ValidationMessage For="() => Order.Name" />
</div>
<label for="PiecesGroup" style="margin-top: 1em;">Pieces</label>
<span style="font-size: 0.7em;">One slice = ca. @GlobalStuffService.GetSizeOfSliceInCM2().ToString("F0") cm<sup>2</sup></span>
<div class="row" id="PiecesGroup">
<div class="form-group col">
<label for="meatPieces">🍖</label>
<InputNumber @bind-Value=Order.reqPiecesMeat class="form-control" id="meatPieces" ParsingErrorMessage="Must be integer value" />
<ValidationMessage For="() => Order.reqPiecesMeat" />
</div>
<div class="form-group col">
<label for="vegetarianPieces">🍄 + 🧀</label>
<InputNumber @bind-Value=Order.reqPiecesVegetarian class="form-control" id="vegetarianPieces" ParsingErrorMessage="Must be integer value" />
<ValidationMessage For="() => Order.reqPiecesVegetarian" />
</div>
<div class="form-group col">
<label for="veganPieces">🌽</label>
<InputNumber @bind-Value=Order.reqPiecesVegan class="form-control" id="veganPieces" ParsingErrorMessage="Must be integer value" />
<ValidationMessage For="() => Order.reqPiecesVegan" />
</div>
</div>
<div class="form-group" style="margin-top: 3rem;">
<label for="priorityMeat" style="text-align:center;"> Priority </label>
<p style="text-align:center; font-size: 0.8em; margin: 0;">
The balancing algorithm tries to avoid changes of the corresponding Variable <br />
<i style="font-size: 0.8em;">E.g. If you want to only get pieces from your chosen category put the slider all the way to the left</i></p>
<div class="priority-slider-container">
<div> Category </div>
<RadzenSlider @bind-Value=Order.priority TValue="float" Min="0" Max="1" Step="0.01" id="priorityMeat" />
<div> Total Count</div>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</EditForm>
<style>
label {
display: block;
}
#priorityMeat,
#priorityVegan {
--rz-slider-horizontal-width: 100%;
margin: 1em 0;
}
.pizza-piece-form-group {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 3px;
margin: 2rem 3px;
}
.priority-slider-container{
padding-bottom: 1rem;
display: flex;
flex-direction:row;
align-items: center;
gap: 1em;
font-size: 0.75em;
}
</style>
@code {
bool errorEncountered = false;
string ErrorMessage = "";
bool submissionSuccessful = false;
string SuccessfulSubmissionMessage = "Request submitted successfully";
PizzaRequest Order = new PizzaRequest();
// Random rnd = new Random();
void FormSubmitted(EditContext editContext)
{
if (PizzaDBService.Create(Order, out ErrorMessage) == null)
{
errorEncountered = true;
submissionSuccessful = false;
return;
}
errorEncountered = false;
submissionSuccessful = true;
Order = new PizzaRequest();
NavigationManager.NavigateTo("/orderlist");
}
}

View file

@ -0,0 +1,198 @@
@page "/order/edit/{id:int}"
@rendermode InteractiveServer
@inherits LayoutComponentBase
@inject PizzaDBService pizzaDBService
@inject NavigationManager Navigation
@inject GlobalStuffService GlobalStuffService
<PageTitle> Change Pizza Order </PageTitle>
@if (errorEncountered)
{
<p style="color: red; font-weight: 700;"> @ErrorMessage </p>
}
@if (submissionSuccessful)
{
<p style="color: lawngreen; font-weight: 700;"> @SuccessfulSubmissionMessage </p>
}
<EditForm Model=@Order OnValidSubmit="@FormSubmitted" FormName="PizzaOrderForm">
<DataAnnotationsValidator />
<h3>
@Order.Name
</h3>
<label for="PiecesGroup" style="margin-top: 1em;">Pieces</label>
<span style="font-size: 0.7em;">One slice = ca. @GlobalStuffService.GetSizeOfSliceInCM2().ToString("F0") cm<sup>2</sup></span>
<div class="row" id="PiecesGroup">
<div class="form-group col">
<label for="meatPieces">🍖</label>
<InputNumber @bind-Value=Order.reqPiecesMeat class="form-control" id="meatPieces" ParsingErrorMessage="Must be integer value" />
<ValidationMessage For="() => Order.reqPiecesMeat" />
</div>
<div class="form-group col">
<label for="vegetarianPieces">🍄 + 🧀</label>
<InputNumber @bind-Value=Order.reqPiecesVegetarian class="form-control" id="vegetarianPieces" ParsingErrorMessage="Must be integer value" />
<ValidationMessage For="() => Order.reqPiecesVegetarian" />
</div>
<div class="form-group col">
<label for="veganPieces">🌽</label>
<InputNumber @bind-Value=Order.reqPiecesVegan class="form-control" id="veganPieces" ParsingErrorMessage="Must be integer value" />
<ValidationMessage For="() => Order.reqPiecesVegan" />
</div>
</div>
<div class="form-group" style="margin-top: 3rem;">
<label for="priorityMeat" style="text-align:center;"> Priority </label>
<p style="text-align:center; font-size: 0.8em; margin: 0;">
The balancing algorithm tries to avoid changes of the corresponding Variable <br />
<i style="font-size: 0.8em;">E.g. If you want to only get pieces from your chosen category put the slider all the way to the left</i>
</p>
<div class="priority-slider-container">
<div> Category </div>
<RadzenSlider @bind-Value=Order.priority TValue="float" Min="0" Max="1" Step="0.01" id="priorityMeat" />
<div> Total Count</div>
</div>
</div>
<input type="submit" value="💾 Save Changes 💾" class="btn btn-primary" />
</EditForm>
<div class="deletion-container">
@if (!showDeletionConfirmation)
{
<button @onclick="ShowDeleteConfirmation"> 🗑 Delete </button>
}
else
{
<p>Are you sure you want to delete this order? <br /></p>
<div class="confirmation-buttons">
<button @onclick="DeleteOrder">🗑 Delete</button>
<button @onclick="() => showDeletionConfirmation = false">Cancel</button>
</div>
}
</div>
<style>
label {
display: block;
}
#priorityMeat,
#priorityVegan {
--rz-slider-horizontal-width: 80%;
margin: 1em 0;
}
.pizza-piece-form-group {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 3px;
margin: 2rem 3px;
}
.deletion-container {
margin: 2rem 0;
background-color: red;
width: fit-content;
padding: 0.5rem;
color: whitesmoke;
font-weight: 700;
border-radius: 0.25rem;
}
.confirmation-buttons {
display: flex;
align-items: center;
justify-content: center;
}
.deletion-container .confirmation-buttons button {
border: solid 0.1rem whitesmoke;
border-radius: 0.25rem;
padding: 0.5rem;
margin: 0.5rem;
}
.deletion-container button {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
border: none;
margin: 0;
padding: 0.3rem;
background: none;
cursor: pointer;
outline: none;
color: whitesmoke;
font-weight: 700;
}
</style>
@code {
bool showDeletionConfirmation = false;
private void ShowDeleteConfirmation()
{
// Display the confirmation modal
showDeletionConfirmation = true;
}
[Parameter]
public int id { get; set; }
bool errorEncountered = false;
string ErrorMessage = "";
bool submissionSuccessful = false;
string SuccessfulSubmissionMessage = "Request submitted successfully";
PizzaRequest Order = new PizzaRequest();
// Random rnd = new Random();
protected override void OnInitialized()
{
Order = pizzaDBService.GetRequestById(id);
if(Order == null)
{
Navigation.NavigateTo("/orderlist", true);
}
}
void FormSubmitted(EditContext editContext)
{
if (!pizzaDBService.UpdateRequest(Order, out ErrorMessage))
{
errorEncountered = true;
submissionSuccessful = false;
return;
}
errorEncountered = false;
submissionSuccessful = true;
Order = pizzaDBService.GetRequestById(id);
// if(Order.reqPiecesMeat + Order.reqPiecesVegetarian + Order.reqPiecesVegan < 1)
// {
// submittedZeroPieces = true;
// submissionSuccessful = false;
// return;
// }
// submittedZeroPieces = false;
// submissionSuccessful = true;
// Status = "Form Submitted";
// Order.Id = rnd.Next(int.MaxValue);
// OrderContext.Requests.Add(Order);
// OrderContext.SaveChanges();
}
void DeleteOrder()
{
pizzaDBService.DeleteById(id);
Navigation.NavigateTo("/orderlist");
}
}

View file

@ -0,0 +1,121 @@
@page "/orderlist"
@page "/"
@using System.Globalization
@inject PizzaDBService PizzaDBService
@inject Microsoft.AspNetCore.Components.NavigationManager navigationManager
@inject GlobalStuffService GlobalStuffService
@attribute [StreamRendering]
<PageTitle>Pizza List</PageTitle>
<div style="display: flex; ">
<TotalPizzasDisplay />
<div style ="display: flex; flex-direction:column; margin-left:auto; align-items:flex-end;">
<OrderButton />
<HelpButton />
</div>
</div>
<h3>Pizza Order List</h3>
<div class="pizza-order-grid">
<div class="right-border">Name</div>
<div class="right-border" style="grid-column-start: span 3;">Pieces</div>
<div class ="right-border">Price</div>
@if (GlobalStuffService.OrdersLocked)
{
<div>Paid?</div>
}
else
{
<div></div>
}
<div class="right-border" style="grid-area: 2 / 1 / 2 / 1"></div>
<div class="right-border" style="font-size: 0.7em;">Meat</div>
<div class="right-border" style="font-size: 0.6em;">Veggie</div>
<div class="right-border" style="font-size: 0.7em;">Vegan</div>
<div class="right-border"></div>
<div style="grid-column-start: span @(gridwidth - 5);"></div>
<div class="right-border" style="grid-area: 3 / 1 / 3 / 1"></div>
<div class="right-border">🍖</div>
<div class="right-border">🍄🧀</div>
<div class="right-border">🌽</div>
<div class="right-border"></div>
<div style="grid-column-start: span @(gridwidth - 5);"></div>
@foreach (PizzaRequest request in PizzaDBService.GetAllRequests())
{
PizzaResult? result = PizzaDBService.GetResultById(request.Id);
if (result == null)
{
<p class="request error" style="color: red; grid-column-start: span @gridwidth;">ERROR: Result of request with name @request.Name not found!</p>
}
else
{
<div class="request right-border">@request.Name</div>
<div class="request right-border">@result.resPiecesMeat<span style="font-size:0.8em; color:dimgrey;">/ @request.reqPiecesMeat</span></div>
<div class="request right-border">@result.resPiecesVegetarian<span style="font-size:0.8em; color:dimgrey;">/ @request.reqPiecesVegetarian</span></div>
<div class="request right-border">@result.resPiecesVegan<span style="font-size:0.8em; color:dimgrey;">/ @request.reqPiecesVegan</span></div>
<div class="request price right-border">@totalCostToString(result.totalCost)</div>
@if (GlobalStuffService.OrdersLocked)
{
if (result.hasPaid)
{
<div class="request">
</div>
}
else
{
<div class="request">
</div>
}
}
else
{
<div class="request"><a href="/order/edit/@request.Id"><button class="btn btn-info" style="width: 2em; height: 2em; padding: 0; text-align:center;">🔧</button></a></div>
}
}
}
</div>
<div style="height:50px;" />
<style>
.pizza-order-grid {
display: grid;
grid-template-columns: 25vw repeat(@(gridwidth - 2), 1fr) 3em;
grid-auto-rows: auto;
}
.pizza-order-grid > * {
word-break: break-all;
overflow: auto;
padding: 5px;
}
.pizza-order-grid > .request {
border-top: solid 1px gray;
font-size: 0.8em;
}
.pizza-order-grid > .right-border:not(.error) {
border-right: solid 1px gray;
}
</style>
@code {
int gridwidth = 6;
string totalCostToString(float totalCost)
{
return totalCost.ToString("C2", CultureInfo.CreateSpecificCulture("de-DE"));
}
private void NavigateToEdit(int id)
{
navigationManager.NavigateTo($"/order/edit/{id}");
}
}

View file

@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View file

@ -0,0 +1,17 @@
@inject GlobalStuffService GlobalStuffService
<p>
<span>🍖: @GlobalStuffService.MeatPizzas</span>
<span>🍄 + 🧀: @GlobalStuffService.VeggiePizzas</span>
<span>🌽: @GlobalStuffService.VeganPizzas</span>
<span>=> @GlobalStuffService.TotalCost.ToString("F2")€</span></p>
<style>
p{
font-size: 0.6em;
color: gray;
}
span{
padding-inline: 0.5em;
}
</style>

View file

@ -0,0 +1,14 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using PizzaBot
@using PizzaBot.Components
@using Radzen
@using Radzen.Blazor
@using PizzaBot.Models
@using PizzaBot.Services

31
PizzaBot/Dockerfile Normal file
View file

@ -0,0 +1,31 @@
FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal-amd64 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
ENV DELETION_PASSPHRASE=""
ENV DATABASE_URL=""
ENV DATABASE_USERNAME=""
ENV DATABASE_PASSWD=""
ENV DATABASE_NAME=""
ENV ADMIN_PATH=""
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /source
COPY PizzaBot.csproj .
RUN dotnet restore "PizzaBot.csproj"
# Copy everything
COPY . .
# Build and publish a release
RUN dotnet build "Pizzabot.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Pizzabot.csproj" -c Release -o /app/publish
# Build runtime image
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Pizzabot.dll"]

View file

@ -0,0 +1,85 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PizzaBot.Models;
#nullable disable
namespace PizzaBot.Migrations
{
[DbContext(typeof(PizzaContext))]
[Migration("20240126122625_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("PizzaBot.Models.PizzaRequest", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<float>("priority")
.HasColumnType("float");
b.Property<int>("reqPiecesMeat")
.HasColumnType("int");
b.Property<int>("reqPiecesVegan")
.HasColumnType("int");
b.Property<int>("reqPiecesVegetarian")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Requests");
});
modelBuilder.Entity("PizzaBot.Models.PizzaResult", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<bool>("hasPaid")
.HasColumnType("tinyint(1)");
b.Property<float>("penaltyMeatVeggi")
.HasColumnType("float");
b.Property<float>("penaltyVeggieVegan")
.HasColumnType("float");
b.Property<int>("resPiecesMeat")
.HasColumnType("int");
b.Property<int>("resPiecesVegan")
.HasColumnType("int");
b.Property<int>("resPiecesVegetarian")
.HasColumnType("int");
b.Property<float>("totalCost")
.HasColumnType("float");
b.HasKey("Id");
b.ToTable("Results");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,67 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PizzaBot.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Requests",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
reqPiecesMeat = table.Column<int>(type: "int", nullable: false),
reqPiecesVegetarian = table.Column<int>(type: "int", nullable: false),
reqPiecesVegan = table.Column<int>(type: "int", nullable: false),
priority = table.Column<float>(type: "float", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Requests", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Results",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
resPiecesMeat = table.Column<int>(type: "int", nullable: false),
resPiecesVegetarian = table.Column<int>(type: "int", nullable: false),
resPiecesVegan = table.Column<int>(type: "int", nullable: false),
penaltyMeatVeggi = table.Column<float>(type: "float", nullable: false),
penaltyVeggieVegan = table.Column<float>(type: "float", nullable: false),
totalCost = table.Column<float>(type: "float", nullable: false),
hasPaid = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Results", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Requests");
migrationBuilder.DropTable(
name: "Results");
}
}
}

View file

@ -0,0 +1,82 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PizzaBot.Models;
#nullable disable
namespace PizzaBot.Migrations
{
[DbContext(typeof(PizzaContext))]
partial class PizzaContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("PizzaBot.Models.PizzaRequest", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<float>("priority")
.HasColumnType("float");
b.Property<int>("reqPiecesMeat")
.HasColumnType("int");
b.Property<int>("reqPiecesVegan")
.HasColumnType("int");
b.Property<int>("reqPiecesVegetarian")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Requests");
});
modelBuilder.Entity("PizzaBot.Models.PizzaResult", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<bool>("hasPaid")
.HasColumnType("tinyint(1)");
b.Property<float>("penaltyMeatVeggi")
.HasColumnType("float");
b.Property<float>("penaltyVeggieVegan")
.HasColumnType("float");
b.Property<int>("resPiecesMeat")
.HasColumnType("int");
b.Property<int>("resPiecesVegan")
.HasColumnType("int");
b.Property<int>("resPiecesVegetarian")
.HasColumnType("int");
b.Property<float>("totalCost")
.HasColumnType("float");
b.HasKey("Id");
b.ToTable("Results");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,53 @@
using System.ComponentModel.DataAnnotations;
namespace PizzaBot.Models
{
public enum PenaltyType
{
Tuxic = 0,
PfeifferTreimer = 1,
PfeifferTreimerLockedDown = 2
}
public class PizzaConfig
{
/// <summary>
/// size of pizza in m
/// </summary>
[Range(0.2f, 10)]
public float SizeX { get; set; }
[Range(0.2f, 10)]
public float SizeY { get; set; }
/// <summary>
/// price of a pizza in ct
/// </summary>
[Range(1000, 10000, ErrorMessage = "Price of a Pizza needs to be between 10€ and 100€")]
public int Price { get; set; }
/// <summary>
/// number of pieces per pizza
/// </summary>
[Range(8, 100, ErrorMessage = "Pieces per Pizza need to be between 8 and 100")]
public int Fragments { get; set; }
/// <summary>
/// amount of toppings a pizza can have
/// </summary>
[Range(1, 50, ErrorMessage = "Toppings per Pizza need to be between 1 and 50")]
public int Toppings { get; set; }
/// <summary>
/// max length of order names
/// </summary>
[Range(1, 500, ErrorMessage = "Name length needs to be between 1 and 500")]
public int NameLength { get; set; }
public PenaltyType PenaltyType { get; set; }
public PizzaConfig GetShallowCopy()
{
return (PizzaConfig)this.MemberwiseClone();
}
}
}

View file

@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
namespace PizzaBot.Models
{
public class PizzaContext : DbContext
{
public DbSet<PizzaRequest> Requests { get; set; }
public DbSet<PizzaResult> Results { get; set; }
public PizzaContext(DbContextOptions options) : base(options) { }
}
}

View file

@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
namespace PizzaBot.Models
{
public class PizzaRequest
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Range(0, 1000, ErrorMessage = "Pieces can't be negative or over 1000")]
public int reqPiecesMeat { get; set; }
[Range(0, 1000, ErrorMessage = "Pieces can't be negative or over 1000")]
public int reqPiecesVegetarian { get; set; }
[Range(0, 1000, ErrorMessage = "Pieces can't be negative or over 1000")]
public int reqPiecesVegan { get; set; }
[Range(0.0f, 1.0f)]
public float priority { get; set; } = 0.5f;
public PizzaRequest GetShallowCopy()
{
return (PizzaRequest)this.MemberwiseClone();
}
}
}

View file

@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
namespace PizzaBot.Models
{
public class PizzaResult
{
public int Id { get; set; }
[Range(0, 1000, ErrorMessage = "Pieces can't be negative or over 1000")]
public int resPiecesMeat { get; set; }
[Range(0, 1000, ErrorMessage = "Pieces can't be negative or over 1000")]
public int resPiecesVegetarian { get; set; }
[Range(0, 1000, ErrorMessage = "Pieces can't be negative or over 1000")]
public int resPiecesVegan { get; set; }
public float penaltyMeatVeggi { get; set; }
public float penaltyVeggieVegan { get; set; }
public float totalCost { get; set; }
public bool hasPaid { get; set; }
}
}

31
PizzaBot/PizzaBot.csproj Normal file
View file

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<None Remove="Components\Pages\PizzaOrder.razor.css" />
</ItemGroup>
<ItemGroup>
<Compile Include="Components\Pages\PizzaOrder.razor.css" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
<PackageReference Include="Radzen.Blazor" Version="4.24.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project>

41
PizzaBot/Program.cs Normal file
View file

@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore;
using PizzaBot.Components;
using PizzaBot.Models;
using PizzaBot.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
String connectionString = "server=" + Environment.GetEnvironmentVariable("DATABASE_URL") +
";uid=" + Environment.GetEnvironmentVariable("DATABASE_USERNAME") +
";pwd=" + Environment.GetEnvironmentVariable("DATABASE_PASSWD") +
";database=" + Environment.GetEnvironmentVariable("DATABASE_NAME");
builder.Services.AddDbContext<PizzaContext>(options => options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)), ServiceLifetime.Singleton, ServiceLifetime.Singleton);
builder.Services.AddSingleton<JSONService>();
builder.Services.AddSingleton<PizzaBalancingService>();
builder.Services.AddSingleton<PizzaDBService>();
builder.Services.AddSingleton<GlobalStuffService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();

View file

@ -0,0 +1,44 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5167"
},
"https": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DELETION_PASSPHRASE": "Lenny ist cool!",
"DATABASE_URL": "localhost",
"DATABASE_USERNAME": "PizzaBotDev",
"DATABASE_PASSWD": "PizzaDeliveryEverywhere!",
"DATABASE_NAME": "PizzaBotTest",
"ADMIN_PATH": "hades"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7032;http://localhost:5167"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:11491",
"sslPort": 44378
}
}
}

View file

@ -0,0 +1,68 @@
using PizzaBot.Models;
namespace PizzaBot.Services
{
public class GlobalStuffService
{
public string Message { get { return _message; }}
private string _message = " ";
public bool OrdersLocked { get { return _ordersLocked; } }
private bool _ordersLocked;
public readonly string LOCKED_ORDERS_MESSAGE = "Orders have been locked. Pizza will be ordered soon.";
public int MeatPizzas { get; set; }
public int VeggiePizzas { get; set; }
public int VeganPizzas { get; set; }
public float TotalCost { get; set; }
private PizzaConfig _pizzaConfig;
private JSONService _jsonService;
public bool ShouldBalance { get; set; } = true;
public GlobalStuffService(JSONService jSONService)
{
_jsonService = jSONService;
_pizzaConfig = _jsonService.ReadPizzaConfig();
}
public void SetMessage(string message)
{
_message = message;
}
public void SetOrdersLocked(bool ordersLocked)
{
_ordersLocked = ordersLocked;
ShouldBalance = true;
}
public PizzaConfig? GetConfig()
{
if(_pizzaConfig == null)
{
_pizzaConfig = _jsonService.ReadPizzaConfig();
if(_pizzaConfig == null)
{
return null;
}
}
return _pizzaConfig.GetShallowCopy();
}
public void SetConfig(PizzaConfig pizzaConfig)
{
_pizzaConfig = pizzaConfig;
_jsonService.WritePizzaConfig(pizzaConfig);
}
public float GetSizeOfSliceInCM2()
{
return ((_pizzaConfig.SizeX*100) * (_pizzaConfig.SizeY*100)) / _pizzaConfig.Fragments;
}
}
}

View file

@ -0,0 +1,38 @@
using PizzaBot.Models;
using System.Text.Json;
namespace PizzaBot.Services
{
public class JSONService
{
public IWebHostEnvironment WebHostEnvironment { get; }
const string CONFIG_DIRECTORY_PATH = "config";
const string PIZZA_CONFIG_FILENAME = "pizza.config";
public JSONService(IWebHostEnvironment webHostEnvironment)
{
WebHostEnvironment = webHostEnvironment;
}
public PizzaConfig? ReadPizzaConfig()
{
string path = Path.Combine(WebHostEnvironment.WebRootPath, CONFIG_DIRECTORY_PATH, PIZZA_CONFIG_FILENAME);
string jsonString = File.ReadAllText(path);
PizzaConfig? pizzaConfig = JsonSerializer.Deserialize<PizzaConfig>(jsonString);
return pizzaConfig;
}
public void WritePizzaConfig(PizzaConfig pizzaConfig)
{
string path = Path.Combine(WebHostEnvironment.WebRootPath, CONFIG_DIRECTORY_PATH, PIZZA_CONFIG_FILENAME);
string jsonString = JsonSerializer.Serialize(pizzaConfig);
File.WriteAllText(path, jsonString);
}
public string GetPizzaConfigPath()
{
return Path.Combine(WebHostEnvironment.WebRootPath, CONFIG_DIRECTORY_PATH, PIZZA_CONFIG_FILENAME);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,229 @@
using PizzaBot.Models;
using Microsoft.EntityFrameworkCore;
using System.Diagnostics.CodeAnalysis;
namespace PizzaBot.Services
{
class PizzaRequestNameEqualityComparer : IEqualityComparer<PizzaRequest>
{
public bool Equals(PizzaRequest? x, PizzaRequest? y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
if (x.Name == y.Name) return true;
return false;
}
public int GetHashCode([DisallowNull] PizzaRequest obj)
{
return obj.Name.GetHashCode();
}
}
public class PizzaDBService
{
private readonly PizzaContext _context;
private readonly PizzaBalancingService _balancingService;
private readonly GlobalStuffService _globalStuffService;
private PizzaRequestNameEqualityComparer _reqNameEqualityComparer = new PizzaRequestNameEqualityComparer();
private Random _rnd = new Random();
public PizzaDBService(PizzaContext context, PizzaBalancingService balancingService, GlobalStuffService globalStuffService)
{
_context = context;
_balancingService = balancingService;
_globalStuffService = globalStuffService;
}
public PizzaRequest? Create(PizzaRequest request, out string ErrorMessage)
{
ErrorMessage = "";
request.Name = request.Name.Trim();
//test if orders are closed
if (_globalStuffService.OrdersLocked)
{
ErrorMessage = "Orders are locked, you are too late.";
return null;
}
//test if request is valid
if (request == null)
{
ErrorMessage = "Request was null. If you see this, contact the admin!";
return null;
}
if (_context.Requests.AsEnumerable().Contains(request, _reqNameEqualityComparer))
{
ErrorMessage = $"Request with name {request.Name} already exists. Use a different name!";
return null;
}
if (request.Name == null || request.Name == "")
{
ErrorMessage = "Request needs a name!";
return null;
}
if (request.Name.Length > _globalStuffService.GetConfig().NameLength)
{
ErrorMessage = $"Request name is too long! Max Length: {_globalStuffService.GetConfig().NameLength}";
return null;
}
if (request.reqPiecesVegan + request.reqPiecesVegetarian + request.reqPiecesMeat < 1)
{
ErrorMessage = "Request needs to have at least one piece!";
return null;
}
//insert valid request
request.Id = _rnd.Next(int.MaxValue);
_context.Requests.Add(request);
_context.SaveChanges();
_globalStuffService.ShouldBalance = true;
return request;
}
public IEnumerable<PizzaRequest> GetAllRequests()
{
if (_globalStuffService.ShouldBalance)
{
Balance();
}
return _context.Requests.OrderBy(r => r.Name);
}
public IEnumerable<PizzaResult> GetAllResults()
{
if (_globalStuffService.ShouldBalance)
{
Balance();
}
return _context.Results.ToList();
}
public PizzaResult? GetResultById(int id)
{
if (_globalStuffService.ShouldBalance)
{
Balance();
}
return _context.Results.Find(id);
}
public PizzaRequest? GetRequestById(int id)
{
if(_context.Requests.Find(id) == null)
{
return null;
}
return _context.Requests.Find(id).GetShallowCopy();
}
public void DeleteById(int id)
{
var request = _context.Requests.Find(id);
var result = _context.Results.Find(id);
if (request != null)
{
_context.Requests.Remove(request);
}
if (result != null)
{
_context.Results.Remove(result);
}
_globalStuffService.ShouldBalance = true;
_context.SaveChanges();
}
public bool UpdateRequest(PizzaRequest request, out string ErrorMessage)
{
ErrorMessage = "";
//test if orders are closed
if (_globalStuffService.OrdersLocked)
{
ErrorMessage = "Orders are locked, you are too late.";
return false;
}
//test if request is valid
if (request == null)
{
ErrorMessage = "Request was null. If you see this, contact the admin!";
return false;
}
if (request.reqPiecesVegan + request.reqPiecesVegetarian + request.reqPiecesMeat < 1)
{
ErrorMessage = "Request needs to have at least one piece!";
return false;
}
_context.Requests.Remove(_context.Requests.Find(request.Id));
_context.Requests.Add(request);
_context.SaveChanges();
_globalStuffService.ShouldBalance = true;
return true;
}
public void Balance()
{
Dictionary<int, PizzaRequest> orders = new Dictionary<int, PizzaRequest>();
foreach (var request in _context.Requests.ToList())
{
orders.Add(request.Id, request);
}
var balancingResult = _balancingService.Distribute(orders);
_context.Results.RemoveRange(_context.Results.ToList());
_context.Results.AddRange(balancingResult.results.Values);
_globalStuffService.MeatPizzas = balancingResult.requiredMeat;
_globalStuffService.VeggiePizzas = balancingResult.requiredVeggie;
_globalStuffService.VeganPizzas = balancingResult.requiredVegan;
_globalStuffService.TotalCost = balancingResult.totalCost;
_context.SaveChanges();
_globalStuffService.ShouldBalance = false;
}
public bool DeleteAllOrders(string passPhrase)
{
if (passPhrase == Environment.GetEnvironmentVariable("DELETION_PASSPHRASE"))
{
_globalStuffService.MeatPizzas = 0;
_globalStuffService.VeggiePizzas = 0;
_globalStuffService.VeganPizzas = 0;
_globalStuffService.TotalCost = 0;
_context.Requests.RemoveRange(_context.Requests);
_context.Results.RemoveRange(_context.Results);
_context.SaveChanges();
return true;
}
return false;
}
public void MarkAsPaid(int id)
{
_context.Results.Find(id).hasPaid = true;
_context.SaveChanges();
}
public void MarkAsNotPaid(int id)
{
_context.Results.Find(id).hasPaid = false;
_context.SaveChanges();
}
}
}

View file

@ -0,0 +1,583 @@
using PizzaBot.Models;
namespace PizzaBot.Services
{
public class old_PizzaBalancingService
{
private readonly JSONService _jsonService;
public old_PizzaBalancingService(JSONService jsonService)
{
_jsonService = jsonService;
}
struct DistributionPerformance
{
public float maxPenalty;
public float avgPenalty;
public float penaltyVariance;
public float penaltyStandardDeviation;
}
public (float penalty, bool isOk) CalculatePenalty(PizzaRequest request, PizzaResult result)
{
int reqMeat = request.reqPiecesMeat;
int reqVeggie = request.reqPiecesVegetarian;
int reqVegan = request.reqPiecesVegan;
int resMeat = result.resPiecesMeat;
int resVeggie = result.resPiecesVegetarian;
int resVegan = result.resPiecesVegan;
float categoryToleranceMeatVeggie = request.priority;
int reqNum = reqMeat + reqVeggie;
int resNum = resMeat + resVeggie;
uint diffNumReqRes = (uint)Math.Abs(reqNum - resNum);
//result is in 0.01 to sq(diffNum)*catTol*0.99
float num_base = 0.01f;
//float penaltyCount = (diffNumReqRes * diffNumReqRes) * (categoryToleranceMeatVeggie * (1 - num_base) + num_base);
//float epsilon = 0.001f;
//float percentMeatInRequest = reqMeat / (reqNum + epsilon);
//float percentMeatInResult = resMeat / (resNum + epsilon);
//float diffMeatShareResReq = percentMeatInResult - percentMeatInRequest;
//float penaltyCat = (diffMeatShareResReq * diffMeatShareResReq) / (categoryToleranceMeatVeggie + epsilon);
//float f = 0.5f; // fav: 0.0: num, 1.0: cat
//float penaltyMeatVeggi = ((1.0f - f) * penaltyCount + f * penaltyCat) / reqNum;
// ________Vanessa Penalties________
float penaltyCount = (Math.Abs(reqNum - resNum)) / (float)reqNum; // 0 - 1
// apply function to make higher differences have higher penalty
penaltyCount = (float)Math.Pow(penaltyCount, 2);
float epsilon = 0.001f;
float percentMeatInRequest = reqMeat / (reqNum + epsilon); // 0 - 1
float percentMeatInResult = resMeat / (resNum + epsilon); // 0 - 1
float percentVeggiInRequest = reqMeat / (reqNum + epsilon);
float percentVeggiInResult = resMeat / (resNum + epsilon);
float diffMeatShareResReq = Math.Abs(percentMeatInResult - percentMeatInRequest); // 0 - 1
float diffVeggiShareResReq = Math.Abs(percentVeggiInResult - percentVeggiInRequest);
float penaltyCat = (diffMeatShareResReq + diffVeggiShareResReq) / 2; // 0 - 1
// apply function to make small differences have higher penalty
penaltyCat = (float)Math.Sqrt(penaltyCat);
// take weighted average of both penalties using categoryToleranceMeatVeggie
float f = categoryToleranceMeatVeggie; // fav: 0.0: num, 1.0: cat
float penaltyMeatVeggi = (1.0f - f) * penaltyCount + f * penaltyCat;
// ________Vanessa Penalties________
//float f = 0.5f; // fav: 0.0: num, 1.0: cat
//float penaltyMeatVeggi = ((1.0f - f) * penaltyCount + f * penaltyCat) / reqNum;
//only ok, if at least one piece of each requested type has been assigned
bool meatOk = !(reqMeat == 0 && resMeat != 0);
bool veggieOk = !(reqVeggie == 0 && resVeggie != 0);
return (penaltyMeatVeggi, categoryToleranceMeatVeggie != 0.0f || (meatOk && veggieOk));
}
public (Dictionary<int, PizzaResult> results, int requiredMeat, int requiredVeggie, int requiredVegan, float totalCost) Distribute(Dictionary<int, PizzaRequest> orders)
{
PizzaConfig config = _jsonService.ReadPizzaConfig();
//PizzaConfig config = new PizzaConfig();
//config.Fragments = 15;
//config.Price = 2150;
// In general, cannot determine the leveling strategy at this point.
// However, the optimal solution follows one of four strategies:
// - drain both
// - drain V, fill P
// - fill P, drain V
// - fill both
// So, try all these to find the optimal solution.
// ppp should be the same for all pizzas of all categories (or?)
// however, the user or this function could try getting better-fitting results
// by testing other ppp values.
// this would probably need floating point requests or distribution factors
bool compareResults(float oldMaxPenalty, float oldAveragePenalty, float newMaxPenalty, float newAveragePenalty)
{
float f = 0.1f;
float oldVal = (1.0f - f) * oldMaxPenalty + oldAveragePenalty;
float newVal = (1.0f - f) * newMaxPenalty + newAveragePenalty;
return newVal < oldVal;
}
var balanced = Balance(orders, config.Fragments, false, false);
Console.WriteLine("chose false, false" + balanced);
var newBalanced = Balance(orders, config.Fragments, false, true);
if (compareResults(balanced.maxPen, balanced.avgPen, newBalanced.maxPen, newBalanced.avgPen))
{
balanced = newBalanced;
Console.WriteLine("chose false, true" + balanced);
}
newBalanced = Balance(orders, config.Fragments, true, false);
if (compareResults(balanced.maxPen, balanced.avgPen, newBalanced.maxPen, newBalanced.avgPen))
{
balanced = newBalanced;
Console.WriteLine("chose true, false" + balanced);
}
newBalanced = Balance(orders, config.Fragments, true, true);
if (compareResults(balanced.maxPen, balanced.avgPen, newBalanced.maxPen, newBalanced.avgPen))
{
balanced = newBalanced;
Console.WriteLine("chose true, true" + balanced);
}
float p = 1.0f / balanced.balanced.Count;
DistributionPerformance performance = new DistributionPerformance();
performance.maxPenalty = balanced.maxPen;
performance.avgPenalty = balanced.avgPen;
foreach (var result in balanced.balanced)
{
float d = performance.avgPenalty - result.Value.penaltyMeatVeggi;
performance.penaltyVariance += d * d * p;
}
performance.penaltyStandardDeviation = (float)Math.Sqrt(performance.penaltyVariance);
// set order price, count and verify required fragments
int requiredMeat = 0, requiredVeggie = 0, requiredVegan = 0;
foreach (var result in balanced.balanced)
{
p = (result.Value.resPiecesMeat + result.Value.resPiecesVegetarian) * config.Price;
p = (p + config.Fragments - 1) / config.Fragments;
result.Value.totalCost = p * 0.01f;
requiredMeat += result.Value.resPiecesMeat;
requiredVeggie += result.Value.resPiecesVegetarian;
}
if (requiredMeat % config.Fragments != 0)
{
throw new Exception("Meat fragments don't fit!");
}
if (requiredVeggie % config.Fragments != 0)
{
throw new Exception("Veggie fragments don't fit!");
}
requiredMeat /= config.Fragments;
requiredVeggie /= config.Fragments;
float totalCost = (requiredMeat + requiredVeggie + requiredVegan) * config.Price / 100.0f;
return (balanced.balanced, requiredMeat, requiredVeggie, 0, totalCost);
}
public (Dictionary<int, PizzaResult> balanced, float maxPen, float avgPen) Balance(Dictionary<int, PizzaRequest> requests, int piecesPerPizza, bool fillMeat, bool fillVeggie)
{
Dictionary<int, PizzaResult> balanced = new Dictionary<int, PizzaResult>();
//allocated pieces per category
int numMeat = 0, numVeggie = 0, numVegan = 0;
//fill categories with preferences
foreach (var request in requests)
{
PizzaResult result = new PizzaResult();
result.Id = request.Value.Id;
result.resPiecesMeat = request.Value.reqPiecesMeat;
result.resPiecesVegetarian = request.Value.reqPiecesVegetarian;
result.resPiecesVegan = request.Value.reqPiecesVegan;
numMeat += request.Value.reqPiecesMeat;
numVeggie += request.Value.reqPiecesVegetarian;
numVegan += request.Value.reqPiecesVegan;
balanced.Add(request.Value.Id, result);
}
//determine next viable pizza order with balancing strategy
int requiredMeatPizzas = numMeat / piecesPerPizza;
int requiredVeggiePizzas = numVeggie / piecesPerPizza;
int requiredVeganPizzas = numVegan / piecesPerPizza;
if (fillMeat)
{
requiredMeatPizzas++;
}
if (fillVeggie)
{
requiredVeggiePizzas++;
}
//determine delta of pieces to next viable pizza order
int deltaMeat = numMeat - requiredMeatPizzas * piecesPerPizza;
int deltaVeggie = numVeggie - requiredVeggiePizzas * piecesPerPizza;
//-----tuxic-----
// balance using provided strategy
// this can be done greedily, advancing to a balanced state,
// if the advancement per operation is constant.
// there are 2 basic operations:
// - compress/expand P/V (advance: 1)
// - move right/left (P->V / V->P) (advance: 0 or 2)
// since we want to minimize the maximum penalty any (not every) one has to pay,
// the cheapest/best operation is the operation that results in the lowest penalty
// when draining both:
// loop (best) compress until one category fits (advance: 1)
// loop select (best) of (advance: 1):
// - (best) compress of overfull category
// - (best) compress of fitting category and (best) move from overfull to fitting
// when filling both:
// loop (best) expand until one category fits (advance: 1)
// loop select (best) of (advance: 1):
// - (best) expand of underfull category
// - (best) expand of fitting category and (best) move from fitting to underfull
// when draining D and filling F:
// // level operations
// loop select (best) of until one fits (advance: 2, but max. one on each side):
// - [A](best) compress D and (best) expand F
// - [B](best) move from D to F
// // drain/fill other
// either if (D fits):
// - loop select (best) of (advance: 1):
// - [C](best) expand F
// - [D](best) move from D to F and (best) expand D
// or if (F fits):
// - loop select (best) of (advance: 1):
// - [E](best) compress D
// - [F](best) move from D to F and (best) compress F
// since [A] = [C] + [E] and [B] = [C] + [F] = [D] + [E] and [B] (^=, but simpler than) [D] + [F],
// and so [A] and [B] are preferable to [C], [D], [E], [F],
// this should yield the optimal solution.
//-----tuxic-----
if (!fillMeat && !fillVeggie)
{
while (!(deltaMeat == 0 || deltaVeggie == 0))
{
// (best) compress any category
var scale = Scale(requests, ref balanced, false, true, true, false);
deltaMeat += scale.deltaMeat;
deltaVeggie += scale.deltaVeggie;
if (scale.deltaMeat == 0 && scale.deltaVeggie == 0)
{
break;
}
}
while (!(deltaMeat == 0 && deltaVeggie == 0))
{
// (best) compress !fitting, allow deferred
var scale = Scale(requests, ref balanced, false, deltaMeat != 0, deltaVeggie != 0, true);
deltaMeat += scale.deltaMeat;
deltaVeggie += scale.deltaVeggie;
if (scale.deltaMeat == 0 && scale.deltaVeggie == 0)
{
break;
}
}
}
else if (fillMeat && fillVeggie)
{
while (!(deltaMeat == 0 || deltaVeggie == 0))
{
// (best) expand any category
var scale = Scale(requests, ref balanced, true, true, true, false);
deltaMeat += scale.deltaMeat;
deltaVeggie += scale.deltaVeggie;
if (scale.deltaMeat == 0 && scale.deltaVeggie == 0)
{
break;
}
}
while (!(deltaMeat == 0 && deltaVeggie == 0))
{
// (best) expand !fitting, allow deferred
var scale = Scale(requests, ref balanced, true, deltaMeat != 0, deltaVeggie != 0, true);
deltaMeat += scale.deltaMeat;
deltaVeggie += scale.deltaVeggie;
if (scale.deltaMeat == 0 && scale.deltaVeggie == 0)
{
break;
}
}
}
else
{
while (!(deltaMeat == 0 || deltaVeggie == 0))
{
var shift = Shift(requests, ref balanced, fillVeggie, true);
if (shift.bestPenalty == float.MaxValue)
{
return (null, shift.bestPenalty, shift.bestPenalty);
}
deltaMeat += shift.deltaMeat;
deltaVeggie += shift.deltaVeggie;
if (shift.deltaMeat == 0 && shift.deltaVeggie == 0)
{
break;
}
}
int deltaFill = fillMeat ? deltaMeat : deltaVeggie;
bool fillFits = deltaFill == 0;
while (!(deltaMeat == 0 && deltaVeggie == 0))
{
var scale = Scale(requests, ref balanced, !fillFits, deltaMeat != 0, deltaVeggie != 0, true);
if (scale.bestPenalty == float.MaxValue)
{
return (null, scale.bestPenalty, scale.bestPenalty);
}
deltaMeat += scale.deltaMeat;
deltaVeggie += scale.deltaVeggie;
if (scale.deltaMeat == 0 && scale.deltaVeggie == 0)
{
break;
}
}
}
float num = balanced.Count;
float avgPenalty = 0;
float maxPenalty = float.MinValue;
foreach (var result in balanced)
{
var penaltyVal = CalculatePenalty(requests[result.Key], result.Value);
result.Value.penaltyMeatVeggi = penaltyVal.penalty;
avgPenalty += penaltyVal.penalty / num;
if (penaltyVal.penalty > maxPenalty)
{
maxPenalty = penaltyVal.penalty;
}
}
return (balanced, maxPenalty, avgPenalty);
}
private Dictionary<int, PizzaResult> DuplicatePizzaResultDict(Dictionary<int, PizzaResult> original)
{
Dictionary<int, PizzaResult> duplicate = new Dictionary<int, PizzaResult>();
foreach (var result in original)
{
var resultCopy = new PizzaResult();
resultCopy.Id = result.Value.Id;
resultCopy.resPiecesMeat = result.Value.resPiecesMeat;
resultCopy.resPiecesVegetarian = result.Value.resPiecesVegetarian;
resultCopy.resPiecesVegan = result.Value.resPiecesVegan;
resultCopy.hasPaid = result.Value.hasPaid;
resultCopy.totalCost = result.Value.totalCost;
resultCopy.penaltyMeatVeggi = result.Value.penaltyMeatVeggi;
resultCopy.penaltyVeggieVegan = result.Value.penaltyVeggieVegan;
duplicate.Add(result.Key, resultCopy);
}
return duplicate;
}
//balance operations
//-----tuxic-----
// try improving balance using on compression/expansion of allowed categories (p/v),
// may allow defering the operation to the other category and moving over.
// return deltas (both zero if failed)
// -----tuxic-----
private (int deltaMeat, int deltaVeggie, float bestPenalty) Scale(Dictionary<int, PizzaRequest> requests, ref Dictionary<int, PizzaResult> resultsIn, bool expand, bool meat, bool veggie, bool allow_deferred)
{
var results = DuplicatePizzaResultDict(resultsIn);
int bestId = -1;
float bestPenalty = float.MaxValue;
PizzaResult bestResult = null;
int delta = expand ? 1 : -1;
int deltaMeat = 0, deltaVeggie = 0;
//adds delta to result of every order
if (meat)
{
foreach (var result in results)
{
int value = result.Value.resPiecesMeat + delta;
if (value >= 0)
{
result.Value.resPiecesMeat = value;
}
else
{
continue;
}
var penResult = CalculatePenalty(requests[result.Key], result.Value);
if (penResult.isOk && penResult.penalty < bestPenalty)
{
bestId = result.Key;
bestPenalty = penResult.penalty;
bestResult = result.Value;
deltaMeat = delta;
deltaVeggie = 0;
}
}
}
results = DuplicatePizzaResultDict(resultsIn);
//adds delta to result of every order
if (veggie)
{
foreach (var result in results)
{
int value = result.Value.resPiecesVegetarian + delta;
if (value >= 0)
{
result.Value.resPiecesVegetarian = value;
}
else
{
continue;
}
var penResult = CalculatePenalty(requests[result.Key], result.Value);
if (penResult.isOk && penResult.penalty < bestPenalty)
{
bestId = result.Key;
bestPenalty = penResult.penalty;
bestResult = result.Value;
deltaMeat = 0;
deltaVeggie = delta;
}
}
}
if (allow_deferred && (meat != veggie))
{
//duplicate results
Dictionary<int, PizzaResult> duplicate = DuplicatePizzaResultDict(resultsIn);
//find best move
var move = Shift(requests, ref duplicate, meat != expand, false);
//find best scale
var scale = Scale(requests, ref duplicate, expand, !meat, !veggie, false);
float maxPenalty = Math.Max(move.bestPenalty, scale.bestPenalty);
if (maxPenalty < bestPenalty)
{
deltaMeat = move.deltaMeat + scale.deltaMeat;
deltaVeggie = move.deltaMeat + scale.deltaVeggie;
bestPenalty = maxPenalty;
resultsIn = duplicate;
}
else if (bestId >= 0)
{
resultsIn[bestId] = bestResult;
}
}
else if (bestId >= 0)
{
resultsIn[bestId] = bestResult;
}
return (deltaMeat, deltaVeggie, bestPenalty);
}
private (int deltaMeat, int deltaVeggie, float bestPenalty) Shift(Dictionary<int, PizzaRequest> requests, ref Dictionary<int, PizzaResult> resultsIn, bool toVeggie, bool allowScaling)
{
var results = DuplicatePizzaResultDict(resultsIn);
int bestId = -1;
float bestPenalty = float.MaxValue;
PizzaResult bestResult = null;
int delta = toVeggie ? -1 : 1;
int deltaMeat = 0, deltaVeggie = 0;
foreach (var result in results)
{
int value = result.Value.resPiecesMeat + delta;
if (value >= 0)
{
result.Value.resPiecesMeat = value;
}
else
{
continue;
}
value = result.Value.resPiecesVegetarian - delta;
if (value >= 0)
{
result.Value.resPiecesVegetarian = value;
}
else
{
continue;
}
var penResult = CalculatePenalty(requests[result.Key], result.Value);
if (penResult.isOk && penResult.penalty < bestPenalty)
{
bestId = result.Key;
bestPenalty = penResult.penalty;
bestResult = result.Value;
deltaMeat = delta;
deltaVeggie = -delta;
}
}
if (allowScaling)
{
Dictionary<int, PizzaResult> duplicate = DuplicatePizzaResultDict(resultsIn);
//find best compress
var compress = Scale(requests, ref duplicate, false, toVeggie, !toVeggie, false);
//find best expand
var expand = Scale(requests, ref duplicate, true, !toVeggie, toVeggie, false);
// cannot be on the same order (id), or it would not be better than the non-deferred scale
float maxPen = Math.Max(compress.bestPenalty, expand.bestPenalty);
if (maxPen < bestPenalty)
{
deltaMeat = compress.deltaMeat + expand.deltaMeat;
deltaVeggie = compress.deltaVeggie + expand.deltaVeggie;
bestPenalty = maxPen;
resultsIn = duplicate;
}
else if (bestId >= 0)
{
resultsIn[bestId] = bestResult;
}
}
else if (bestId >= 0)
{
resultsIn[bestId] = bestResult;
}
return (deltaMeat, deltaVeggie, bestPenalty);
}
}
}

View file

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

10
PizzaBot/appsettings.json Normal file
View file

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Urls": "http://*:8080"
}

51
PizzaBot/wwwroot/app.css Normal file
View file

@ -0,0 +1,51 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"SizeX":0.6,"SizeY":0.4,"Price":2250,"Fragments":15,"Toppings":4,"NameLength":40,"PenaltyType":1}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

1
README.md Normal file
View file

@ -0,0 +1 @@
# TestBlazorApp

1
TestDatabase/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
database/

View file

@ -0,0 +1,14 @@
services:
database:
image: mysql
volumes:
- ./database:/var/lib/mysql
- ./setup.sql:/docker-entrypoint-initdb.d/1.sql
restart: always
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: QEwqTdS#7pxEy#
MYSQL_USER: PizzaBotDev
MYSQL_PASSWORD: PizzaDeliveryEverywhere!
MYSQL_DATABASE: PizzaBotTest

17
TestDatabase/setup.sql Normal file
View file

@ -0,0 +1,17 @@
CREATE TABLE PizzaBotTest.Requests (
Id INT PRIMARY KEY,
Name LONGTEXT NOT NULL,
reqPiecesMeat INT NOT NULL,
reqPiecesVegetarian INT NOT NULL,
reqPiecesVegan INT NOT NULL,
priority FLOAT);
CREATE TABLE PizzaBotTest.Results (
Id INT PRIMARY KEY,
resPiecesMeat INT NOT NULL,
resPiecesVegetarian INT NOT NULL,
resPiecesVegan INT NOT NULL,
penaltyMeatVeggi FLOAT NOT NULL,
penaltyVeggieVegan FLOAT NOT NULL,
totalCost FLOAT NOT NULL,
hasPaid TINYINT NOT NULL);