Merge branch 'feature/pricelist' into develop

This commit is contained in:
Tim Gröger 2021-04-17 18:32:06 +02:00
commit a8a6cd8814
45 changed files with 4493 additions and 1426 deletions

View File

@ -8,7 +8,7 @@
"homepage": "https://flaschengeist.dev/Flaschengeist",
"description": "Modular student club administration system",
"bugs": {
"url" : "https://flaschengeist.dev/Flaschengeist/flaschengeist-frontend/issues"
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist-frontend/issues"
},
"scripts": {
"format": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'",
@ -18,7 +18,8 @@
"axios": "^0.21.1",
"cordova": "^10.0.0",
"pinia": "^2.0.0-alpha.10",
"quasar": "^2.0.0-beta.12"
"quasar": "^2.0.0-beta.12",
"vuedraggable": "^4.0.1"
},
"devDependencies": {
"@quasar/app": "^3.0.0-beta.13",
@ -29,6 +30,8 @@
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.20.0",
"@typescript-eslint/parser": "^4.20.0",
"electron": "^12.0.4",
"electron-packager": "^14.1.1",
"eslint": "^7.23.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^7.8.0",

168
public/no-image.svg Normal file
View File

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1024"
height="1024"
viewBox="0 0 270.93333 270.93334"
version="1.1"
id="svg37"
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
sodipodi:docname="no-image.svg">
<defs
id="defs31">
<rect
x="-328.72475"
y="24.854798"
width="167.56944"
height="62.537879"
id="rect1100" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.66"
inkscape:cx="-222.85714"
inkscape:cy="248.57143"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
units="px"
inkscape:window-width="2560"
inkscape:window-height="1303"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1" />
<metadata
id="metadata34">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<circle
id="path10"
cx="135.46666"
cy="135.46666"
style="fill:#1976d2;fill-opacity:1;stroke-width:0.265065;opacity:1"
r="135.46666" />
<path
id="path897"
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:2.4226772;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 443.15234 56.630859 A 66.428573 82.142858 16.845913 0 0 371.42188 118.32031 A 66.428573 82.142858 16.845913 0 0 411.19531 216.18945 A 66.428573 82.142858 16.845913 0 0 417.72266 217.72852 C 381.34438 302.48672 370.45756 410.53422 348.14648 499.98438 C 297.07736 687.66963 128.47878 622.66455 146.45508 716.38281 C 160.50823 789.6481 350.53291 863.07431 404.40039 881.20117 C 404.36927 881.70781 404.3327 882.22702 404.30664 882.7207 C 402.76028 912.46642 410.7207 918.72461 410.7207 918.72461 L 432.67188 918.59766 C 432.67187 918.59766 438.27004 922.75802 441.00195 922.53125 C 443.73387 922.30455 446.94531 918.40625 446.94531 918.40625 L 516.89062 924.79297 C 516.89062 924.79297 519.31971 928.09007 521.125 928.27148 C 522.93029 928.45267 525.90234 925.73828 525.90234 925.73828 L 547.53125 926.69531 C 547.53125 926.69531 549.70216 931.12195 551.70508 931.54492 C 553.70725 931.97129 555.09081 928.73815 557.0293 929.03711 C 572.07401 931.3732 576.56924 937.03281 582.65039 957.60156 C 587.77505 974.93535 595.22123 992.9761 628.75781 1000.5176 C 648.78447 1005.021 775.66385 1000.6487 849.12305 998.74219 C 849.12305 998.74219 858.5188 1005.9328 870.60547 1002.9863 C 910.19976 993.33421 919.78542 952.33706 923.33398 921.09961 C 926.88262 889.86212 910.49308 842.14037 892.4043 835.26367 C 874.31552 828.38693 867.54688 834.25781 867.54688 834.25781 C 794.38713 828.15886 695.31802 794.91067 642.43555 796.74805 C 623.72658 797.39809 605.92942 809.14688 595.65625 826.28125 C 588.66186 837.94703 583.48245 859.20701 563.14258 859.07812 C 560.89588 859.11237 559.32296 855.73577 557.25 855.80078 C 555.17708 855.86568 552.45117 858.87891 552.45117 858.87891 L 531.66797 857.41211 C 531.66797 857.41211 529.50203 853.62212 527.45508 853.38477 C 525.40816 853.14741 521.82422 856.70312 521.82422 856.70312 L 449.90234 850.62305 C 449.90234 850.62305 447.13702 845.72836 444.7168 844.78711 C 442.29665 843.84582 436.68945 846.35352 436.68945 846.35352 L 415.74219 842.4082 C 415.74219 842.4082 407.89779 846.84558 405.02539 873.69922 C 358.98121 845.1153 229.07948 771.28872 265.40039 726.04102 C 306.49077 674.8517 472.92361 721.17976 650.12695 656.71875 C 726.82706 628.35646 797.61671 580.25753 845.84766 519.08398 A 66.376657 87.827348 48.964508 0 0 927.6875 519.24805 A 66.376657 87.827348 48.964508 0 0 980.98047 413.88477 A 66.376657 87.827348 48.964508 0 0 907.37109 380.61328 C 911.18565 353.15829 910.56576 324.56748 904.71094 295.1582 C 874.19541 141.87518 734.80037 48.0882 582.23438 78.189453 C 572.33148 80.143182 562.48437 82.604632 552.73047 85.566406 C 533.35813 91.448951 516.25153 99.658419 501.06836 109.80859 A 66.428573 82.142858 16.845913 0 0 458.80469 58.953125 A 66.428573 82.142858 16.845913 0 0 443.15234 56.630859 z M 598.64062 188.20703 C 604.22194 188.22929 609.06312 189.70004 612.92773 192.57031 C 630.98057 205.9773 623.87627 246.11384 597.05859 282.2168 C 570.24164 318.31869 533.86885 336.71569 515.81836 323.30664 C 497.76711 309.89888 504.87302 269.76201 531.68945 233.66016 C 549.9558 209.06922 573.57677 191.68513 592.82031 188.66992 L 592.82227 188.66992 C 594.83831 188.35408 596.78019 188.19961 598.64062 188.20703 z M 778.42969 278.44141 C 784.01155 278.46343 788.85382 279.93226 792.71875 282.80273 C 810.7689 296.21155 803.66213 336.34605 776.8457 372.44727 C 750.02943 408.54893 713.65672 426.94663 695.60547 413.53906 C 677.55528 400.13024 684.66205 359.99578 711.47852 323.89453 C 729.74482 299.3036 753.36587 281.91757 772.60938 278.90234 C 774.62562 278.58637 776.56907 278.43406 778.42969 278.44141 z M 582.87695 399.91016 C 586.53338 399.95128 589.93604 400.53593 593.03711 401.66406 C 620.67041 411.71655 618.60959 461.69743 588.43555 513.29883 C 558.26146 564.90004 511.39926 598.58305 483.76562 588.53125 C 456.13229 578.47876 458.1931 528.49788 488.36719 476.89648 C 510.90103 438.36065 543.79364 408.38759 571.19336 401.41992 C 575.31072 400.37297 579.22053 399.86903 582.87695 399.91016 z M 643.83594 816.76172 C 685.1496 816.94824 851.34766 854.11133 851.34766 854.11133 C 851.34766 854.11133 688.31573 835.12065 650.18555 833.82812 C 635.17621 833.31932 591.27148 868.21875 591.27148 868.21875 C 591.27148 868.21875 622.20895 822.68111 636.20117 817.55664 C 637.73138 816.99622 640.33478 816.74591 643.83594 816.76172 z "
transform="scale(0.26458333)" />
<path
id="path10-8"
style="fill-opacity:1;stroke-width:3.64724414;fill:#ffffff;stroke-miterlimit:4;stroke-dasharray:none;stroke:#ffffff;stroke-opacity:1;opacity:0.80057803"
d="M 512 0 A 511.99997 511.99997 0 0 0 0 512 A 511.99997 511.99997 0 0 0 512 1024 A 511.99997 511.99997 0 0 0 659.25 1002.3672 C 706.36552 1003.0651 793.17503 1000.1943 849.12305 998.74219 C 849.12305 998.74219 858.5188 1005.9328 870.60547 1002.9863 C 910.19976 993.33421 919.78542 952.33706 923.33398 921.09961 C 926.20746 895.80534 916.007 859.7095 902.41797 843.23633 A 511.99997 511.99997 0 0 0 1024 512 A 511.99997 511.99997 0 0 0 512 0 z "
transform="scale(0.26458333)" />
<rect
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.33699;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect942-1"
width="76.028526"
height="73.388649"
x="-21.278112"
y="109.32571"
ry="3.5350549"
transform="rotate(-30.00892)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.391977;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect944-7"
width="69.4505"
height="56.151112"
x="-17.909185"
y="113.14103"
ry="0"
transform="rotate(-30.00892)" />
<path
id="path10-3-94"
style="fill:#1976d2;fill-opacity:1;stroke-width:0.0683297"
d="m 68.884108,90.871044 a 34.92124,34.92124 0 0 0 -11.74769,43.865396 l 2.70637,4.68588 a 34.92124,34.92124 0 0 0 15.52651,12.5468 L 123.2496,124.31547 a 34.92124,34.92124 0 0 0 -2.38462,-18.10096 l -4.46922,-7.73814 A 34.92124,34.92124 0 0 0 73.568128,88.16575 Z" />
<path
id="path897-6-8"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:0.165241;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 69.039278,95.120659 a 5.6025989,4.5307973 76.836993 0 0 -2.13225,6.090461 5.6025989,4.5307973 76.836993 0 0 5.68756,4.42371 5.6025989,4.5307973 76.836993 0 0 0.43811,-0.13196 c 0.74268,6.24695 3.78537,13.00002 5.51895,19.04423 3.38604,12.82723 -8.78931,14.73905 -4.53067,19.66107 1.76551,2.04052 6.4553,2.39549 11.0381,2.16453 l 3.43497,-1.98389 c -3.77377,-0.36303 -7.4885,-1.26121 -7.11831,-3.66758 0.68073,-4.42505 12.09089,-7.36618 20.358052,-17.21816 3.5626,-4.29154 6.10303,-9.54716 6.86491,-14.80546 a 4.5272563,5.9903123 18.955588 0 0 4.83918,-2.78197 4.5272563,5.9903123 18.955588 0 0 -0.44664,-8.041018 4.5272563,5.9903123 18.955588 0 0 -5.48235,0.545791 c -0.71126,-1.751685 -1.72325,-3.419125 -3.07226,-4.956384 -7.031112,-8.012324 -18.463402,-8.796579 -26.447502,-1.814405 -0.51824,0.453207 -1.01578,0.934617 -1.49084,1.442271 -0.94352,1.008269 -1.67389,2.076631 -2.2244,3.194051 a 5.6025989,4.5307973 76.836993 0 0 -4.23087,-1.562013 5.6025989,4.5307973 76.836993 0 0 -1.00374,0.396734 z m 13.67184,2.467287 c 0.33015,-0.189526 0.66651,-0.267379 0.99269,-0.229688 1.52358,0.176032 2.47307,2.788932 2.12069,5.836062 -0.35237,3.04705 -1.87308,5.37436 -3.3966,5.19813 -1.52351,-0.17613 -2.47289,-2.78913 -2.12054,-5.83615 0.24002,-2.07551 1.04204,-3.90793 2.07575,-4.742444 l 1.7e-4,-9.5e-5 c 0.10831,-0.08739 0.21769,-0.162799 0.32783,-0.225833 z m 13.69685,-0.803574 c 0.33018,-0.189545 0.6664,-0.267533 0.9926,-0.229843 1.52349,0.176235 2.4729,2.789172 2.12054,5.836151 -0.35234,3.04702 -1.87298,5.37441 -3.39651,5.1983 -1.52348,-0.17624 -2.4729,-2.78917 -2.12054,-5.83617 0.24,-2.075497 1.04211,-3.908161 2.07582,-4.742688 0.10834,-0.0874 0.21795,-0.162716 0.32809,-0.22575 z m -7.40627,13.845038 c 0.21745,-0.12213 0.43816,-0.20399 0.65981,-0.24312 1.97501,-0.34891 3.55829,2.67343 3.53636,6.75042 -0.0219,4.077 -1.64072,7.66488 -3.61571,8.01383 -1.975,0.34891 -3.55819,-2.67326 -3.53627,-6.75027 0.0164,-3.04468 0.93655,-5.93703 2.31716,-7.28321 0.20748,-0.20228 0.42129,-0.36535 0.63865,-0.48765 z m 17.352832,21.22847 c -0.10315,0.0588 -0.20272,0.11824 -0.29832,0.17807 -1.05105,0.65671 -1.69275,1.90208 -1.75,3.22056 l 1.74663,-1.00878 c 0.10074,-0.33823 0.21509,-0.61452 0.34506,-0.77008 0.0713,-0.0853 0.2166,-0.18885 0.42393,-0.30733 0.72661,-0.41504 2.22115,-1.01463 3.95458,-1.65068 l 5.84666,-3.37678 c -3.78613,1.29335 -7.9478,2.39117 -10.26846,3.71516 z" />
<rect
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.33699;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect942"
width="76.028526"
height="73.388649"
x="97.497429"
y="66.694847"
ry="3.5350549" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.391977;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect944"
width="69.4505"
height="56.151112"
x="100.86639"
y="70.51017"
ry="0" />
<path
id="path10-3"
style="fill:#1976d2;fill-opacity:1;stroke-width:0.0683297"
d="M 132.97782,70.510038 A 34.92124,34.92124 0 0 0 100.8663,102.61974 v 5.41128 a 34.92124,34.92124 0 0 0 7.17007,18.63021 h 55.29238 a 34.92124,34.92124 0 0 0 6.98797,-16.86711 v -8.93604 A 34.92124,34.92124 0 0 0 138.38694,70.510038 Z" />
<path
id="path897-6"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:0.165241;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 130.9868,74.267588 a 4.5307973,5.6025989 16.845913 0 0 -4.89247,4.20761 4.5307973,5.6025989 16.845913 0 0 2.71268,6.675244 4.5307973,5.6025989 16.845913 0 0 0.44538,0.10485 c -2.4812,5.78097 -3.22383,13.15053 -4.74557,19.251518 -3.4832,12.80118 -14.98258,8.3674 -13.7565,14.75951 0.5083,2.64997 4.39189,5.30288 8.47586,7.39491 h 3.96671 c -3.08632,-2.20176 -5.85386,-4.8374 -4.32979,-6.73605 2.80259,-3.4914 14.15415,-0.33165 26.2404,-4.72825 5.23137,-1.93447 10.05977,-5.215 13.34938,-9.38737 a 4.5272563,5.9903123 48.964508 0 0 5.58183,0.0112 4.5272563,5.9903123 48.964508 0 0 3.63483,-7.186478 4.5272563,5.9903123 48.964508 0 0 -5.0204,-2.26929 c 0.26017,-1.87259 0.21778,-3.82264 -0.18155,-5.82851 -2.08133,-10.454754 -11.58886,-16.851564 -21.9947,-14.798494 -0.67543,0.13326 -1.34705,0.3013 -2.01232,0.50331 -1.32131,0.40122 -2.4881,0.96108 -3.52367,1.65338 a 4.5307973,5.6025989 16.845913 0 0 -2.8825,-3.46863 4.5307973,5.6025989 16.845913 0 0 -1.0676,-0.15845 z m 10.60512,8.974304 c 0.38068,10e-4 0.71089,0.10181 0.97449,0.29758 1.2313,0.91443 0.74671,3.65194 -1.08241,6.11436 -1.82906,2.46235 -4.30989,3.71712 -5.54104,2.80255 -1.23119,-0.91448 -0.74644,-3.65202 1.08259,-6.11436 1.24587,-1.67724 2.85684,-2.8629 4.16936,-3.06855 h 1.8e-4 c 0.1375,-0.0215 0.26994,-0.0321 0.39683,-0.0316 z m 12.26265,6.15442 c 0.38072,0.001 0.71088,0.10162 0.97449,0.2974 1.23112,0.91456 0.74644,3.65206 -1.08258,6.11436 -1.82902,2.46234 -4.30984,3.71721 -5.54104,2.80274 -1.23112,-0.91456 -0.74645,-3.65206 1.08258,-6.11437 1.24586,-1.67724 2.85702,-2.86307 4.16954,-3.06873 0.13752,-0.0215 0.27011,-0.0319 0.39701,-0.0314 z m -13.33783,8.28494 c 0.24939,0.003 0.48145,0.0425 0.69297,0.11947 1.88474,0.68563 1.74421,4.094668 -0.31382,7.614168 -2.05805,3.51949 -5.25426,5.8168 -7.13902,5.13121 -1.88474,-0.68563 -1.74422,-4.09448 0.31382,-7.61399 1.53693,-2.62835 3.78032,-4.672758 5.64914,-5.147988 0.28082,-0.0714 0.54752,-0.10567 0.79691,-0.10287 z m 4.40955,27.061498 c -0.11872,-7e-4 -0.23468,0.001 -0.34739,0.005 -1.2386,0.043 -2.41713,0.80049 -3.12612,1.9136 h 2.01701 c 0.2564,-0.24251 0.4936,-0.42457 0.68395,-0.49428 0.10436,-0.0382 0.28201,-0.0552 0.52081,-0.0541 0.83678,0.004 2.43085,0.23225 4.25002,0.54842 h 6.75175 c -3.92545,-0.7736 -8.07829,-1.90434 -10.75003,-1.91848 z" />
<rect
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.33699;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect942-6"
width="76.028526"
height="73.388649"
x="183.4511"
y="-21.159952"
ry="3.5350549"
transform="rotate(27.418518)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.391977;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect944-6"
width="69.4505"
height="56.151112"
x="186.82002"
y="-17.344629"
ry="0"
transform="rotate(27.418518)" />
<path
id="path10-3-9"
style="fill:#1976d2;fill-opacity:1;stroke-width:0.0683297"
d="m 202.32517,85.418659 a 34.92124,34.92124 0 0 0 -43.2904,13.715795 l -2.49182,4.803416 a 34.92124,34.92124 0 0 0 -2.21435,19.83912 l 49.0812,25.46141 a 34.92124,34.92124 0 0 0 13.97007,-11.7545 l 4.11493,-7.93223 A 34.92124,34.92124 0 0 0 207.12667,87.90949 Z" />
<path
id="path897-6-3"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:0.165241;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 198.82751,87.837273 a 4.5307973,5.6025989 44.264431 0 0 -6.28043,1.482039 4.5307973,5.6025989 44.264431 0 0 -0.6659,7.174546 4.5307973,5.6025989 44.264431 0 0 0.34706,0.298162 c -4.86454,3.98902 -8.91733,10.18876 -13.07755,14.90366 -8.9867,9.75921 -17.15262,0.52818 -19.00775,6.76684 -0.76908,2.58635 1.45663,6.72959 4.11849,10.46723 l 3.52111,1.82662 c -1.72575,-3.37564 -2.96872,-6.98962 -0.74155,-7.97318 4.09551,-1.80864 12.71689,6.22341 25.47003,7.88625 5.5345,0.69181 11.33116,0.003 16.17256,-2.18564 a 4.5272563,5.9903123 76.383026 0 0 4.94964,2.5803 4.5272563,5.9903123 76.383026 0 0 6.5358,-4.70541 4.5272563,5.9903123 76.383026 0 0 -3.41147,-4.3262 c 1.09325,-1.54243 1.95359,-3.29295 2.5228,-5.25738 2.96674,-10.23876 -2.52713,-20.295083 -12.70946,-23.264393 -0.66092,-0.192737 -1.33447,-0.352844 -2.01803,-0.479873 -1.35764,-0.252295 -2.65117,-0.292618 -3.88921,-0.154954 a 4.5307973,5.6025989 44.264431 0 0 -0.96143,-4.406339 4.5307973,5.6025989 44.264431 0 0 -0.87472,-0.632269 z m 5.28126,12.849707 c 0.33746,0.17619 0.58416,0.41773 0.72799,0.71289 0.67191,1.37871 -1.01884,3.58556 -3.7764,4.92908 -2.75748,1.34349 -5.53743,1.31492 -6.20913,-0.0638 -0.67178,-1.3787 1.01911,-3.58551 3.77656,-4.929 1.87826,-0.91512 3.85425,-1.22576 5.11402,-0.80391 l 1.7e-4,8e-5 c 0.13196,0.0442 0.25439,0.0958 0.3668,0.15469 z m 8.05112,11.10986 c 0.33749,0.17621 0.58422,0.41755 0.72807,0.71273 0.67168,1.37874 -1.01913,3.58554 -3.77655,4.929 -2.75744,1.3435 -5.53743,1.31502 -6.20922,-0.0637 -0.67169,-1.37874 1.01912,-3.58554 3.77655,-4.92901 1.87826,-0.91512 3.8545,-1.22583 5.11428,-0.80399 0.13197,0.0442 0.25445,0.0961 0.36687,0.15495 z m -15.65465,1.21237 c 0.21999,0.1175 0.4078,0.25943 0.56011,0.42515 1.3573,1.47651 -0.33727,4.43789 -3.7848,6.61434 -3.44753,2.17643 -7.34258,2.74386 -8.69992,1.26738 -1.3573,-1.47651 0.33717,-4.43772 3.78471,-6.61418 2.57461,-1.62536 5.50741,-2.40706 7.38513,-1.96834 0.28216,0.0659 0.53468,0.15833 0.75477,0.27565 z m -8.54725,26.05213 c -0.10506,-0.0553 -0.20878,-0.10718 -0.31067,-0.15553 -1.11927,-0.53219 -2.51422,-0.40249 -3.65614,0.2591 l 1.79043,0.92881 c 0.33927,-0.0972 0.63366,-0.14958 0.83473,-0.12381 0.11023,0.0142 0.27575,0.0809 0.48722,0.19181 0.74094,0.38887 2.05083,1.32553 3.52006,2.44389 l 5.9933,3.10909 c -3.12826,-2.49432 -6.2939,-5.41036 -8.65901,-6.65322 z" />
<text
xml:space="preserve"
id="text1098"
style="font-style:normal;font-weight:normal;font-size:22.57779999999999987px;line-height:1.35;font-family:sans-serif;white-space:pre;shape-inside:url(#rect1100);fill:#000000;fill-opacity:1;stroke:none;"
x="52.690414"
y="0"
transform="translate(380.03788,147.12437)"><tspan
x="-284.65002"
y="47.162986"><tspan
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:22.5778px;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold Condensed';text-align:center;text-anchor:middle">Kein Bild </tspan></tspan><tspan
x="-292.36704"
y="78.223231"><tspan
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:22.5778px;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold Condensed';text-align:center;text-anchor:middle">vorhanden</tspan></tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="de.wu5.flaschengeist" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<widget id="de.wu5.flaschengeist" version="2.0.0-alpha.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>Flaschengeist</name>
<description>Dynamischen Managementsystem für Studentenclubs</description>
<description>Modular student club administration system</description>
<author email="dev@cordova.apache.org" href="http://cordova.io">
Apache Cordova Team
</author>

View File

@ -1,8 +1,9 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import 'quasar/dist/types/feature-flag';
import "quasar/dist/types/feature-flag";
declare module 'quasar/dist/types/feature-flag' {
declare module "quasar/dist/types/feature-flag" {
interface QuasarFeatureFlags {
cordova: true;
}

File diff suppressed because it is too large Load Diff

10
src-electron/electron-flag.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";
declare module "quasar/dist/types/feature-flag" {
interface QuasarFeatureFlags {
electron: true;
}
}

View File

@ -0,0 +1,56 @@
import { app, BrowserWindow, nativeTheme } from 'electron'
import path from 'path'
try {
if (process.platform === 'win32' && nativeTheme.shouldUseDarkColors === true) {
require('fs').unlinkSync(require('path').join(app.getPath('userData'), 'DevTools Extensions'))
}
} catch (_) { }
let mainWindow
function createWindow () {
/**
* Initial window options
*/
mainWindow = new BrowserWindow({
width: 1000,
height: 600,
useContentSize: true,
webPreferences: {
contextIsolation: true,
// More info: /quasar-cli/developing-electron-apps/electron-preload-script
preload: path.resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD)
}
})
mainWindow.loadURL(process.env.APP_URL)
if (process.env.DEBUGGING) {
// if on DEV or Production with debug enabled
mainWindow.webContents.openDevTools()
} else {
// we're on production; no access to devtools pls
mainWindow.webContents.on('devtools-opened', () => {
mainWindow.webContents.closeDevTools()
})
}
mainWindow.on('closed', () => {
mainWindow = null
})
}
app.on('ready', createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})

View File

@ -0,0 +1,17 @@
/**
* This file is used specifically for security reasons.
* Here you can access Nodejs stuff and inject functionality into
* the renderer thread (accessible there through the "window" object)
*
* WARNING!
* If you import anything from node_modules, then make sure that the package is specified
* in package.json > dependencies and NOT in devDependencies
*
* Example (injects window.myAPI.doAThing() into renderer thread):
*
* const { contextBridge } = require('electron')
*
* contextBridge.exposeInMainWorld('myAPI', {
* doAThing: () => {}
* })
*/

Binary file not shown.

BIN
src-electron/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1,6 +1,14 @@
import {computed} from 'vue';
import { LocalStorage } from 'quasar';
const config = {
baseURL: '/api',
pollingInterval: 30000,
};
const baseURL = computed(() =>
LocalStorage.getItem<string>('baseURL') || config.baseURL
);
export {baseURL}
export default config;

View File

@ -90,6 +90,8 @@ declare namespace FG {
tags?: Array<Tag>;
type?: DrinkType;
volumes: Array<DrinkPriceVolume>;
uuid: string;
receipt?: Array<string>;
}
interface DrinkIngredient {
id: number;
@ -130,5 +132,6 @@ declare namespace FG {
interface Tag {
id: number;
name: string;
color: string;
}
}

View File

@ -36,9 +36,9 @@
</q-form>
</q-card-section>
<div class="row justify-end">
<q-btn flat round icon="mdi-menu-down" class="cordova-only" @click="openServerSettings" />
<q-btn v-if='$q.platform.is.cordova || $q.platform.is.electron' flat round icon="mdi-menu-down" @click="openServerSettings" />
</div>
<q-slide-transition class="cordova-only">
<q-slide-transition v-if='$q.platform.is.cordova || $q.platform.is.electron'>
<div v-show="visible">
<q-separator />
<q-card-section>
@ -63,6 +63,7 @@ import { setBaseURL, api } from 'boot/axios';
import { notEmpty } from 'src/utils/validators';
import { useUserStore } from 'src/plugins/user/store';
import PasswordInput from 'src/components/utils/PasswordInput.vue';
import { useQuasar } from 'quasar';
export default defineComponent({
name: 'Login',
@ -77,6 +78,7 @@ export default defineComponent({
const password = ref('');
const server = ref<string | undefined>(api.defaults.baseURL);
const visible = ref(false);
const $q = useQuasar()
function openServerSettings() {
visible.value = !visible.value;
@ -150,6 +152,7 @@ export default defineComponent({
server,
userid,
visible,
$q
};
},
});

View File

@ -0,0 +1,62 @@
<template>
<q-carousel
v-model="volume"
transition-prev="slide-right"
transition-next="slide-left"
animated
swipeable
control-color="primary"
arrows
>
<q-carousel-slide v-for="volume in volumes" :key="volume.id" :name="volume.id">
<build-manual-volume-part :volume="volume" />
</q-carousel-slide>
</q-carousel>
<div class="full-width row justify-center q-pa-sm">
<div class="q-px-sm">
<q-btn-toggle v-model="volume" :options="btn_options" rounded />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, computed } from 'vue';
import { DrinkPriceVolume } from '../../store';
import BuildManualVolumePart from './BuildManualVolumePart.vue';
export default defineComponent({
name: 'BuildManualVolume',
components: { BuildManualVolumePart },
props: {
volumes: {
type: Array as PropType<Array<DrinkPriceVolume>>,
required: true,
},
},
setup(props) {
const _volume = ref<number>();
const volume = computed({
get: () => {
if (_volume.value !== undefined) {
return _volume.value;
}
return props.volumes[0].id;
},
set: (val: number) => (_volume.value = val),
});
const options = computed(() => {
let ret: Array<{ label: number; value: number }> = [];
props.volumes.forEach((volume: DrinkPriceVolume) => {
ret.push({ label: volume.id, value: volume.id });
});
return ret;
});
const btn_options = computed<Array<{ label: string; value: number }>>(() => {
const retVal: Array<{ label: string; value: number }> = [];
props.volumes.forEach((volume: DrinkPriceVolume) => {
retVal.push({ label: `${(<number>volume.volume).toFixed(3)}L`, value: volume.id });
});
return retVal;
});
return { volume, options, btn_options };
},
});
</script>

View File

@ -0,0 +1,65 @@
<template>
<q-card-section>
<div class="text-h6">Zutaten</div>
<div v-for="ingredient in volume.ingredients" :key="ingredient.id">
<div v-if="ingredient.drink_ingredient">
<div class="full-width row q-gutter-sm q-py-sm">
<div class="col">
{{ name(ingredient.drink_ingredient?.ingredient_id) }}
</div>
<div class="col">
{{
ingredient.drink_ingredient?.volume
? `${ingredient.drink_ingredient?.volume * 100} cl`
: ''
}}
</div>
</div>
<q-separator />
</div>
</div>
<div v-for="ingredient in volume.ingredients" :key="ingredient.id">
<div v-if="ingredient.extra_ingredient">
<div class="full-width row q-gutter-sm q-py-sm">
<div class="col">
{{ ingredient.extra_ingredient?.name }}
</div>
<div class="col"></div>
</div>
<q-separator />
</div>
</div>
</q-card-section>
<q-card-section>
<div class="text-h6">Preise</div>
<div class="full-width row q-gutter-sm justify-around">
<div v-for="price in volume.prices" :key="price.id">
<div class="text-body1">{{ price.price.toFixed(2) }}</div>
<q-badge v-if="price.public" class="text-caption"> öffentlich </q-badge>
<div class="text-caption text-weight-thin">
{{ price.description }}
</div>
</div>
</div>
</q-card-section>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { DrinkPriceVolume, usePricelistStore } from '../../store';
export default defineComponent({
name: 'BuildManualVolumePart',
props: {
volume: {
type: Object as PropType<DrinkPriceVolume>,
required: true,
},
},
setup() {
const store = usePricelistStore();
function name(id: number) {
return store.drinks.find((a) => a.id === id)?.name;
}
return { name };
},
});
</script>

View File

@ -1,317 +1,189 @@
<template>
<q-table
v-model:pagination="pagination"
title="Kalkulationstabelle"
title="Preistabelle"
:columns="columns"
:rows="drinks"
:visible-columns="visibleColumn"
:dense="$q.screen.lt.md"
row-key="id"
virtual-scroll
dense
:filter="search"
:filter-method="filter"
grid
:rows-per-page-options="[0]"
>
<template #header="props">
<q-tr :props="props">
<q-th auto-width />
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template #top-right>
<div class="row justify-end q-gutter-sm">
<q-btn label="Aufpreise">
<search-input v-model="search" :keys="search_keys" />
<slot></slot>
<q-btn v-if="!public && !nodetails" label="Aufpreise">
<q-menu anchor="center middle" self="center middle">
<min-price-setting />
</q-menu>
</q-btn>
<q-btn label="neues Getränk" color="positive" icon-right="add">
<q-menu v-model="showNewDrink" anchor="center middle" self="center middle">
<new-drink @close="showNewDrink = false" />
</q-menu>
<q-btn
v-if="!public && !nodetails && editable && hasPermission(PERMISSIONS.CREATE)"
color="primary"
round
icon="mdi-plus"
@click="newDrink"
>
<q-tooltip> Neues Getränk </q-tooltip>
</q-btn>
<q-select
v-model="visibleColumn"
multiple
filled
dense
options-dense
display-value="Sichtbarkeit"
emit-value
map-options
:options="[...columns, ...column_calc, ...column_prices]"
option-value="name"
options-cover
/>
</div>
</template>
<template #body="drinks_props">
<q-tr :props="drinks_props">
<q-td auto-width>
<q-btn
v-if="drinks_props.row.volumes.length === 0"
size="xs"
color="negative"
round
dense
icon="mdi-delete"
class="q-mx-sm"
@click="deleteDrink(drinks_props.row)"
/>
</q-td>
<q-td key="picture" :props="drinks_props" style="width: 128px">
<q-img
:src="`http://localhost/api/pricelist/drinks/${drinks_props.row.id}/picture?size=128`"
/>
<q-popup-edit
v-slot="scope"
v-model="drinkPic"
buttons
label-set="Speichern"
label-cancel="Abbrechen"
@update:modelValue="savePicture(drinks_props.row)"
>
<q-file v-model="scope.value" filled>
<template #prepend>
<q-icon name="attach_file" />
</template>
</q-file>
</q-popup-edit>
</q-td>
<q-td key="name" :props="drinks_props">
{{ drinks_props.row.name }}
<q-popup-edit
v-slot="scope"
v-model="drinks_props.row.name"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@update:modelValue="updateDrink(drinks_props.row)"
>
<q-input
v-model="scope.value"
filled
dense
autofocus
clearable
@keyup.enter="scope.set"
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
<q-card>
<q-img style="max-height: 256px" :src="image(props.row.uuid)">
<div
v-if="!public && !nodetails && editable"
class="absolute-top-right justify-end"
style="background-color: transparent"
>
<q-btn
round
icon="mdi-pencil"
style="background-color: rgba(0, 0, 0, 0.5)"
@click="editDrink = props.row"
/>
</div>
<div class="absolute-bottom-right justify-end">
<div class="text-subtitle1 text-right">
{{ props.row.name }}
</div>
<div class="text-caption text-right">
{{ props.row.type.name }}
</div>
</div>
</q-img>
<q-card-section>
<q-badge
v-for="tag in props.row.tags"
:key="`${props.row.id}-${tag.id}`"
class="text-caption"
rounded
:style="`background-color: ${tag.color}`"
>
{{ tag.name }}
</q-badge>
</q-card-section>
<q-card-section v-if="!public && !nodetails">
<div class="fit row">
<q-input
v-if="props.row.article_id"
class="col-xs-12 col-sm-6 q-pa-sm"
:model-value="props.row.article_id"
outlined
readonly
label="Artikelnummer"
dense
/>
<q-input
v-if="props.row.volume"
class="col-xs-12 col-sm-6 q-pa-sm"
:model-value="props.row.volume"
outlined
readonly
label="Inhalt"
dense
suffix="L"
/>
<q-input
v-if="props.row.package_size"
class="col-xs-12 col-sm-6 q-pa-sm"
:model-value="props.row.package_size"
outlined
readonly
label="Gebindegröße"
dense
/>
<q-input
v-if="props.row.cost_per_package"
class="col-xs-12 col-sm-6 q-pa-sm"
:model-value="props.row.cost_per_package"
outlined
readonly
label="Preis Gebinde"
suffix="€"
dense
/>
<q-input
v-if="props.row.cost_per_volume"
class="col-xs-12 col-sm-6 q-pa-sm q-pb-lg"
:model-value="props.row.cost_per_volume"
outlined
readonly
label="Preis pro L"
hint="Inkl. 19% Mehrwertsteuer"
suffix="€"
dense
/>
</div>
</q-card-section>
<q-card-section v-if="props.row.volumes.length > 0 && notLoading">
<drink-price-volumes
:model-value="props.row.volumes"
:public="public"
:nodetails="nodetails"
/>
</q-popup-edit>
</q-td>
<q-td key="drink_type" :props="drinks_props">
{{ drinks_props.row.type.name }}
<q-popup-edit
v-slot="scope"
v-model="drinks_props.row.type"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@update:modelValue="updateDrink(drinks_props.row)"
>
<q-select
v-model="scope.value"
:options="drinkTypes"
option-label="name"
filled
dense
autofocus
@keyup.enter="scope.set"
/>
</q-popup-edit>
</q-td>
<q-td key="article_id" :props="drinks_props">
{{ drinks_props.row.article_id || 'o.A.' }}
<q-popup-edit
v-slot="scope"
v-model="drinks_props.row.article_id"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@update:modelValue="updateDrink(drinks_props.row)"
>
<q-input
v-model="scope.value"
filled
dense
autofocus
clearable
@keyup.enter="scope.set"
/>
</q-popup-edit>
</q-td>
<q-td key="volume_package" :props="drinks_props">
{{ drinks_props.row.volume ? `${drinks_props.row.volume} L` : 'o.A.' }}
<q-popup-edit
v-if="
!drinks_props.row.volumes.some((volume) =>
volume.ingredients.some((ingredient) => ingredient.drink_ingredient)
)
"
v-slot="scope"
v-model.number="drinks_props.row.volume"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@update:modelValue="updateDrink(drinks_props.row)"
>
<q-input
v-model.number="scope.value"
filled
dense
autofocus
type="number"
clearable
step="0.01"
min="0"
suffix="L"
@keyup.enter="scope.set"
/>
</q-popup-edit>
</q-td>
<q-td key="package_size" :props="drinks_props">
{{ drinks_props.row.package_size || 'o.A.' }}
<q-popup-edit
v-if="
!drinks_props.row.volumes.some((volume) =>
volume.ingredients.some((ingredient) => ingredient.drink_ingredient)
)
"
v-slot="scope"
v-model="drinks_props.row.package_size"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@update:modelValue="updateDrink(drinks_props.row)"
>
<q-input
v-model.number="scope.value"
filled
dense
autofocus
type="number"
min="0"
@keyup.enter="scope.set"
/>
</q-popup-edit>
</q-td>
<q-td key="cost_price_package_netto" :props="drinks_props">
{{
drinks_props.row.cost_price_package_netto
? `${drinks_props.row.cost_price_package_netto.toFixed(2)}`
: 'o.A.'
}}
<q-popup-edit
v-if="
!drinks_props.row.volumes.some((volume) =>
volume.ingredients.some((ingredient) => ingredient.drink_ingredient)
)
"
v-slot="scope"
v-model="drinks_props.row.cost_price_package_netto"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@update:modelValue="updateDrink(drinks_props.row)"
>
<q-input
v-model.number="scope.value"
filled
dense
autofocus
type="number"
step="0.01"
min="0"
suffix="€"
@keyup.enter="scope.set"
/>
</q-popup-edit>
</q-td>
<q-td key="cost_price_pro_volume" :props="drinks_props">
{{
drinks_props.row.cost_price_pro_volume
? `${drinks_props.row.cost_price_pro_volume.toFixed(3)}`
: 'o.A.'
}}
<q-popup-edit
v-if="
!(
!!drinks_props.row.cost_price_package_netto &&
!!drinks_props.row.volume &&
!!drinks_props.row.package_size
) &&
!drinks_props.row.volumes.some((volume) =>
volume.ingredients.some((ingredient) => ingredient.drink_ingredient)
)
"
v-slot="scope"
v-model="drinks_props.row.cost_price_pro_volume"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@update:modelValue="updateDrink(drinks_props.row)"
>
<q-input
v-model.number="scope.value"
filled
dense
autofocus
type="number"
min="0"
step="0.1"
suffix="€"
@keyup.enter="scope.set"
/>
</q-popup-edit>
</q-td>
<q-td key="volumes" :props="drinks_props">
<drink-price-volumes-table
:rows="drinks_props.row.volumes"
:visible-columns="visibleColumn"
:columns="column_calc"
:drink="drinks_props.row"
@updateDrink="updateDrink(drinks_props.row)"
/>
</q-td>
</q-tr>
</q-card-section>
</q-card>
</div>
</template>
</q-table>
<q-dialog :model-value="editDrink !== undefined" persistent>
<drink-modify
:drink="editDrink"
@save="editing_drink"
@cancel="editDrink = undefined"
@delete="deleteDrink"
/>
</q-dialog>
</template>
<script lang="ts">
import {
defineComponent,
onBeforeMount,
ComputedRef,
computed,
ref,
getCurrentInstance,
} from 'vue';
import DrinkPriceVolumesTable from 'src/plugins/pricelist/components/CalculationTable/DrinkPriceVolumesTable.vue';
import { useMainStore } from 'src/stores';
import { Drink, usePricelistStore } from 'src/plugins/pricelist/store';
import { defineComponent, onBeforeMount, ComputedRef, computed, ref } from 'vue';
import { Drink, usePricelistStore, DrinkPriceVolume } from 'src/plugins/pricelist/store';
import MinPriceSetting from 'src/plugins/pricelist/components/MinPriceSetting.vue';
import NewDrink from 'src/plugins/pricelist/components/CalculationTable/NewDrink.vue';
import SearchInput from './SearchInput.vue';
import DrinkPriceVolumes from 'src/plugins/pricelist/components/CalculationTable/DrinkPriceVolumes.vue';
import DrinkModify from './DrinkModify.vue';
import { filter, Search } from '../utils/filter';
import { Notify } from 'quasar';
import { sort } from '../utils/sort';
import { DeleteObjects } from 'src/plugins/pricelist/utils/utils';
import { hasPermission } from 'src/utils/permission';
import { PERMISSIONS } from 'src/plugins/pricelist/permissions';
import { baseURL } from 'src/config';
function sort(a: string | number, b: string | number) {
if (a > b) return 1;
if (b > a) return -1;
return 0;
}
export default defineComponent({
name: 'CalculationTable',
components: { MinPriceSetting, DrinkPriceVolumesTable, NewDrink },
setup() {
const mainStore = useMainStore();
components: {
SearchInput,
MinPriceSetting,
DrinkPriceVolumes,
DrinkModify,
},
props: {
public: {
type: Boolean,
default: false,
},
editable: {
type: Boolean,
default: false,
},
nodetails: {
type: Boolean,
default: false,
},
},
setup(props) {
const store = usePricelistStore();
const root = getCurrentInstance()?.proxy;
onBeforeMount(() => {
store.getPriceCalcColumn(user);
void store.getDrinks();
});
const user = mainStore.currentUser.userid;
const columns = [
{
name: 'picture',
@ -319,18 +191,14 @@ export default defineComponent({
},
{
name: 'name',
label: 'Getränkename',
label: 'Name',
field: 'name',
sortable: true,
sort,
filterable: true,
public: true,
},
{
name: 'article_id',
label: 'Artikelnummer',
field: 'article_id',
sortable: true,
sort,
},
{
name: 'drink_type',
label: 'Kategorie',
@ -338,6 +206,34 @@ export default defineComponent({
format: (val: FG.DrinkType) => `${val.name}`,
sortable: true,
sort: (a: FG.DrinkType, b: FG.DrinkType) => sort(a.name, b.name),
filterable: true,
public: true,
},
{
name: 'tags',
label: 'Tags',
field: 'tags',
format: (val: Array<FG.Tag>) => {
let retVal = '';
val.forEach((tag, index) => {
if (index > 0) {
retVal += ', ';
}
retVal += tag.name;
});
return retVal;
},
filterable: true,
public: true,
},
{
name: 'article_id',
label: 'Artikelnummer',
field: 'article_id',
sortable: true,
sort,
filterable: true,
public: false,
},
{
name: 'volume_package',
@ -345,6 +241,7 @@ export default defineComponent({
field: 'volume',
sortable: true,
sort,
public: false,
},
{
name: 'package_size',
@ -352,19 +249,21 @@ export default defineComponent({
field: 'package_size',
sortable: true,
sort,
public: false,
},
{
name: 'cost_price_package_netto',
name: 'cost_per_package',
label: 'Preis Netto/Gebinde',
field: 'cost_price_package_netto',
field: 'cost_per_package',
format: (val: number | null) => (val ? `${val.toFixed(3)}` : ''),
sortable: true,
sort,
public: false,
},
{
name: 'cost_price_pro_volume',
name: 'cost_per_volume',
label: 'Preis mit 19%/Liter',
field: 'cost_price_pro_volume',
field: 'cost_per_volume',
format: (val: number | null) => (val ? `${val.toFixed(3)}` : ''),
sortable: true,
sort: (a: ComputedRef, b: ComputedRef) => sort(a.value, b.value),
@ -373,6 +272,35 @@ export default defineComponent({
name: 'volumes',
label: 'Preiskalkulation',
field: 'volumes',
format: (val: Array<DrinkPriceVolume>) => {
let retVal = '';
val.forEach((val, index) => {
if (index > 0) {
retVal += ', ';
}
retVal += val.id;
});
return retVal;
},
sortable: false,
},
{
name: 'receipt',
label: 'Bauanleitung',
field: 'receipt',
format: (val: Array<string>) => {
let retVal = '';
val.forEach((value, index) => {
if (index > 0) {
retVal += ', ';
}
retVal += value;
});
return retVal;
},
filterable: true,
sortable: false,
public: false,
},
];
const column_calc = [
@ -410,16 +338,17 @@ export default defineComponent({
field: 'public',
},
];
const visibleColumn = computed({
get: () => store.pricecalc_columns,
set: (val) => {
store.updatePriceCalcColumn(user, val);
},
});
// eslint-disable-next-line vue/return-in-computed-property
const pagination = computed(() => {
rowsPerPage: store.drinks.length;
const search_keys = computed(() =>
columns.filter(
(column) => column.filterable && (props.public || props.nodetails ? column.public : true)
)
);
const pagination = ref({
sortBy: 'name',
descending: false,
rowsPerPage: store.drinks.length,
});
const drinkTypes = computed(() => store.drinkTypes);
@ -427,8 +356,12 @@ export default defineComponent({
function updateDrink(drink: Drink) {
void store.updateDrink(drink);
}
function deleteDrink(drink: Drink) {
store.deleteDrink(drink);
function deleteDrink() {
if (editDrink.value) {
store.deleteDrink(editDrink.value);
}
editDrink.value = undefined;
}
const showNewDrink = ref(false);
@ -447,19 +380,102 @@ export default defineComponent({
drinkPic.value = undefined;
}
function savePicture(drink: Drink) {
console.log('hier bin ich!!!', drinkPic.value);
if (drinkPic.value && drinkPic.value instanceof File)
store
.upload_drink_picture(drink, drinkPic.value)
.then(() => {
root?.$forceUpdate();
})
.catch((response: Response) => {
if (response && response.status == 400) {
onPictureRejected();
}
});
async function savePicture(drinkPic: File) {
if (editDrink.value) {
await store.upload_drink_picture(editDrink.value, drinkPic).catch((response: Response) => {
if (response && response.status == 400) {
onPictureRejected();
}
});
}
}
async function deletePicture() {
if (editDrink.value) {
await store.delete_drink_picture(editDrink.value);
}
}
const search = ref<Search>({
value: '',
key: '',
label: '',
});
const emptyDrink: Drink = {
id: -1,
article_id: undefined,
package_size: undefined,
name: '',
volume: undefined,
cost_per_volume: undefined,
cost_per_package: undefined,
tags: [],
type: undefined,
volumes: [],
uuid: '',
};
function newDrink() {
editDrink.value = Object.assign({}, emptyDrink);
}
const editDrink = ref<Drink>();
async function editing_drink(
drink: Drink,
toDeleteObjects: DeleteObjects,
drinkPic: File | undefined,
deletePic: boolean
) {
notLoading.value = false;
for (const ingredient of toDeleteObjects.ingredients) {
await store.deleteIngredient(ingredient);
}
for (const price of toDeleteObjects.prices) {
await store.deletePrice(price);
}
for (const volume of toDeleteObjects.volumes) {
await store.deleteVolume(volume, drink);
}
if (drink.id > 0) {
await store.updateDrink(drink);
} else {
const _drink = await store.setDrink(drink);
if (editDrink.value) {
editDrink.value.id = _drink.id;
}
}
if (deletePic) {
await deletePicture();
}
if (drinkPic instanceof File) {
await savePicture(drinkPic);
}
editDrink.value = undefined;
notLoading.value = true;
}
function get_volumes(drink_id: number) {
return store.drinks.find((a) => a.id === drink_id)?.volumes;
}
const notLoading = ref(true);
const imageloading = ref<Array<{ id: number; loading: boolean }>>([]);
function getImageLoading(id: number) {
const loading = imageloading.value.find((a) => a.id === id);
if (loading) {
return loading.loading;
}
return false;
}
function image(uuid: string | undefined) {
if (uuid) {
return `${baseURL.value}/pricelist/picture/${uuid}?size=256`;
}
return 'no-image.svg';
}
return {
@ -468,14 +484,26 @@ export default defineComponent({
columns,
column_calc,
column_prices,
visibleColumn,
drinkTypes,
updateDrink,
deleteDrink,
showNewDrink,
drinkPic,
savePicture,
console,
deletePicture,
search,
filter,
search_keys,
tags: computed(() => store.tags),
editDrink,
editing_drink,
get_volumes,
notLoading,
getImageLoading,
newDrink,
hasPermission,
PERMISSIONS,
image,
};
},
});

View File

@ -0,0 +1,60 @@
<template>
<div
v-for="(step, index) in steps"
:key="index"
class="full-width row q-gutter-sm justify-between q-py-sm"
>
<div class="row">
<div>{{ index + 1 }}.</div>
<div class="q-pl-sm">
{{ step }}
</div>
</div>
<q-btn
v-if="editable"
round
color="negative"
size="sm"
icon="mdi-delete"
@click="deleteStep(index)"
/>
</div>
<div v-if="editable" class="full-width row q-gutter-sm justify-between">
<q-input v-model="newStep" filled label="Arbeitsschritt" dense />
<q-btn label="Schritt hinzufügen" dense @click="addStep" />
</div>
</template>
<script lang="ts">
import { PropType, defineComponent, ref } from 'vue';
export default defineComponent({
name: 'BuildManual',
props: {
steps: {
type: Array as PropType<Array<string>>,
default: undefined,
},
editable: {
type: Boolean,
default: true,
},
},
emits: {
deleteStep: (index: number) => index,
addStep: (val: string) => val,
},
setup(_, { emit }) {
const newStep = ref('');
function deleteStep(index: number) {
emit('deleteStep', index);
}
function addStep() {
emit('addStep', newStep.value);
newStep.value = '';
}
return { newStep, addStep, deleteStep };
},
});
</script>
<style scoped></style>

View File

@ -1,72 +0,0 @@
<template>
<q-btn
v-if="pricePerVolume"
color="positive"
icon-right="add"
label="Abgabe hinzufügen"
size="xs"
>
<q-menu anchor="center middle" self="center middle">
<div class="row justify-around q-pa-sm">
<q-input
v-model.number="newVolume.volume"
filled
dense
label="Liter"
type="number"
min="0"
step="0.01"
/>
</div>
<div class="row justify-between q-pa-sm">
<q-btn v-close-popup label="Abbrechen" @click="cancelAddVolume" />
<q-btn v-close-popup label="Speichern" color="primary" @click="addVolume(rows)" />
</div>
</q-menu>
</q-btn>
</template>
<script lang="ts">
import { DrinkPriceVolume } from '../../../store';
import { ref, defineComponent } from 'vue';
export default defineComponent({
name: 'NewVolume',
props: {
pricePerVolume: {
type: undefined,
required: true,
},
},
emits: { addVolume: (val: DrinkPriceVolume) => !!val },
setup(_, { emit }) {
const emptyVolume: DrinkPriceVolume = {
id: -1,
_volume: 0,
min_prices: [{ percentage: 100 }, { percentage: 250 }, { percentage: 300 }],
prices: [],
ingredients: [],
};
const newVolume = ref<DrinkPriceVolume>(emptyVolume);
function cancelAddVolume() {
setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
newVolume.value = emptyVolume;
}, 200);
}
function addVolume() {
emit('addVolume', <DrinkPriceVolume>newVolume.value);
}
return {
addVolume,
cancelAddVolume,
newVolume,
};
},
});
</script>
<style scoped></style>

View File

@ -0,0 +1,340 @@
<template>
<q-carousel
v-model="volume"
transition-prev="slide-right"
transition-next="slide-left"
animated
swipeable
control-color="primary"
arrows
:keep-alive="false"
>
<q-carousel-slide v-for="volume in volumes" :key="volume.id" :name="volume.id">
<div class="full-width row">
<q-input
v-model.number="volume._volume"
class="q-pa-sm col-10"
:outlined="!editable || !volume_can_edit"
:filled="editable && volume_can_edit"
:readonly="!editable || !volume_can_edit"
dense
label="Inhalt"
mask="#.###"
fill-mask="0"
suffix="L"
min="0"
step="0.001"
@update:model-value="updateVolume(volume)"
/>
<div
v-if="deleteable && editable && hasPermission(PERMISSIONS.DELETE_VOLUME)"
class="q-pa-sm col-2 text-right"
>
<q-btn round icon="mdi-delete" size="sm" color="negative" @click="deleteVolume">
<q-tooltip> Abgabe entfernen </q-tooltip>
</q-btn>
</div>
</div>
<div v-if="!public && !nodetails" class="full-width row q-gutter-sm q-pa-sm justify-around">
<div v-for="(min_price, index) in volume.min_prices" :key="index">
<q-badge class="text-body1" color="primary"> {{ min_price.percentage }}% </q-badge>
<div class="text-body1">{{ min_price.price.toFixed(3) }}</div>
</div>
</div>
<div class="q-pa-sm">
<div v-for="(price, index) in volume.prices" :key="price.id">
<div class="fit row justify-around q-py-sm">
<div
v-if="!editable || !hasPermission(PERMISSIONS.EDIT_PRICE)"
class="text-body1 col-3"
>
{{ price.price.toFixed(2) }}
</div>
<q-input
v-else
v-model.number="price.price"
class="col-3"
type="number"
min="0"
step="0.01"
suffix="€"
filled
dense
label="Preis"
@update:model-value="change"
/>
<div class="text-body1 col-2">
<q-toggle
v-model="price.public"
:disable="!editable || !hasPermission(PERMISSIONS.EDIT_PRICE)"
checked-icon="mdi-earth"
unchecked-icon="mdi-earth-off"
@update:model-value="change"
/>
</div>
<div
v-if="!editable || !hasPermission(PERMISSIONS.EDIT_PRICE)"
class="text-body1 col-5"
>
{{ price.description }}
</div>
<q-input
v-else
v-model="price.description"
class="col-5"
filled
dense
label="Beschreibung"
@update:model-value="change"
/>
<div v-if="editable && hasPermission(PERMISSIONS.DELETE_PRICE)" class="col-1">
<q-btn round icon="mdi-delete" color="negative" size="xs" @click="deletePrice(price)">
<q-tooltip> Preis entfernen </q-tooltip>
</q-btn>
</div>
</div>
<q-separator v-if="index < volume.prices.length - 1" />
</div>
<div
v-if="!public && !nodetails && isUnderMinPrice"
class="fit warning bg-red text-center text-white text-body1"
>
Einer der Preise ist unterhalb des niedrigsten minimal Preises.
</div>
<div
v-if="editable && hasPermission(PERMISSIONS.EDIT_PRICE)"
class="full-width row justify-end text-right"
>
<q-btn round icon="mdi-plus" size="sm" color="primary">
<q-tooltip> Preis hinzufügen </q-tooltip>
<q-menu anchor="center middle" self="center middle">
<new-price @save="addPrice" />
</q-menu>
</q-btn>
</div>
</div>
<div class="q-pa-sm">
<ingredients
v-if="!public && !costPerVolume"
v-model="volume.ingredients"
:editable="editable && hasPermission(PERMISSIONS.EDIT_INGREDIENTS_DRINK)"
@update="updateVolume(volume)"
@delete-ingredient="deleteIngredient"
/>
</div>
</q-carousel-slide>
</q-carousel>
<div class="full-width row justify-center q-pa-sm">
<div class="q-px-sm">
<q-btn-toggle v-model="volume" :options="options" rounded />
</div>
<div v-if="editable" class="q-px-sm">
<q-btn class="q-px-sm" round icon="mdi-plus" color="primary" size="sm" @click="newVolume">
<q-tooltip> Abgabe hinzufügen </q-tooltip>
</q-btn>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref, onBeforeMount } from 'vue';
import { DrinkPriceVolume } from 'src/plugins/pricelist/store';
import Ingredients from 'src/plugins/pricelist/components/CalculationTable/Ingredients.vue';
import NewPrice from 'src/plugins/pricelist/components/CalculationTable/NewPrice.vue';
import { calc_volume, clone } from '../../utils/utils';
import { hasPermission } from 'src/utils/permission';
import { PERMISSIONS } from '../../permissions';
export default defineComponent({
name: 'DrinkPriceVolume',
components: { Ingredients, NewPrice },
props: {
modelValue: {
type: Array as PropType<Array<DrinkPriceVolume>>,
required: true,
},
costPerVolume: {
type: undefined,
default: undefined,
},
editable: {
type: Boolean,
default: false,
},
public: {
type: Boolean,
default: false,
},
nodetails: {
type: Boolean,
default: false,
},
},
emits: {
'update:modelValue': (val: Array<DrinkPriceVolume>) => val,
update: (val: number) => val,
'delete-volume': (val: DrinkPriceVolume) => val,
'delete-price': (val: FG.DrinkPrice) => val,
'delete-ingredient': (val: FG.Ingredient) => val,
},
setup(props, { emit }) {
onBeforeMount(() => {
//volumes.value = <Array<DrinkPriceVolume>>JSON.parse(JSON.stringify(props.modelValue));
volumes.value = clone(props.modelValue);
});
const volumes = ref<Array<DrinkPriceVolume>>([]);
const _volume = ref<number | undefined>();
const volume = computed<number | undefined>({
get: () => {
if (_volume.value !== undefined) {
return _volume.value;
}
if (volumes.value.length > 0) {
return volumes.value[0].id;
}
return undefined;
},
set: (val: number | undefined) => (_volume.value = val),
});
const edit_volume = computed(() => {
return volumes.value.find((a) => a.id === volume.value);
});
const options = computed<Array<{ label: string; value: number }>>(() => {
const retVal: Array<{ label: string; value: number }> = [];
volumes.value.forEach((volume: DrinkPriceVolume) => {
retVal.push({ label: `${(<number>volume.volume).toFixed(3)}L`, value: volume.id });
});
return retVal;
});
function updateVolume(_volume: DrinkPriceVolume) {
const index = volumes.value.findIndex((a) => a.id === _volume.id);
if (index > -1) {
volumes.value[index].volume = calc_volume(_volume);
volumes.value[index]._volume = <number>volumes.value[index].volume;
}
change();
setTimeout(() => {
emit('update', index);
}, 50);
}
const volume_can_edit = computed(() => {
if (edit_volume.value) {
return !edit_volume.value.ingredients.some((ingredient) => ingredient.drink_ingredient);
}
return true;
});
const newVolumeId = ref(-1);
function newVolume() {
const new_volume: DrinkPriceVolume = {
id: newVolumeId.value,
_volume: 0,
volume: 0,
prices: [],
ingredients: [],
min_prices: [],
};
newVolumeId.value--;
volumes.value.push(new_volume);
change();
_volume.value = volumes.value[volumes.value.length - 1].id;
}
function deleteVolume() {
if (edit_volume.value) {
if (edit_volume.value.id > 0) {
emit('delete-volume', edit_volume.value);
}
const index = volumes.value.findIndex((a) => a.id === edit_volume.value?.id);
if (index > -1) {
_volume.value = volumes.value[0].id;
volumes.value.splice(index, 1);
}
}
}
const deleteable = computed(() => {
if (edit_volume.value) {
const has_ingredients = edit_volume.value.ingredients.length > 0;
const has_prices = edit_volume.value.prices.length > 0;
return !(has_ingredients || has_prices);
}
return true;
});
function addPrice(price: FG.DrinkPrice) {
if (edit_volume.value) {
edit_volume.value.prices.push(price);
change();
}
}
function deletePrice(price: FG.DrinkPrice) {
if (edit_volume.value) {
const index = edit_volume.value.prices.findIndex((a) => a.id === price.id);
if (index > -1) {
if (edit_volume.value.prices[index].id > 0) {
emit('delete-price', edit_volume.value.prices[index]);
change();
}
edit_volume.value.prices.splice(index, 1);
}
}
}
function deleteIngredient(ingredient: FG.Ingredient) {
emit('delete-ingredient', ingredient);
}
function change() {
emit('update:modelValue', volumes.value);
}
const isUnderMinPrice = computed(() => {
if (volumes.value) {
const this_volume = volumes.value.find((a) => a.id === volume.value);
if (this_volume) {
if (this_volume.min_prices.length > 0) {
const min_price = this_volume.min_prices.sort((a, b) => {
if (a.price > b.price) return 1;
if (a.price < b.price) return -1;
return 0;
})[0];
console.log('min_price', min_price);
return this_volume.prices.some((a) => a.price < min_price.price);
}
}
}
return false;
});
return {
volumes,
volume,
options,
updateVolume,
volume_can_edit,
newVolume,
deleteable,
addPrice,
deletePrice,
deleteVolume,
deleteIngredient,
change,
isUnderMinPrice,
hasPermission,
PERMISSIONS,
};
},
});
</script>
<style scoped>
.warning {
border-radius: 5px;
}
</style>

View File

@ -1,193 +0,0 @@
<template>
<q-table
:columns="columns"
:rows="rows"
dense
:visible-columns="visibleColumns"
row-key="id"
flat
>
<template #header="props">
<q-tr :props="props">
<q-th auto-width />
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
v-if="!drink.cost_price_pro_volume"
size="sm"
color="accent"
round
dense
:icon="props.expand ? 'mdi-chevron-up' : 'mdi-chevron-down'"
@click="props.expand = !props.expand"
/>
<q-btn
v-if="props.row.ingredients.length === 0 && props.row.prices.length === 0"
size="xs"
color="negative"
round
dense
icon="mdi-delete"
class="q-mx-sm"
@click="deleteVolume(props.row, drink)"
/>
</q-td>
<q-td key="volume" :props="props">
{{ parseFloat(props.row.volume).toFixed(3) }}L
<q-popup-edit
v-if="rows.cost_price_pro_volume"
v-model="props.row.volume"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@save="updateVolume(props.row, drink)"
>
<q-input v-model.number="props.row.volume" dense filled type="number" suffix="L" />
</q-popup-edit>
</q-td>
<q-td key="min_prices" :props="props">
<div
v-for="(min_price, index) in props.row.min_prices"
:key="`min_prices` + index"
class="row justify-between"
>
<div class="col">
<q-badge color="primary">{{ min_price.percentage }}%</q-badge>
</div>
<div class="col" style="text-align: end">
{{ min_price.price ? min_price.price.toFixed(3) : Number(0).toFixed(2) }}
</div>
</div>
</q-td>
<q-td key="prices" :props="props">
<price-table
:columns="column_prices"
:rows="props.row.prices"
:row="props.row"
:visible-columns="visibleColumns"
@updateDrink="updateDrink"
/>
</q-td>
</q-tr>
<q-tr v-show="props.expand" :props="props">
<q-td colspan="100%">
<ingredients
:ingredients="props.row.ingredients"
:volume="props.row"
@updateDrink="updateDrink"
/>
</q-td>
</q-tr>
</template>
<template #bottom>
<div class="full-width row justify-end">
<new-volume
:price-per-volume="drink.cost_price_pro_volume"
@addVolume="addVolume($event, drink)"
/>
</div>
</template>
<template #no-data>
<div class="full-width row justify-end">
<new-volume
:price-per-volume="drink.cost_price_pro_volume"
@addVolume="addVolume($event, drink)"
/>
</div>
</template>
</q-table>
</template>
<script lang="ts">
import { Drink, DrinkPriceVolume, usePricelistStore } from '../../store';
import PriceTable from 'src/plugins/pricelist/components/CalculationTable/PriceTable.vue';
import Ingredients from 'src/plugins/pricelist/components/CalculationTable/Ingredients.vue';
import NewVolume from 'src/plugins/pricelist/components/CalculationTable/DrinkPriceVolumeTable/NewVolume.vue';
import { PropType, defineComponent } from 'vue';
const columns = [
{
name: 'volume',
label: 'Abgabe in l',
field: 'volume',
},
{
name: 'min_prices',
label: 'Minimal Preise',
field: 'min_prices',
},
{
name: 'prices',
label: 'Preise',
field: 'prices',
},
];
export default defineComponent({
name: 'DrinkPriceVolumsTable',
components: { PriceTable, Ingredients, NewVolume },
props: {
visibleColumns: {
type: Array,
default: columns,
},
columns: {
type: Array,
default: columns,
},
rows: {
type: Array as PropType<Array<DrinkPriceVolume>>,
required: true,
},
drink: {
type: Object as PropType<Drink>,
required: true,
},
},
emits: { updateDrink: () => true },
setup(_, { emit }) {
const store = usePricelistStore();
function addVolume(volume: DrinkPriceVolume, drink: Drink) {
drink.volumes.push(volume);
updateDrink();
}
function updateDrink() {
emit('updateDrink');
}
function deleteVolume(volume: FG.DrinkPriceVolume, drink: FG.Drink) {
store.deleteVolume(volume, drink);
}
const column_prices = [
{
name: 'price',
label: 'Preis',
field: 'price',
format: (val: number) => `${val.toFixed(2)}`,
},
{
name: 'description',
label: 'Beschreibung',
field: 'description',
},
{
name: 'public',
label: 'Öffentlich',
field: 'public',
},
];
return { addVolume, updateDrink, deleteVolume, column_prices };
},
});
</script>
<style scoped></style>

View File

@ -1,24 +1,26 @@
<template>
<div class="full-width">
<div
v-for="ingredient in ingredients"
:key="`volume:${volume.id},ingredient:${ingredient.id}`"
v-for="ingredient in edit_ingredients"
:key="`ingredient:${ingredient.id}`"
class="full-width row justify-evenly q-py-xs"
>
<div class="full-width row justify-evenly">
<div v-if="ingredient.drink_ingredient" class="col">
<div class="full-width row justify-evenly q-py-xs">
<div class="col">
{{ get_drink_ingredient_name(ingredient.drink_ingredient.drink_ingredient_id) }}
{{ get_drink_ingredient_name(ingredient.drink_ingredient.ingredient_id) }}
<q-popup-edit
v-model="ingredient.drink_ingredient.drink_ingredient_id"
v-if="editable"
v-slot="scope"
v-model="ingredient.drink_ingredient.ingredient_id"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@save="updateDrink"
@save="updateValue"
>
<q-select
v-model="ingredient.drink_ingredient.drink_ingredient_id"
v-model="scope.ingredient_id"
class="col q-px-sm"
label="Getränk"
filled
@ -34,14 +36,16 @@
<div class="col">
{{ ingredient.drink_ingredient.volume.toFixed(3) }}L
<q-popup-edit
v-if="editable"
v-slot="scope"
v-model="ingredient.drink_ingredient.volume"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@save="updateDrink"
@save="updateValue"
>
<q-input
v-model.number="ingredient.drink_ingredient.volume"
v-model.number="scope.value"
class="col q-px-sm"
label="Volume"
type="number"
@ -63,11 +67,12 @@
<div class="col">{{ ingredient.extra_ingredient.price.toFixed(3) }}</div>
</div>
<q-popup-edit
v-if="editable"
v-model="ingredient.extra_ingredient"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@save="updateDrink"
@save="updateValue"
>
<q-select
v-model="ingredient.extra_ingredient"
@ -78,21 +83,24 @@
/>
</q-popup-edit>
</div>
<div class="col-1 row justify-end q-pa-xs">
<div v-if="editable" class="col-1 row justify-end q-pa-xs">
<q-btn
icon="mdi-delete"
round
size="xs"
color="negative"
@click="deleteIngredient(ingredient, volume)"
/>
@click="deleteIngredient(ingredient)"
>
<q-tooltip> Zutat entfernen </q-tooltip>
</q-btn>
</div>
</div>
<q-separator />
</div>
<div class="full-width row justify-end q-py-xs">
<q-btn size="sm" icon-right="add" color="positive" label="Zutat hinzufügen">
<q-menu anchor="center middle" self="center middle">
<div v-if="editable" class="full-width row justify-end q-py-xs">
<q-btn size="sm" round icon="mdi-plus" color="primary">
<q-tooltip> Neue Zutat hinzufügen </q-tooltip>
<q-menu anchor="center middle" self="center middle" persistent>
<div class="full-width row justify-around q-gutter-sm q-pa-sm">
<div class="col">
<q-select
@ -105,40 +113,39 @@
/>
</div>
<div class="col">
<q-input
v-if="newIngredient && newIngredient.volume"
v-model.number="newIngredientVolume"
filled
dense
label="Volume"
type="number"
step="0.01"
min="0"
suffix="L"
/>
<q-input
v-else-if="newIngredient && newIngredient.price"
v-model="newIngredient.price"
filled
dense
label="Preis"
disable
min="0"
step="0.1"
fill-mask="0"
mask="#.##"
suffix="€"
/>
<q-slide-transition>
<q-input
v-if="newIngredient && newIngredient.volume"
v-model.number="newIngredientVolume"
filled
dense
label="Volume"
type="number"
step="0.01"
min="0"
suffix="L"
/>
</q-slide-transition>
<q-slide-transition>
<q-input
v-if="newIngredient && newIngredient.price"
v-model="newIngredient.price"
filled
dense
label="Preis"
disable
min="0"
step="0.1"
fill-mask="0"
mask="#.##"
suffix="€"
/>
</q-slide-transition>
</div>
</div>
<div class="full-width row jusitfy-between q-gutter-sm q-pa-sm">
<q-btn v-close-popup label="Abbrechen" @click="cancelAddIngredient" />
<q-btn
v-close-popup
label="Speichern"
color="positive"
@click="addIngredient(volume)"
/>
<div class="full-width row justify-around q-gutter-sm q-pa-sm">
<q-btn v-close-popup flat label="Abbrechen" @click="cancelAddIngredient" />
<q-btn v-close-popup flat label="Speichern" color="primary" @click="addIngredient" />
</div>
</q-menu>
</q-btn>
@ -147,35 +154,43 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref } from 'vue';
import { DrinkPriceVolume, usePricelistStore } from '../../store';
import { computed, defineComponent, PropType, ref, onBeforeMount, unref } from 'vue';
import { usePricelistStore } from '../../store';
import { clone } from '../../utils/utils';
export default defineComponent({
name: 'Ingredients',
props: {
ingredients: {
type: Array as PropType<FG.Ingredient[]>,
modelValue: {
type: Object as PropType<Array<FG.Ingredient>>,
required: true,
},
volume: {
type: Object /*as PropType<DrinkPriceVolume>*/,
required: true,
editable: {
type: Boolean,
default: false,
},
},
emits: { updateDrink: () => true },
setup(_, { emit }) {
emits: {
'update:modelValue': (val: Array<FG.Ingredient>) => val,
update: () => true,
'delete-ingredient': (val: FG.Ingredient) => val,
},
setup(props, { emit }) {
onBeforeMount(() => {
//edit_ingredients.value = <Array<FG.Ingredient>>JSON.parse(JSON.stringify(props.modelValue))
//edit_ingredients.value = props.modelValue
edit_ingredients.value = clone(props.modelValue);
});
const store = usePricelistStore();
/*const emptyIngredient: FG.Ingredient = {
id: -1,
drink_ingredient: undefined,
extra_ingredient: undefined,
};*/
const edit_ingredients = ref<Array<FG.Ingredient>>([]);
const newIngredient = ref<FG.Drink | FG.ExtraIngredient>();
const newIngredientVolume = ref<number>(0);
function addIngredient(volume: DrinkPriceVolume) {
function addIngredient() {
let _ingredient: FG.Ingredient;
if ((<FG.Drink>newIngredient.value)?.volume && newIngredient.value) {
volume.ingredients.push({
_ingredient = {
id: -1,
drink_ingredient: {
id: -1,
@ -183,33 +198,46 @@ export default defineComponent({
volume: newIngredientVolume.value,
},
extra_ingredient: undefined,
});
} else if (newIngredient.value) {
volume.ingredients.push({
};
} else {
_ingredient = {
id: -1,
drink_ingredient: undefined,
extra_ingredient: <FG.ExtraIngredient>newIngredient.value,
});
};
}
updateDrink();
edit_ingredients.value.push(_ingredient);
emit('update:modelValue', unref(edit_ingredients));
update();
cancelAddIngredient();
}
function updateDrink() {
console.log('updateDrink from Ingredients');
emit('updateDrink');
function updateValue(value: number, initValue: number) {
console.log('updateValue', value, initValue);
emit('update:modelValue', unref(edit_ingredients));
update();
}
function cancelAddIngredient() {
setTimeout(() => {
(newIngredient.value = undefined), (newIngredientVolume.value = 0);
}, 200);
}
function deleteIngredient(ingredient: FG.Ingredient, volume: DrinkPriceVolume) {
store.deleteIngredient(ingredient, volume);
function deleteIngredient(ingredient: FG.Ingredient) {
const index = edit_ingredients.value.findIndex((a) => a.id === ingredient.id);
if (index > -1) {
if (edit_ingredients.value[index].id > 0) {
emit('delete-ingredient', edit_ingredients.value[index]);
}
edit_ingredients.value.splice(index, 1);
}
emit('update:modelValue', unref(edit_ingredients));
update();
}
const drinks = computed(() =>
store.drinks.filter((drink) => {
console.log('computed drinks', drink.name, drink.cost_price_pro_volume);
return drink.cost_price_pro_volume;
console.log('computed drinks', drink.name, drink.cost_per_volume);
return drink.cost_per_volume;
})
);
const extra_ingredients = computed(() => store.extraIngredients);
@ -218,6 +246,12 @@ export default defineComponent({
return store.drinks.find((a) => a.id === id)?.name;
}
function update() {
setTimeout(() => {
emit('update');
}, 50);
}
return {
addIngredient,
drinks,
@ -225,9 +259,10 @@ export default defineComponent({
newIngredient,
newIngredientVolume,
cancelAddIngredient,
updateDrink,
updateValue,
deleteIngredient,
get_drink_ingredient_name,
edit_ingredients,
};
},
});

View File

@ -1,135 +0,0 @@
<template>
<div class="q-pa-sm">
<div class="q-table__title q-pa-sm">Neues Getränk</div>
<q-form @submit="addDrink" @reset="cancelAddDrink">
<q-input
v-model="newDrink.name"
class="col-sm-4 col-xs-6 q-pa-sm"
filled
label="Getränkname"
lazy-rules
:rules="[notEmpty]"
/>
<q-input
v-model="newDrink.article_id"
class="col-sm-4 col-xs-6 q-pa-sm"
filled
label="Artikelnummer"
/>
<q-select
v-model="newDrink.type"
class="col-sm-4 col-xs-6 q-pa-sm"
filled
label="Kategorie"
:options="drinkTypes"
option-label="name"
lazy-rules
:rules="[notEmpty]"
/>
<q-input
v-model.number="newDrink.volume"
class="col-sm-4 col-xs-6 q-pa-sm"
filled
label="Inhalt in L/Gebinde"
type="number"
/>
<q-input
v-model.number="newDrink.package_size"
class="col-sm-4 col-xs-6 q-pa-sm"
filled
label="Gebindegröße"
type="number"
/>
<q-input
v-model.number="newDrink.cost_per_package"
class="col-sm-4 col-xs-6 q-pa-sm"
filled
label="Preis Netto/Gebinde"
type="number"
/>
<q-input
v-model="cost_per_volume"
class="col-sm-4 col-xs-6 q-pa-sm"
filled
label="Preis mit 19%/Liter"
:disable="calc_price_pro_volume"
/>
<div class="row justify-between">
<q-btn v-close-popup label="Abbrechen" type="reset" />
<q-btn label="Speichern" type="submit" />
</div>
</q-form>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue';
import { usePricelistStore } from 'src/plugins/pricelist/store';
import { notEmpty } from 'src/utils/validators';
export default defineComponent({
name: 'NewDrink',
emits: { close: () => true },
setup(_, { emit }) {
const store = usePricelistStore();
const emptyDrink: FG.Drink = {
id: -1,
article_id: undefined,
package_size: undefined,
name: '',
volume: undefined,
cost_per_volume: undefined,
cost_per_package: undefined,
tags: [],
type: undefined,
volumes: [],
};
const calc_price_pro_volume = computed(
() =>
!!newDrink.value.cost_per_package &&
!!newDrink.value.volume &&
!!newDrink.value.package_size
);
const cost_per_volume = computed({
get: () => {
if (calc_price_pro_volume.value) {
const retVal =
((newDrink.value.cost_per_package || 0) /
((newDrink.value.volume || 0) * (newDrink.value.package_size || 0))) *
1.19;
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
newDrink.value.cost_per_volume = Math.round(retVal * 1000) / 1000;
}
return newDrink.value.cost_per_volume;
},
set: (val) => {
newDrink.value.cost_per_volume = val;
},
});
async function addDrink() {
// Maybe try catch and handle error (e.g. name used...)
await store.setDrink(newDrink.value);
cancelAddDrink();
emit('close');
}
function cancelAddDrink() {
newDrink.value = emptyDrink;
}
const newDrink = ref<FG.Drink>(emptyDrink);
return {
drinkTypes: computed(() => store.drinkTypes),
newDrink,
calc_price_pro_volume,
cost_per_volume,
addDrink,
cancelAddDrink,
notEmpty,
};
},
});
</script>
<style scoped></style>

View File

@ -0,0 +1,59 @@
<template>
<div class="row justify-around q-pa-sm">
<q-input
v-model.number="newPrice.price"
dense
filled
class="q-px-sm"
type="number"
label="Preis"
suffix="€"
min="0"
step="0.1"
/>
<q-input
v-model="newPrice.description"
dense
filled
class="q-px-sm"
label="Beschreibung"
clearable
/>
<q-toggle v-model="newPrice.public" dense class="q-px-sm" label="Öffentlich" />
</div>
<div class="row justify-between q-pa-sm">
<q-btn v-close-popup label="Abbrechen" @click="cancelAddPrice" />
<q-btn v-close-popup label="Speichern" color="primary" @click="addPrice(row)" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: 'NewPrice',
emits: {
save: (val: FG.DrinkPrice) => val,
},
setup(_, { emit }) {
const emptyPrice: FG.DrinkPrice = {
id: -1,
price: 0,
description: '',
public: true,
};
const newPrice = ref(emptyPrice);
function addPrice() {
emit('save', newPrice.value);
cancelAddPrice();
}
function cancelAddPrice() {
setTimeout(() => {
newPrice.value = emptyPrice;
}, 200);
}
return { newPrice, addPrice, cancelAddPrice };
},
});
</script>
<style scoped></style>

View File

@ -1,217 +0,0 @@
<template>
<q-table
v-model:pagination="pagination"
style="max-height: 130px"
dense
hide-header
:columns="columns"
:rows="rows"
:visible-columns="visibleColumns"
flat
virtual-scroll
:rows-per-page-options="[0]"
>
<template #body="prices_props">
<q-tr :props="prices_props">
<q-td key="price" :props="prices_props">
{{ prices_props.row.price.toFixed(2) }}
<q-popup-edit
v-slot="scope"
v-model="prices_props.row.price"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@update:modelValue="updateDrink"
>
<q-input
v-model.number="scope.value"
type="number"
label="Preis"
dense
filled
autofocus
min="0"
step="0.1"
suffix="€"
@keyup.enter="scope.set"
/> </q-popup-edit
></q-td>
<q-td key="description" :props="prices_props">
{{ prices_props.row.description }}
<q-popup-edit
v-slot="scope"
v-model="prices_props.row.description"
buttons
label="Beschreibung"
label-cancel="Abbrechen"
label-set="Speichern"
@update:modelValue="updateDrink"
>
<q-input
v-model="scope.value"
dense
autofocus
filled
clearable
@keyup.enter="scope.set"
/>
</q-popup-edit>
</q-td>
<q-td key="public" :props="prices_props">
<q-toggle v-model="prices_props.row.public" dense @update:modelValue="updateDrink" />
</q-td>
<q-td>
<q-btn
color="negative"
padding="xs"
round
size="xs"
icon="mdi-delete"
@click="deletePrice(prices_props.row, row)"
/>
</q-td>
</q-tr>
</template>
<template #bottom>
<div class="full-width row justify-end">
<q-btn size="xs" icon-right="add" color="positive" label="Preis hinzufügen">
<q-menu anchor="center middle" self="center middle">
<div class="row justify-around q-pa-sm">
<q-input
v-model.number="newPrice.price"
dense
filled
class="q-px-sm"
type="number"
label="Preis"
suffix="€"
min="0"
step="0.1"
/>
<q-input
v-model="newPrice.description"
dense
filled
class="q-px-sm"
label="Beschreibung"
clearable
/>
<q-toggle v-model="newPrice.public" dense class="q-px-sm" label="Öffentlich" />
</div>
<div class="row justify-between q-pa-sm">
<q-btn v-close-popup label="Abbrechen" @click="cancelAddPrice" />
<q-btn v-close-popup label="Speichern" color="primary" @click="addPrice(row)" />
</div>
</q-menu>
</q-btn>
</div>
</template>
<template #no-data class="justify-end">
<div class="full-width row justify-end">
<q-btn size="xs" icon-right="add" color="positive" label="Preis hinzufügen">
<q-menu anchor="center middle" self="center middle">
<div class="row justify-around q-pa-sm">
<q-input
v-model.number="newPrice.price"
dense
filled
class="q-px-sm"
type="number"
label="Preis"
suffix="€"
min="0"
step="0.1"
/>
<q-input
v-model="newPrice.description"
dense
filled
class="q-px-sm"
label="Beschreibung"
clearable
/>
<q-toggle v-model="newPrice.public" dense class="q-px-sm" label="Öffentlich" />
</div>
<div class="row justify-between q-pa-sm">
<q-btn v-close-popup label="Abbrechen" @click="cancelAddPrice" />
<q-btn v-close-popup label="Speichern" color="primary" @click="addPrice(row)" />
</div>
</q-menu>
</q-btn>
</div>
</template>
</q-table>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { DrinkPriceVolume, usePricelistStore } from '../../store';
export default defineComponent({
name: 'PriceTable',
props: {
columns: {
type: Array,
required: true,
},
rows: {
type: Array,
required: true,
},
row: {
type: Object /*as PropType<DrinkPriceVolume>*/,
required: true,
},
visibleColumns: {
type: Array,
required: true,
},
},
emits: { updateDrink: () => true },
setup(props, { emit }) {
const store = usePricelistStore();
const emptyPrice: FG.DrinkPrice = {
id: -1,
price: 0,
description: '',
public: true,
};
const newPrice = ref(emptyPrice);
function addPrice(volume: DrinkPriceVolume) {
volume.prices.push(newPrice.value);
updateDrink();
cancelAddPrice();
}
function updateDrink() {
emit('updateDrink');
}
function cancelAddPrice() {
setTimeout(() => {
newPrice.value = emptyPrice;
}, 200);
}
function deletePrice(price: FG.DrinkPrice, volume: FG.DrinkPriceVolume) {
console.log(price, volume);
store.deletePrice(price, volume);
}
const pagination = ref({
rowsPerPage: (<DrinkPriceVolume>props.row).prices.length,
});
return {
newPrice,
addPrice,
cancelAddPrice,
updateDrink,
deletePrice,
pagination,
console,
};
},
});
</script>
<style scoped></style>

View File

@ -0,0 +1,354 @@
<template>
<q-card>
<q-card-section>
<div class="text-h6">Getränk Bearbeiten</div>
</q-card-section>
<q-card-section>
<div class="full-width row">
<q-input
v-model="edit_drink.name"
class="col-xs-12 col-sm-6 q-pa-sm"
filled
label="Name"
dense
/>
<q-select
v-model="edit_drink.type"
class="col-xs-12 col-sm-6 q-pa-sm"
filled
label="Kategorie"
dense
:options="types"
option-label="name"
/>
</div>
</q-card-section>
<q-card-section>
<q-img :src="image" style="max-height: 256px" fit="contain" />
<div class="full-width row">
<div class="col-10 q-pa-sm">
<q-file
v-model="drinkPic"
filled
clearable
dense
@update:model-value="imagePreview"
@clear="imgsrc = undefined"
>
<template #prepend>
<q-icon name="mdi-image" />
</template>
</q-file>
</div>
<div class="col-2 q-pa-sm text-right">
<q-btn round icon="mdi-delete" color="negative" size="sm" @click="delete_pic">
<q-tooltip> Bild entfernen </q-tooltip>
</q-btn>
</div>
</div>
</q-card-section>
<q-card-section>
<q-select
v-model="edit_drink.tags"
multiple
:options="tags"
label="Tags"
option-label="name"
filled
dense
>
<template #selected-item="item">
<q-chip
removable
:tabindex="item.tabindex"
:style="`background-color: ${item.opt.color}`"
@remove="item.removeAtIndex(item.index)"
>
{{ item.opt.name }}
</q-chip>
</template>
<template #option="item">
<q-item v-bind="item.itemProps" v-on="item.itemEvents">
<q-chip :style="`background-color: ${item.opt.color}`">
<q-avatar v-if="item.selected" icon="mdi-check" color="positive" text-color="white" />
{{ item.opt.name }}
</q-chip>
</q-item>
</template>
</q-select>
</q-card-section>
<q-card-section>
<div class="fit row">
<q-input
v-model="edit_drink.article_id"
class="col-xs-12 col-sm-6 q-pa-sm"
filled
label="Artikelnummer"
dense
/>
<q-input
v-model="edit_drink.volume"
class="col-xs-12 col-sm-6 q-pa-sm"
filled
label="Inhalt"
dense
suffix="L"
/>
<q-input
v-model="edit_drink.package_size"
class="col-xs-12 col-sm-6 q-pa-sm"
filled
label="Gebindegröße"
dense
/>
<q-input
v-model="edit_drink.cost_per_package"
class="col-xs-12 col-sm-6 q-pa-sm"
filled
label="Preis Gebinde"
suffix="€"
dense
/>
<q-input
v-model="cost_per_volume"
class="col-xs-12 col-sm-6 q-pa-sm q-pb-lg"
:outlined="auto_cost_per_volume || hasIngredients"
:filled="!auto_cost_per_volume && !hasIngredients"
:readonly="auto_cost_per_volume || hasIngredients"
label="Preis pro L"
hint="Inkl. 19% Mehrwertsteuer"
suffix="€"
dense
/>
</div>
</q-card-section>
<q-card-section :key="key">
<drink-price-volumes
v-model="edit_volumes"
:cost-per-volume="cost_per_volume"
:editable="hasPermission(PERMISSIONS.EDIT_VOLUME)"
@update="updateVolume"
@delete-volume="deleteVolume"
@delete-price="deletePrice"
@delete-ingredient="deleteIngredient"
/>
</q-card-section>
<q-card-section>
<build-manual :steps="edit_drink.receipt" @deleteStep="deleteStep" @addStep="addStep" />
</q-card-section>
<q-card-actions class="justify-around">
<q-btn label="Abbrechen" @click="cancel" />
<q-btn v-if="can_delete" label="Löschen" color="negative" @click="delete_drink" />
<q-btn label="Speichern" color="primary" @click="save" />
</q-card-actions>
</q-card>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, onBeforeMount, computed } from 'vue';
import { Drink, DrinkPriceVolume, usePricelistStore } from '../store';
import DrinkPriceVolumes from './CalculationTable/DrinkPriceVolumes.vue';
import { clone, calc_min_prices, DeleteObjects, calc_cost_per_volume } from '../utils/utils';
import BuildManual from 'src/plugins/pricelist/components/CalculationTable/BuildManual.vue';
import { baseURL } from 'src/config';
import { hasPermission } from 'src/utils/permission';
import { PERMISSIONS } from 'src/plugins/pricelist/permissions';
export default defineComponent({
name: 'DrinkModify',
components: { BuildManual, DrinkPriceVolumes },
props: {
drink: {
type: Object as PropType<Drink>,
required: true,
},
},
emits: {
save: (
drink: Drink,
toDeleteObjects: DeleteObjects,
drinkPic: File | undefined,
deletePic: boolean
) => (drink && toDeleteObjects) || drinkPic || deletePic,
delete: () => true,
cancel: () => true,
},
setup(props, { emit }) {
onBeforeMount(() => {
//edit_drink.value = <Drink>JSON.parse(JSON.stringify(props.drink));
edit_drink.value = clone(props.drink);
edit_volumes.value = clone(props.drink.volumes);
});
const key = ref(0);
const store = usePricelistStore();
const toDeleteObjects = ref<DeleteObjects>({
prices: [],
volumes: [],
ingredients: [],
});
const edit_drink = ref<Drink>();
const edit_volumes = ref<Array<DrinkPriceVolume>>([]);
function save() {
(<Drink>edit_drink.value).volumes = edit_volumes.value;
emit('save', <Drink>edit_drink.value, toDeleteObjects.value, drinkPic.value, deletePic.value);
}
function cancel() {
emit('cancel');
}
function updateVolume(index: number) {
if (index > -1 && edit_volumes.value) {
edit_volumes.value[index].min_prices = calc_min_prices(
edit_volumes.value[index],
//edit_drink.value.cost_per_volume,
cost_per_volume.value,
store.min_prices
);
}
}
function updateVolumes() {
setTimeout(() => {
edit_volumes.value?.forEach((_, index) => {
updateVolume(index);
});
key.value++;
}, 50);
}
function deletePrice(price: FG.DrinkPrice) {
toDeleteObjects.value.prices.push(price);
}
function deleteVolume(volume: DrinkPriceVolume) {
toDeleteObjects.value.volumes.push(volume);
}
function deleteIngredient(ingredient: FG.Ingredient) {
toDeleteObjects.value.ingredients.push(ingredient);
}
function addStep(event: string) {
edit_drink.value?.receipt?.push(event);
}
function deleteStep(event: number) {
edit_drink.value?.receipt?.splice(event, 1);
}
const drinkPic = ref();
const imgsrc = ref();
const deletePic = ref(false);
function delete_pic() {
deletePic.value = true;
imgsrc.value = undefined;
drinkPic.value = undefined;
if (edit_drink.value) {
edit_drink.value.uuid = '';
}
}
function imagePreview() {
if (drinkPic.value && drinkPic.value instanceof File) {
let reader = new FileReader();
reader.onload = (e) => {
imgsrc.value = e.target?.result;
};
reader.readAsDataURL(drinkPic.value);
}
}
const image = computed(() => {
if (deletePic.value) {
return 'no-image.svg';
}
if (imgsrc.value) {
return <string>imgsrc.value;
}
if (edit_drink.value?.uuid) {
return `${baseURL.value}/pricelist/picture/${edit_drink.value.uuid}?size=256`;
}
return 'no-image.svg';
});
const can_delete = computed(() => {
if (edit_drink.value) {
if (edit_drink.value.id < 0) {
return false;
}
const _edit_drink = edit_drink.value;
const test = _edit_drink.volumes ? _edit_drink.volumes.length === 0 : true;
console.log(test);
return test;
}
return false;
});
function delete_drink() {
emit('delete');
}
const auto_cost_per_volume = computed(
() =>
!!(
edit_drink.value?.cost_per_package &&
edit_drink.value?.package_size &&
edit_drink.value?.volume
)
);
const cost_per_volume = computed({
get: () => {
let retVal: number;
if (auto_cost_per_volume.value) {
retVal = <number>calc_cost_per_volume(<Drink>edit_drink.value);
} else {
retVal = <number>(<Drink>edit_drink.value).cost_per_volume;
}
updateVolumes();
return retVal;
},
set: (val: number) => ((<Drink>edit_drink.value).cost_per_volume = val),
});
const hasIngredients = computed(() =>
edit_volumes.value?.some((a) => a.ingredients.length > 0)
);
return {
edit_drink,
save,
cancel,
updateVolume,
deletePrice,
deleteIngredient,
deleteVolume,
addStep,
deleteStep,
tags: computed(() => store.tags),
image,
imgsrc,
drinkPic,
imagePreview,
delete_pic,
types: computed(() => store.drinkTypes),
can_delete,
delete_drink,
auto_cost_per_volume,
cost_per_volume,
edit_volumes,
key,
hasIngredients,
hasPermission,
PERMISSIONS,
};
},
});
</script>

View File

@ -1,48 +1,48 @@
<template>
<div>
<q-dialog v-model="edittype">
<q-card>
<q-card-section>
<div class="text-h6">Editere Getränkeart {{ actualDrinkType.name }}</div>
</q-card-section>
<q-card-section>
<q-input v-model="newDrinkTypeName" dense label="name" filled />
</q-card-section>
<q-card-actions>
<q-btn flat color="danger" label="Abbrechen" @click="discardChanges()" />
<q-btn flat color="primary" label="Speichern" @click="saveChanges()" />
</q-card-actions>
</q-card>
</q-dialog>
<q-page padding>
<q-table title="Getränkearten" :rows="rows" :row-key="(row) => row.id" :columns="columns">
<template #top-right>
<q-input
v-model="newDrinkType"
class="q-px-sm"
dense
placeholder="Neue Getränkeart"
filled
<q-table
title="Getränkearten"
:rows="rows"
:row-key="(row) => row.id"
:columns="columns"
style="height: 100%"
:pagination="pagination"
>
<template #top-right>
<div class="full-width row q-gutter-sm">
<q-input v-model="newDrinkType" dense placeholder="Neue Getränkeart" filled />
<q-btn round color="primary" icon="mdi-plus" @click="addType">
<q-tooltip> Getränkeart hinzufügen </q-tooltip>
</q-btn>
</div>
</template>
<template #body="props">
<q-tr :props="props">
<q-td key="drinkTypeName" :props="props">
{{ props.row.name }}
<q-popup-edit
v-model="props.row.name"
buttons
label-set="Speichern"
label-cancel="Abbrechen"
@save="saveChanges(props.row)"
>
<template #default="scope">
<q-input v-model="scope.value" dense label="name" filled />
</template>
</q-popup-edit>
</q-td>
<q-td key="actions" :props="props">
<q-btn
round
icon="mdi-delete"
color="negative"
size="sm"
@click="deleteType(props.row.id)"
/>
<div></div>
<q-btn color="primary" icon="mdi-plus" label="Hinzufügen" @click="addType" />
</template>
<template #body-cell-actions="props">
<q-td :props="props" align="right" :auto-width="true">
<q-btn
round
flat
icon="mdi-pencil"
@click="editType({ id: props.row.id, name: props.row.name })"
/>
<q-btn round flat icon="mdi-delete" @click="deleteType(props.row.id)" />
</q-td>
</template>
</q-table>
</q-page>
</div>
</q-td>
</q-tr>
</template>
</q-table>
</template>
<script lang="ts">
@ -54,14 +54,9 @@ export default defineComponent({
setup() {
const store = usePricelistStore();
const newDrinkType = ref('');
const newDrinkTypeName = ref('');
const edittype = ref(false);
const emptyDrinkType: FG.DrinkType = { id: -1, name: '' };
const actualDrinkType = ref(emptyDrinkType);
onBeforeMount(() => {
console.log(store);
void store.getDrinkTypes();
void store.getDrinkTypes(true);
});
const rows = computed(() => store.drinkTypes);
const columns = [
@ -70,14 +65,14 @@ export default defineComponent({
label: 'Getränkeart',
field: 'name',
align: 'left',
sortable: true
sortable: true,
},
{
name: 'actions',
label: 'Aktionen',
field: 'actions',
align: 'right'
}
align: 'right',
},
];
async function addType() {
@ -85,30 +80,22 @@ export default defineComponent({
newDrinkType.value = '';
}
function editType(drinkType: FG.DrinkType) {
edittype.value = true;
actualDrinkType.value = drinkType;
}
async function saveChanges() {
try {
await store.changeDrinkTypeName({
id: actualDrinkType.value.id,
name: newDrinkTypeName.value
});
} catch (e) {}
discardChanges();
}
function discardChanges() {
actualDrinkType.value = emptyDrinkType;
newDrinkTypeName.value = '';
edittype.value = false;
function saveChanges(drinkType: FG.DrinkType) {
setTimeout(() => {
const _drinkType = store.drinkTypes.find((a) => a.id === drinkType.id);
if (_drinkType) {
void store.changeDrinkTypeName(drinkType);
}
}, 50);
}
function deleteType(id: number) {
void store.removeDrinkType(id);
}
const pagination = ref({
sortBy: 'name',
rowsPerPage: 10,
});
return {
columns,
@ -116,14 +103,10 @@ export default defineComponent({
addType,
newDrinkType,
deleteType,
edittype,
editType,
actualDrinkType,
newDrinkTypeName,
discardChanges,
saveChanges
saveChanges,
pagination,
};
}
},
});
</script>

View File

@ -1,60 +1,67 @@
<template>
<div>
<q-dialog v-model="edittype">
<q-card>
<q-card-section>
<div class="text-h6">Editere Extrazutaten {{ actualExtraIngredient.name }}</div>
</q-card-section>
<q-card-section>
<q-input v-model="actualExtraIngredient.name" dense label="Name" filled />
<q-input
v-model.number="actualExtraIngredient.price"
dense
label="Preis"
filled
type="number"
min="0"
step="0.1"
suffix="€"
/>
</q-card-section>
<q-card-actions>
<q-btn flat color="danger" label="Abbrechen" @click="discardChanges()" />
<q-btn flat color="primary" label="Speichern" @click="saveChanges()" />
</q-card-actions>
</q-card>
</q-dialog>
<q-page padding>
<q-table title="Getränkearten" :rows="rows" :row-key="(row) => row.id" :columns="columns">
<q-table
title="Getränkearten"
:rows="rows"
:row-key="(row) => row.id"
:columns="columns"
:pagination="pagination"
>
<template #top-right>
<q-input
v-model="newExtraIngredient.name"
class="q-px-sm"
dense
placeholder="Neue Zutatenbezeichnung"
label="Neue Zutatenbezeichnung"
filled
/>
<q-input
v-model.number="newExtraIngredient.price"
class="q-px-sm"
dense
placeholder="Preis"
label="Preis"
filled
type="number"
min="0"
step="0.1"
suffix="€"
/>
<q-btn color="primary" icon="mdi-plus" label="Hinzufügen" @click="addExtraIngredient" />
<div class="full-width row q-gutter-sm">
<q-input
v-model="newExtraIngredient.name"
dense
placeholder="Neue Zutatenbezeichnung"
label="Neue Zutatenbezeichnung"
filled
/>
<q-input
v-model.number="newExtraIngredient.price"
dense
placeholder="Preis"
label="Preis"
filled
type="number"
min="0"
step="0.1"
suffix="€"
/>
<q-btn color="primary" icon="mdi-plus" round @click="addExtraIngredient">
<q-tooltip> Zutat hinzufügen </q-tooltip>
</q-btn>
</div>
</template>
<template #body-cell-actions="props">
<q-td :props="props" align="right" :auto-width="true">
<q-btn round flat icon="mdi-pencil" @click="editType(props.row)" />
<q-btn round flat icon="mdi-delete" @click="deleteType(props.row)" />
</q-td>
<template #body="props">
<q-tr :props="props">
<q-td key="name" :props="props" align="left">
{{ props.row.name }}
<q-popup-edit
v-model="props.row.name"
buttons
label-set="Speichern"
label-cancel="Abbrechen"
@save="saveChanges(props.row)"
>
<template #default="scope">
<q-input v-model="scope.value" dense label="name" filled />
</template>
</q-popup-edit>
</q-td>
<q-td key="price" :props="props" align="right">
{{ props.row.price.toFixed(2) }}
</q-td>
<q-td key="actions" :props="props" align="right" auto-width>
<q-btn
round
icon="mdi-delete"
color="negative"
size="sm"
@click="deleteType(props.row)"
/>
</q-td>
</q-tr>
</template>
</q-table>
</q-page>
@ -75,8 +82,6 @@ export default defineComponent({
id: -1,
};
const newExtraIngredient = ref<FG.ExtraIngredient>(emptyExtraIngredient);
const edittype = ref(false);
const actualExtraIngredient = ref(emptyExtraIngredient);
const rows = computed(() => store.extraIngredients);
const columns = [
@ -93,6 +98,7 @@ export default defineComponent({
field: 'price',
sortable: true,
format: (val: number) => `${val.toFixed(2)}`,
align: 'right',
},
{
name: 'actions',
@ -108,38 +114,38 @@ export default defineComponent({
discardChanges();
}
function editType(extraIngredient: FG.ExtraIngredient) {
edittype.value = true;
actualExtraIngredient.value = extraIngredient;
}
async function saveChanges() {
await store.updateExtraIngredient(actualExtraIngredient.value);
setTimeout(() => discardChanges(), 200);
function saveChanges(ingredient: FG.ExtraIngredient) {
setTimeout(() => {
const _ingredient = store.extraIngredients.find((a) => a.id === ingredient.id);
if (_ingredient) {
void store.updateExtraIngredient(_ingredient);
}
}, 50);
}
function discardChanges() {
actualExtraIngredient.value = emptyExtraIngredient;
newExtraIngredient.value.name = '';
newExtraIngredient.value.price = 0;
edittype.value = false;
}
function deleteType(extraIngredient: FG.ExtraIngredient) {
void store.deleteExtraIngredient(extraIngredient);
}
const pagination = ref({
sortBy: 'name',
rowsPerPage: 10,
});
return {
columns,
rows,
addExtraIngredient,
newExtraIngredient,
deleteType,
edittype,
editType,
actualExtraIngredient,
discardChanges,
saveChanges,
pagination,
};
},
});

View File

@ -0,0 +1,327 @@
<template>
<q-table
title="Preisliste"
:columns="columns"
:rows="drinks"
:visible-columns="visibleColumns"
:filter="search"
:filter-method="filter"
dense
:pagination="pagination"
:fullscreen="fullscreen"
>
<template #top-right>
<div class="row justify-end q-gutter-sm">
<search-input v-model="search" :keys="options" />
<q-select
v-model="visibleColumns"
multiple
filled
dense
options-dense
display-value="Sichtbarkeit"
emit-value
map-options
:options="options"
option-value="name"
options-cover
/>
<q-btn round icon="mdi-backburger">
<q-tooltip anchor='top middle' self='bottom middle'> Reihenfolge ändern </q-tooltip>
<q-menu anchor="bottom middle" self="top middle">
<drag v-model="order" class="q-list" ghost-class="ghost" group="people" item-key="id">
<template #item="{ element }">
<q-item>
<q-item-section>
{{ element.label }}
</q-item-section>
</q-item>
</template>
</drag>
</q-menu>
</q-btn>
<slot></slot>
<q-btn
round
:icon="fullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'"
@click="fullscreen = !fullscreen"
/>
</div>
</template>
<template #body-cell-tags="props">
<q-td :props="props">
<q-badge
v-for="tag in props.row.tags"
:key="`${props.row.id}-${tag.id}`"
class="text-caption"
rounded
:style="`background-color: ${tag.color}`"
>
{{ tag.name }}
</q-badge>
</q-td>
</template>
<template #body-cell-public="props">
<q-td :props="props">
<q-toggle
v-model="props.row.public"
disable
checked-icon="mdi-earth"
unchecked-icon="mdi-earth-off"
/>
</q-td>
</template>
</q-table>
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeMount, ref, ComponentPublicInstance } from 'vue';
import { usePricelistStore, Order } from '../store';
import { useMainStore } from 'src/stores';
import { Search, filter } from 'src/plugins/pricelist/utils/filter';
import SearchInput from 'src/plugins/pricelist/components/SearchInput.vue';
import draggable from 'vuedraggable';
const drag: ComponentPublicInstance = <ComponentPublicInstance>draggable;
interface Row {
name: string;
label: string;
field: string;
sortable?: boolean;
filterable?: boolean;
format?: (val: never) => string;
align?: string;
}
export default defineComponent({
name: 'Pricelist',
components: { SearchInput, drag },
props: {
public: {
type: Boolean,
default: false,
},
},
setup(props) {
const store = usePricelistStore();
const user = ref('');
onBeforeMount(() => {
if (!props.public) {
user.value = useMainStore().currentUser.userid;
void store.getPriceListColumnOrder(user.value);
void store.getDrinks();
void store.getPriceCalcColumn(user.value);
} else {
user.value = '';
}
});
const _order = ref<Array<Order>>([
{
name: 'name',
label: 'Name',
},
{
name: 'type',
label: 'Kategorie',
},
{
name: 'tags',
label: 'Tags',
},
{
name: 'volume',
label: 'Inhalt',
},
{
name: 'price',
label: 'Preis',
},
{
name: 'public',
label: 'Öffentlich',
},
{
name: 'description',
label: 'Beschreibung',
},
]);
const order = computed<Array<Order>>({
get: () => {
if (props.public) {
return _order.value;
}
if (store.pricelist_columns_order.length === 0) {
return _order.value;
}
return store.pricelist_columns_order;
},
set: (val: Array<Order>) => {
if (!props.public) {
void store.updatePriceListColumnOrder(user.value, val);
} else {
_order.value = val;
}
},
});
const _columns: Array<Row> = [
{
name: 'name',
label: 'Name',
field: 'name',
sortable: true,
filterable: true,
align: 'left',
},
{
name: 'type',
label: 'Kategorie',
field: 'type',
sortable: true,
filterable: true,
format: (val: FG.DrinkType) => val.name,
},
{
name: 'tags',
label: 'Tags',
field: 'tags',
filterable: true,
format: (val: Array<FG.Tag>) => {
let retVal = '';
val.forEach((tag, index) => {
if (index >= val.length - 1 && index > 0) {
retVal += ', ';
}
retVal += tag.name;
});
return retVal;
},
},
{
name: 'volume',
label: 'Inhalt',
field: 'volume',
filterable: true,
sortable: true,
format: (val: number) => `${val.toFixed(3)}L`,
},
{
name: 'price',
label: 'Preis',
field: 'price',
sortable: true,
filterable: true,
format: (val: number) => `${val.toFixed(2)}`,
},
{
name: 'public',
label: 'Öffentlich',
field: 'public',
format: (val: boolean) => (val ? 'Öffentlich' : 'nicht Öffentlich'),
},
{
name: 'description',
label: 'Beschreibung',
field: 'description',
filterable: true,
},
];
const columns = computed(() => {
const retVal: Array<Row> = [];
if (order.value) {
order.value.forEach((col) => {
const _col = _columns.find((a) => a.name === col.name);
if (_col) {
retVal.push(_col);
}
});
retVal.forEach((element, index) => {
element.align = 'right';
if (index === 0) {
element.align = 'left';
}
});
return retVal;
}
return _columns;
});
const _options = computed(() => {
const retVal: Array<{ name: string; label: string; field: string }> = [];
columns.value.forEach((col) => {
if (props.public) {
if (col.name !== 'public') {
retVal.push(col);
}
} else {
retVal.push(col);
}
});
return retVal;
});
const _colums = computed<Array<string>>(() => {
const retVal: Array<string> = [];
columns.value.forEach((col) => {
if (props.public) {
if (col.name !== 'public') {
retVal.push(col.name);
}
} else {
retVal.push(col.name);
}
});
return retVal;
});
const _visibleColumns = ref(_colums.value);
const visibleColumns = computed({
get: () => (props.public ? _visibleColumns.value : store.pricecalc_columns),
set: (val) => {
if (!props.public) {
void store.updatePriceCalcColumn(user.value, val);
} else {
_visibleColumns.value = val;
}
},
});
const search = ref<Search>({
value: '',
key: '',
label: '',
});
const pagination = ref({
sortBy: 'name',
rowsPerPage: 10,
});
const fullscreen = ref(false);
return {
drinks: computed(() => store.pricelist),
columns,
order,
visibleColumns,
options: _options,
search,
filter,
pagination,
fullscreen,
};
},
});
</script>
<style scoped lang="sass">
.ghost
opacity: 0.5
background: $accent
</style>

View File

@ -0,0 +1,89 @@
<template>
<q-dialog v-model="alert">
<q-card>
<q-card-section>
<div class="text-h6">Suche</div>
</q-card-section>
<q-card-section class="q-pt-none">
<div>
Wenn du in die Suche etwas eingibst, wird in allen Spalten gesucht. Mit einem `@` Zeichen,
kann man die Suche eingrenzen auf eine Spalte. Zumbeispiel: `Tequilaparty@Tags`
</div>
</q-card-section>
<q-card-section>
<div>Mögliche Suchbegriffe nach dem @:</div>
<div class="fit row q-gutter-sm">
<div v-for="key in keys" :key="key.name">
{{ key.label }}
</div>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn v-close-popup flat label="OK" color="primary" />
</q-card-actions>
</q-card>
</q-dialog>
<q-input v-model="v_model" filled dense>
<template #append>
<q-icon name="mdi-magnify" />
</template>
<template #prepend>
<q-btn icon="mdi-help-circle" flat round @click="alert = true" />
</template>
</q-input>
</template>
<script lang="ts">
import { defineComponent, computed, PropType, ref } from 'vue';
import { Search, Col } from '../utils/filter';
export default defineComponent({
name: 'SearchInput',
props: {
modelValue: {
type: Object as PropType<Search>,
default: { value: '', key: undefined, label: '' },
},
keys: {
type: Object as PropType<Array<Col>>,
required: true,
},
},
emits: {
'update:modelValue': (val: {
value: string;
key: string | undefined;
label: string | undefined;
}) => val,
},
setup(props, { emit }) {
const v_model = computed<string>({
get: () => {
if (!props.modelValue.label || props.modelValue.label === '') {
return `${props.modelValue.value}`;
}
return `${props.modelValue.value}@${props.modelValue.label}`;
},
set: (val: string) => {
const split = val.toLowerCase().split('@');
if (split.length < 2) {
emit('update:modelValue', { value: split[0], label: undefined, key: undefined });
} else {
props.keys.find((key) => {
if (key.label.toLowerCase() === split[1]) {
console.log(key.name);
emit('update:modelValue', { value: split[0], label: split[1], key: key.name });
return true;
}
return false;
});
}
},
});
const alert = ref(false);
return { v_model, alert };
},
});
</script>
a

View File

@ -0,0 +1,181 @@
<template>
<div>
<q-page padding>
<q-table
title="Tags"
:rows="rows"
:row-key="(row) => row.id"
:columns="columns"
:pagination="pagination"
>
<template #top-right>
<q-btn color="primary" icon="mdi-plus" round>
<q-tooltip> Tag hinzufügen </q-tooltip>
<q-menu v-model="popup" anchor="center middle" self="center middle" persistent>
<q-input
v-model="newTag.name"
filled
dense
label="Name"
class="q-pa-sm"
:rule="[notExists]"
/>
<q-color
:model-value="newTag.color"
flat
class="q-pa-sm"
@change="
(val) => {
newTag.color = val;
}
"
/>
<div class="full-width row q-gutter-sm justify-around q-py-sm">
<q-btn v-close-popup flat label="Abbrechen" />
<q-btn flat label="Speichern" color="primary" @click="save" />
</div>
</q-menu>
</q-btn>
</template>
<template #header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width />
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td key="name" :props="props">
{{ props.row.name }}
<q-popup-edit
v-model="props.row.name"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@update:modelValue="updateTag(props.row)"
>
<template #default="scope">
<q-input v-model="scope.value" :rules="[notExists]" dense filled />
</template>
</q-popup-edit>
</q-td>
<q-td key="color" :props="props">
<div class="full-width row q-gutter-sm justify-end items-center">
<div>
{{ props.row.color }}
</div>
<div class="color-box" :style="`background-color: ${props.row.color};`">&nbsp;</div>
</div>
<q-popup-edit
v-model="props.row.color"
buttons
label-cancel="Abbrechen"
label-set="Speichern"
@update:modelValue="updateTag(props.row)"
>
<template #default="slot">
<div class="full-width row justify-center">
<q-color
:model-value="slot.value"
class="full-width"
flat
@change="(val) => (slot.value = val)"
/>
</div>
</template>
</q-popup-edit>
</q-td>
<q-td>
<q-btn
icon="mdi-delete"
color="negative"
round
size="sm"
@click="deleteTag(props.row)"
/>
</q-td>
</q-tr>
</template>
</q-table>
</q-page>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onBeforeMount, computed } from 'vue';
import { usePricelistStore } from '../store';
export default defineComponent({
name: 'Tags',
setup() {
const store = usePricelistStore();
onBeforeMount(() => {
void store.getTags();
});
const columns = [
{
name: 'name',
label: 'Name',
field: 'name',
align: 'left',
},
{
name: 'color',
label: 'Farbe',
field: 'color',
},
];
const rows = computed(() => store.tags);
const emptyTag = {
id: -1,
color: '#1976d2',
name: '',
};
async function save() {
await store.setTag(newTag.value);
popup.value = false;
newTag.value = emptyTag;
}
const newTag = ref(emptyTag);
const popup = ref(false);
function notExists(val: string) {
const index = store.tags.findIndex((a) => a.name === val);
if (index > -1) {
return 'Tag existiert bereits.';
}
return true;
}
const pagination = ref({
sortBy: 'name',
rowsPerPage: 10,
});
return {
columns,
rows,
newTag,
popup,
save,
updateTag: store.updateTag,
notExists,
deleteTag: store.deleteTag,
pagination,
};
},
});
</script>
<style scoped>
.color-box {
min-width: 28px;
min-heigh: 28px;
max-width: 28px;
max-height: 28px;
border-width: 1px;
border-color: black;
border-radius: 5px;
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<q-card>
<q-card-section>
<div class="q-table__title">Cocktailbuilder</div>
</q-card-section>
<q-card-section>
<ingredients
v-model="volume.ingredients"
class="q-pa-sm"
editable
@update:modelValue="update"
/>
</q-card-section>
<q-card-section>
<div class="q-table__title">Du solltest mindest sowiel verlangen oder bezahlen:</div>
<div class="full-width row q-gutter-sm justify-around">
<div v-for="min_price in volume.min_prices" :key="min_price.percentage">
<div>
<q-badge class="text-h6" color="primary"> {{ min_price.percentage }}% </q-badge>
</div>
<div>{{ min_price.price.toFixed(2) }}</div>
</div>
</div>
</q-card-section>
</q-card>
</template>
<script lang="ts">
import { defineComponent, onBeforeMount, ref } from 'vue';
import Ingredients from 'src/plugins/pricelist/components/CalculationTable/Ingredients.vue';
import { DrinkPriceVolume, usePricelistStore } from 'src/plugins/pricelist/store';
import { calc_min_prices } from '../utils/utils';
export default defineComponent({
name: 'CocktailBuilder',
components: { Ingredients },
setup() {
onBeforeMount(() => {
void store.get_min_prices().finally(() => {
volume.value.min_prices = calc_min_prices(volume.value, undefined, store.min_prices);
});
void store.getDrinks();
void store.getDrinkTypes();
void store.getExtraIngredients();
});
const store = usePricelistStore();
const emptyVolume: DrinkPriceVolume = {
id: -1,
_volume: 0,
min_prices: [],
prices: [],
ingredients: [],
};
const volume = ref(emptyVolume);
function update() {
volume.value.min_prices = calc_min_prices(volume.value, undefined, store.min_prices);
}
return { volume, update };
},
});
</script>
<style scoped></style>

View File

@ -0,0 +1,38 @@
<template>
<calculation-table v-if="!list" nodetails>
<q-btn icon="mdi-view-list" round @click="list = !list">
<q-tooltip> Zur Listenansicht wechseln </q-tooltip>
</q-btn>
</calculation-table>
<pricelist v-if="list">
<q-btn icon="mdi-cards-variant" round @click="list = !list">
<q-tooltip> Zur Kartenansicht wechseln </q-tooltip>
</q-btn>
</pricelist>
</template>
<script lang="ts">
import { defineComponent, onBeforeMount, computed } from 'vue';
import CalculationTable from '../components/CalculationTable.vue';
import Pricelist from 'src/plugins/pricelist/components/Pricelist.vue';
import { usePricelistStore } from 'src/plugins/pricelist/store';
import { useMainStore } from 'src/stores';
export default defineComponent({
name: 'InnerPricelist',
components: { Pricelist, CalculationTable },
setup() {
const store = usePricelistStore();
const mainStore = useMainStore();
onBeforeMount(() => {
void store.getDrinks();
void store.getPriceListView(mainStore.currentUser.userid);
});
const list = computed({
get: () => store.pricelist_view,
set: (val: boolean) => store.updatePriceListView(mainStore.currentUser.userid, val),
});
return { list };
},
});
</script>

View File

@ -0,0 +1,36 @@
<template>
<calculation-table v-if="!list" public>
<q-btn icon="mdi-view-list" round @click="list = !list">
<q-tooltip> Zur Listenansicht wechseln </q-tooltip>
</q-btn>
</calculation-table>
<pricelist v-if="list" public>
<q-btn icon="mdi-cards-variant" round @click="list = !list">
<q-tooltip> Zur Kartenansicht wechseln </q-tooltip>
</q-btn>
</pricelist>
</template>
<script>
import { defineComponent } from 'vue';
import CalculationTable from '../components/CalculationTable.vue';
import Pricelist from '../components/Pricelist.vue';
import { usePricelistStore } from '../store';
import { onBeforeMount, ref } from 'vue';
export default defineComponent({
name: 'OuterPricelist',
components: { Pricelist, CalculationTable },
setup() {
const store = usePricelistStore();
onBeforeMount(() => {
void store.getDrinks();
});
const list = ref(false);
return { list };
},
});
</script>
<style scoped></style>

View File

@ -1,100 +0,0 @@
<template>
<q-page padding>
<q-card>
<q-table title="Getränke" :columns="columns_drinks" :rows="drinks" row-key="name">
<template #body-cell-volumes="props">
<q-td :props="props">
<q-table
:columns="columns_volumes"
:rows="props.row.volumes"
hide-header
:hide-bottom="props.row.volumes.length < 5"
flat
>
<template #body-cell-prices="props_volumes">
<q-td :props="props_volumes">
<q-table
:columns="columns_prices"
:rows="props_volumes.row.prices"
hide-header
:hide-bottom="props_volumes.row.prices.length < 5"
flat
>
<template #body-cell-public="props_prices">
<q-td :props="props_prices">
<q-toggle v-model="props_prices.row.public" disable />
</q-td>
</template>
</q-table>
</q-td>
</template>
</q-table>
</q-td>
</template>
</q-table>
</q-card>
</q-page>
</template>
<script lang="ts">
import { defineComponent, onBeforeMount, computed } from 'vue';
import { usePricelistStore } from '../store';
export default defineComponent({
name: 'Pricelist',
setup() {
const store = usePricelistStore();
onBeforeMount(() => {
void store.getDrinks();
});
const drinks = computed(() => store.drinks);
const columns_drinks = [
{
name: 'name',
label: 'Getränk',
field: 'name',
align: 'center',
},
{
name: 'volumes',
label: 'Preise',
field: 'volumes',
align: 'center',
},
];
const columns_volumes = [
{
name: 'volume',
label: 'Inhalt',
field: 'volume',
format: (val: number) => `${val.toFixed(3)}L`,
align: 'left',
},
{
name: 'prices',
label: 'Preise',
field: 'prices',
},
];
const columns_prices = [
{
name: 'price',
label: 'Preis',
field: 'price',
format: (val: number) => `${val.toFixed(2)}`,
},
{
name: 'description',
label: 'Beschreibung',
field: 'description',
},
{
name: 'public',
label: 'Öffentlich',
field: 'public',
},
];
return { columns_drinks, columns_volumes, columns_prices, drinks };
},
});
</script>

View File

@ -0,0 +1,175 @@
<template>
<q-table
grid
title="Rezepte"
:rows="drinks"
row-key="id"
hide-header
:filter="search"
:filter-method="filter"
:columns="options"
>
<template #top-right>
<search-input v-model="search" :keys="search_keys" />
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
<q-card>
<q-img
style="max-height: 256px"
loading="lazy"
:src="image(props.row.uuid)"
placeholder-src="no-image.svg"
>
<div class="absolute-bottom-right justify-end">
<div class="text-subtitle1 text-right">
{{ props.row.name }}
</div>
<div class="text-caption text-right">
{{ props.row.type.name }}
</div>
</div>
</q-img>
<q-card-section>
<q-badge
v-for="tag in props.row.tags"
:key="`${props.row.id}-${tag.id}`"
class="text-caption"
rounded
:style="`background-color: ${tag.color}`"
>
{{ tag.name }}
</q-badge>
</q-card-section>
<build-manual-volume :volumes="props.row.volumes" />
<q-card-section>
<div class="text-h6">Anleitung</div>
<build-manual :steps="props.row.receipt" :editable="false" />
</q-card-section>
</q-card>
</div>
</template>
</q-table>
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
import { usePricelistStore } from 'src/plugins/pricelist/store';
import BuildManual from 'src/plugins/pricelist/components/CalculationTable/BuildManual.vue';
import BuildManualVolume from '../components/BuildManual/BuildManualVolume.vue';
import SearchInput from '../components/SearchInput.vue';
import { filter, Search } from '../utils/filter';
import { sort } from '../utils/sort';
import { baseURL } from 'src/config';
export default defineComponent({
name: 'Reciepts',
components: { BuildManual, BuildManualVolume, SearchInput },
setup() {
const store = usePricelistStore();
onBeforeMount(() => {
void store.getDrinks();
});
const drinks = computed(() =>
store.drinks.filter((drink) => {
return drink.volumes.some((volume) => volume.ingredients.length > 0);
})
);
const columns_drinks = [
{
name: 'picture',
label: 'Bild',
align: 'center',
},
{
name: 'name',
label: 'Name',
field: 'name',
align: 'center',
sortable: true,
filterable: true,
},
{
name: 'drink_type',
label: 'Kategorie',
field: 'type',
format: (val: FG.DrinkType) => `${val.name}`,
sortable: true,
sort: (a: FG.DrinkType, b: FG.DrinkType) => sort(a.name, b.name),
filterable: true,
},
{
name: 'tags',
label: 'Tag',
field: 'tags',
format: (val: Array<FG.Tag>) => {
let retVal = '';
val.forEach((tag, index) => {
if (index > 0) {
retVal += ', ';
}
retVal += tag.name;
});
return retVal;
},
filterable: true,
},
{
name: 'volumes',
label: 'Preise',
field: 'volumes',
align: 'center',
},
];
const columns_volumes = [
{
name: 'volume',
label: 'Inhalt',
field: 'volume',
align: 'left',
},
{
name: 'prices',
label: 'Preise',
field: 'prices',
},
];
const columns_prices = [
{
name: 'price',
label: 'Preis',
field: 'price',
},
{
name: 'description',
label: 'Beschreibung',
field: 'description',
},
{
name: 'public',
label: 'Öffentlich',
field: 'public',
},
];
const search = ref<Search>({ value: '', key: '', label: '' });
const search_keys = computed(() => columns_drinks.filter((column) => column.filterable));
function image(uuid: string | undefined) {
if (uuid) {
return `${baseURL.value}/pricelist/picture/${uuid}?size=256`;
}
return 'no-image.svg';
}
return {
drinks,
options: [...columns_drinks, ...columns_volumes, ...columns_prices],
search,
filter,
search_keys,
image,
};
},
});
</script>
<style scoped></style>

View File

@ -32,7 +32,7 @@
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
>
<q-tab-panel name="pricelist">
<calculation-table />
<calculation-table editable />
</q-tab-panel>
<q-tab-panel name="extra_ingredients">
<extra-ingredients />
@ -40,6 +40,9 @@
<q-tab-panel name="drink_types">
<drink-types />
</q-tab-panel>
<q-tab-panel name="tags">
<tags />
</q-tab-panel>
</q-tab-panels>
</q-page>
</div>
@ -51,15 +54,18 @@ import { Screen } from 'quasar';
import DrinkTypes from 'src/plugins/pricelist/components/DrinkTypes.vue';
import CalculationTable from 'src/plugins/pricelist/components/CalculationTable.vue';
import ExtraIngredients from 'src/plugins/pricelist/components/ExtraIngredients.vue';
import Tags from '../components/Tags.vue';
import { usePricelistStore } from 'src/plugins/pricelist/store';
import { hasPermissions } from 'src/utils/permission';
export default defineComponent({
name: 'Settings',
components: { ExtraIngredients, DrinkTypes, CalculationTable },
components: { ExtraIngredients, DrinkTypes, Tags, CalculationTable },
setup() {
interface Tab {
name: string;
label: string;
permissions: Array<string>;
}
const store = usePricelistStore();
onBeforeMount(() => {
@ -69,6 +75,7 @@ export default defineComponent({
console.log(store.extraIngredients);
})
.catch((err) => console.log(err));
void store.getTags();
void store.getDrinkTypes();
void store.getDrinks();
void store.get_min_prices();
@ -85,12 +92,40 @@ export default defineComponent({
},
});
const tabs: Tab[] = [
{ name: 'pricelist', label: 'Getränke' },
{ name: 'extra_ingredients', label: 'Zutaten' },
{ name: 'drink_types', label: 'Getränketypen' },
const _tabs: Tab[] = [
{ name: 'pricelist', label: 'Getränke', permissions: ['drink_edit'] },
{
name: 'extra_ingredients',
label: 'Zutaten',
permissions: ['edit_ingredients', 'delete_ingredients'],
},
{
name: 'drink_types',
label: 'Getränketypen',
permissions: ['drink_type_edit', 'drink_type_delete'],
},
{
name: 'tags',
label: 'Tags',
permissions: ['drink_tag_edit', 'drink_tag_create', 'drink_tag_delete'],
},
];
const tabs = computed(() => {
const retVal: Tab[] = [];
_tabs.forEach((tab) => {
if (tab.permissions.length > 0) {
if (hasPermissions(tab.permissions)) {
retVal.push(tab);
}
}
if (tab.permissions.length === 0) {
retVal.push(tab);
}
});
return retVal;
});
const tab = ref<string>('pricelist');
return { tabs, tab, showDrawer };

View File

@ -0,0 +1,33 @@
export const PERMISSIONS = {
CREATE: 'drink_create',
EDIT: 'drink_edit',
DELETE: 'drink_delete',
CREATE_TAG: 'drink_tag_create',
EDIT_PRICE: 'edit_price',
DELETE_PRICE: 'delete_price',
EDIT_VOLUME: 'edit_volume',
DELETE_VOLUME: 'delete_volume',
EDIT_INGREDIENTS_DRINK: 'edit_ingredients_drink',
DELETE_INGREDIENTS_DRINK: 'delete_ingredients_drink',
EDIT_INGREDIENTS: 'edit_ingredients',
DELETE_INGREDIENTS: 'delete_ingredients',
EDIT_TAG: 'drink_tag_edit',
DELETE_TAG: 'drink_tag_delete',
CREATE_TYPE: 'drink_type_create',
EDIT_TYPE: 'drink_type_edit',
DELETE_TYPE: 'drink_type_delete',
EDIT_MIN_PRICES: 'edit_min_prices',
};

View File

@ -1,9 +1,10 @@
import { innerRoutes } from './routes';
import { innerRoutes, outerRoutes } from './routes';
import { FG_Plugin } from 'src/plugins';
const plugin: FG_Plugin.Plugin = {
name: 'Pricelist',
innerRoutes,
outerRoutes,
requiredModules: [],
requiredBackendModules: ['pricelist'],
version: '0.0.1',

View File

@ -19,14 +19,36 @@ export const innerRoutes: FG_Plugin.MenuRoute[] = [
route: {
path: 'pricelist',
name: 'drinks-pricelist',
component: () => import('../pages/Pricelist.vue'),
component: () => import('../pages/InnerPricelist.vue'),
},
},
{
title: 'Rezepte',
shortcut: false,
icon: 'mdi-receipt',
permissions: ['user'],
route: {
path: 'reciepts',
name: 'reciepts',
component: () => import('../pages/Receipts.vue'),
},
},
{
title: 'Cocktailbuilder',
shortcut: false,
icon: 'mdi-glass-cocktail',
permissions: ['user'],
route: {
path: 'cocktail-builder',
name: 'cocktail-builder',
component: () => import('../pages/CocktailBuilder.vue'),
},
},
{
title: 'Einstellungen',
icon: 'mdi-coffee-to-go',
shortcut: false,
permissions: ['user'],
permissions: ['drink_edit', 'drink_tag_edit'],
route: {
path: 'settings',
name: 'drinks-settings',
@ -36,3 +58,16 @@ export const innerRoutes: FG_Plugin.MenuRoute[] = [
],
},
];
export const outerRoutes: FG_Plugin.MenuRoute[] = [
{
title: 'Preisliste',
icon: 'mdi-glass-mug-variant',
shortcut: true,
route: {
path: 'pricelist',
name: 'outter-pricelist',
component: () => import('../pages/OuterPricelist.vue'),
},
},
];

View File

@ -1,20 +1,30 @@
import { api } from 'src/boot/axios';
import { defineStore } from 'pinia';
import { AxiosResponse } from 'axios';
import { computed, ComputedRef, WritableComputedRef } from 'vue';
import {
calc_volume,
calc_cost_per_volume,
calc_all_min_prices,
} from 'src/plugins/pricelist/utils/utils';
interface MinPrice extends Omit<FG.MinPrices, 'price'> {
price?: WritableComputedRef<number>;
}
interface DrinkPriceVolume extends Omit<Omit<FG.DrinkPriceVolume, 'volume'>, 'min_prices'> {
interface DrinkPriceVolume extends Omit<FG.DrinkPriceVolume, 'volume'> {
_volume: number;
volume?: WritableComputedRef<number>;
min_prices: MinPrice[];
volume?: number;
}
interface Drink extends Omit<Omit<FG.Drink, 'cost_price_pro_volume'>, 'volumes'> {
interface Drink extends Omit<Omit<FG.Drink, 'cost_per_volume'>, 'volumes'> {
volumes: DrinkPriceVolume[];
cost_price_pro_volume: WritableComputedRef<number | undefined>;
_cost_price_pro_volume?: number;
cost_per_volume?: number;
_cost_per_volume?: number;
}
interface Pricelist {
name: string;
type: FG.DrinkType;
tags: Array<FG.Tag>;
volume: number;
price: number;
public: boolean;
description: string;
}
class DrinkPriceVolume implements DrinkPriceVolume {
@ -24,23 +34,7 @@ class DrinkPriceVolume implements DrinkPriceVolume {
this.prices = prices;
this.ingredients = ingredients;
this.min_prices = [];
this.volume = computed<number>({
get: () => {
if (this.ingredients.some((ingredient) => !!ingredient.drink_ingredient)) {
let retVal = 0;
this.ingredients.forEach((ingredient) => {
if (ingredient.drink_ingredient?.volume) {
retVal += ingredient.drink_ingredient.volume;
}
});
this._volume = retVal;
return retVal;
} else {
return this._volume;
}
},
set: (val) => (this._volume = val),
});
this.volume = calc_volume(this);
}
}
@ -55,6 +49,8 @@ class Drink {
cost_per_package,
tags,
type,
uuid,
receipt,
}: FG.Drink) {
this.id = id;
this.article_id = article_id;
@ -62,25 +58,21 @@ class Drink {
this.name = name;
this.volume = volume;
this.cost_per_package = cost_per_package;
this.cost_per_volume = cost_per_volume;
this.cost_price_pro_volume = computed({
get: () => {
if (!!this.volume && !!this.package_size && !!this.cost_per_package) {
const retVal =
((this.cost_per_package || 0) / ((this.volume || 0) * (this.package_size || 0))) * 1.19;
this._cost_price_pro_volume = Math.round(retVal * 1000) / 1000;
}
return this._cost_price_pro_volume;
},
set: (val) => (this._cost_price_pro_volume = val),
});
this._cost_per_volume = cost_per_volume;
this.cost_per_volume = calc_cost_per_volume(this);
this.tags = tags;
this.type = type;
this.volumes = [];
this.uuid = uuid;
this.receipt = receipt || [];
}
}
interface Order {
label: string;
name: string;
}
export const usePricelistStore = defineStore({
id: 'pricelist',
@ -88,8 +80,11 @@ export const usePricelistStore = defineStore({
drinkTypes: [] as Array<FG.DrinkType>,
drinks: [] as Array<Drink>,
extraIngredients: [] as Array<FG.ExtraIngredient>,
pricecalc_columns: [] as Array<string>,
min_prices: [] as Array<number>,
tags: [] as Array<FG.Tag>,
pricecalc_columns: [] as Array<string>,
pricelist_view: false as boolean,
pricelist_columns_order: [] as Array<Order>,
}),
actions: {
@ -157,7 +152,7 @@ export const usePricelistStore = defineStore({
});
this.drinks.push(_drink);
});
this.create_min_prices();
calc_all_min_prices(this.drinks, this.min_prices);
},
sortPrices(volume: DrinkPriceVolume) {
volume.prices.sort((a, b) => {
@ -166,51 +161,36 @@ export const usePricelistStore = defineStore({
return 0;
});
},
deletePrice(price: FG.DrinkPrice, volume: FG.DrinkPriceVolume) {
api
.delete(`pricelist/prices/${price.id}`)
.then(() => {
const index = volume.prices.findIndex((a) => a.id == price.id);
if (index > -1) {
volume.prices.splice(index, 1);
}
})
.catch((err) => console.warn(err));
async deletePrice(price: FG.DrinkPrice) {
await api.delete(`pricelist/prices/${price.id}`);
},
deleteVolume(volume: FG.DrinkPriceVolume, drink: FG.Drink) {
api
.delete(`pricelist/volumes/${volume.id}`)
.then(() => {
const index = drink.volumes.findIndex((a) => a.id === volume.id);
if (index > -1) {
drink.volumes.splice(index, 1);
}
})
.catch((err) => console.warn(err));
async deleteVolume(volume: DrinkPriceVolume, drink: Drink) {
await api.delete(`pricelist/volumes/${volume.id}`);
const index = drink.volumes.findIndex((a) => a.id === volume.id);
if (index > -1) {
drink.volumes.splice(index, 1);
}
},
deleteIngredient(ingredient: FG.Ingredient, volume: DrinkPriceVolume) {
api
.delete(`pricelist/ingredients/${ingredient.id}`)
.then(() => {
const index = volume.ingredients.findIndex((a) => a.id === ingredient.id);
if (index > -1) {
volume.ingredients.splice(index, 1);
}
})
.catch((err) => console.warn(err));
async deleteIngredient(ingredient: FG.Ingredient) {
await api.delete(`pricelist/ingredients/${ingredient.id}`);
},
async setDrink(drink: FG.Drink) {
const { data } = await api.post<FG.Drink>('pricelist/drinks', drink);
async setDrink(drink: Drink) {
const { data } = await api.post<FG.Drink>('pricelist/drinks', {
...drink,
});
const _drink = new Drink(data);
data.volumes.forEach((volume) => {
const _volume = new DrinkPriceVolume(volume);
_drink.volumes.push(_volume);
});
this.drinks.push(_drink);
this.create_min_prices();
calc_all_min_prices(this.drinks, this.min_prices);
return _drink;
},
async updateDrink(drink: Drink) {
const { data } = await api.put<FG.Drink>(`pricelist/drinks/${drink.id}`, drink);
const { data } = await api.put<FG.Drink>(`pricelist/drinks/${drink.id}`, {
...drink,
});
const index = this.drinks.findIndex((a) => a.id === data.id);
if (index > -1) {
const _drink = new Drink(data);
@ -220,7 +200,7 @@ export const usePricelistStore = defineStore({
});
this.drinks[index] = _drink;
}
this.create_min_prices();
calc_all_min_prices(this.drinks, this.min_prices);
},
deleteDrink(drink: Drink) {
api
@ -233,81 +213,101 @@ export const usePricelistStore = defineStore({
})
.catch((err) => console.warn(err));
},
getPriceCalcColumn(userid: string) {
api
.get(`pricelist/users/${userid}/pricecalc_columns`)
.then(({ data }: AxiosResponse<Array<string>>) => {
if (data.length > 0) {
this.pricecalc_columns = data;
}
})
.catch((err) => console.log(err));
},
updatePriceCalcColumn(userid: string, data: Array<string>) {
api
.put(`pricelist/users/${userid}/pricecalc_columns`, data)
.then(() => {
this.pricecalc_columns = data;
})
.catch((err) => console.log(err));
},
async get_min_prices() {
const { data } = await api.get<Array<number>>('pricelist/settings/min_prices');
this.min_prices = data;
},
async set_min_prices() {
await api.post<Array<number>>('pricelist/settings/min_prices', this.min_prices);
this.create_min_prices();
},
create_min_prices() {
this.drinks.forEach((drink) => {
drink.volumes.forEach((volume) => {
volume.min_prices = [];
this.min_prices.forEach((min_price) => {
let computedMinPrice: ComputedRef;
if (drink.cost_price_pro_volume) {
computedMinPrice = computed<number>(
() =>
(<number>(<unknown>drink.cost_price_pro_volume) *
<number>(<unknown>volume.volume) *
min_price) /
100
);
} else {
computedMinPrice = computed<number>(() => {
let retVal = 0;
let extraIngredientPrice = 0;
volume.ingredients.forEach((ingredient) => {
if (ingredient.drink_ingredient) {
const _drink = usePricelistStore().drinks.find(
(a) => a.id === ingredient.drink_ingredient?.ingredient_id
);
retVal +=
ingredient.drink_ingredient.volume *
<number>(<unknown>_drink?.cost_price_pro_volume);
}
if (ingredient.extra_ingredient) {
extraIngredientPrice += ingredient.extra_ingredient.price;
}
});
return (retVal * min_price) / 100 + extraIngredientPrice;
});
}
volume.min_prices.push({ percentage: min_price, price: computedMinPrice });
});
});
});
calc_all_min_prices(this.drinks, this.min_prices);
},
async upload_drink_picture(drink: Drink, file: File) {
const formData = new FormData();
formData.append('file', file);
await api.post(`pricelist/drinks/${drink.id}/picture`, formData, {
const { data } = await api.post<FG.Drink>(`pricelist/drinks/${drink.id}/picture`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
const _drink = this.drinks.find((a) => a.id === drink.id);
if (_drink) {
_drink.uuid = data.uuid;
}
},
async delete_drink_picture(drink: Drink) {
await api.delete(`pricelist/drinks/${drink.id}/picture`);
drink.uuid = '';
},
async getTags() {
const { data } = await api.get<Array<FG.Tag>>('/pricelist/tags');
this.tags = data;
},
async setTag(tag: FG.Tag) {
const { data } = await api.post<FG.Tag>('/pricelist/tags', tag);
this.tags.push(data);
},
async updateTag(tag: FG.Tag) {
const { data } = await api.put<FG.Tag>(`/pricelist/tags/${tag.id}`, tag);
const index = this.tags.findIndex((a) => a.id === data.id);
if (index > -1) {
this.tags[index] = data;
}
},
async deleteTag(tag: FG.Tag) {
await api.delete(`/pricelist/tags/${tag.id}`);
const index = this.tags.findIndex((a) => a.id === tag.id);
if (index > -1) {
this.tags.splice(index, 1);
}
},
async getPriceCalcColumn(userid: string) {
const { data } = await api.get<Array<string>>(`pricelist/users/${userid}/pricecalc_columns`);
this.pricecalc_columns = data;
},
async updatePriceCalcColumn(userid: string, data: Array<string>) {
await api.put<Array<string>>(`pricelist/users/${userid}/pricecalc_columns`, data);
this.pricecalc_columns = data;
},
async getPriceListView(userid: string) {
const { data } = await api.get<{ value: boolean }>(`pricelist/users/${userid}/pricelist`);
this.pricelist_view = data.value;
},
async updatePriceListView(userid: string, data: boolean) {
await api.put<Array<string>>(`pricelist/users/${userid}/pricelist`, { value: data });
this.pricelist_view = data;
},
async getPriceListColumnOrder(userid: string) {
const { data } = await api.get<Array<Order>>(
`pricelist/users/${userid}/pricecalc_columns_order`
);
this.pricelist_columns_order = data;
},
async updatePriceListColumnOrder(userid: string, data: Array<Order>) {
await api.put<Array<string>>(`pricelist/users/${userid}/pricecalc_columns_order`, data);
this.pricelist_columns_order = data;
},
},
getters: {
pricelist() {
const retVal: Array<Pricelist> = [];
this.drinks.forEach((drink) => {
drink.volumes.forEach((volume) => {
volume.prices.forEach((price) => {
retVal.push({
name: drink.name,
type: <FG.DrinkType>drink.type,
tags: <Array<FG.Tag>>drink.tags,
volume: <number>volume.volume,
price: price.price,
public: price.public,
description: <string>price.description,
});
});
});
});
return retVal;
},
},
});
export { DrinkPriceVolume, MinPrice, Drink };
export { DrinkPriceVolume, Drink, Order };

View File

@ -0,0 +1,39 @@
import { Drink } from '../store';
function filter(
rows: Array<Drink>,
terms: Search,
cols: Array<Col>,
cellValue: { (col: Col, row: Drink): string }
) {
if (terms.value) {
return rows.filter((row) => {
if (!terms.key || terms.key === '') {
return cols.some((col) => {
const val = cellValue(col, row) + '';
const haystack = val === 'undefined' || val === 'null' ? '' : val.toLowerCase();
return haystack.indexOf(terms.value) !== -1;
});
}
const index = cols.findIndex((col) => col.name === terms.key);
const val = cellValue(cols[index], row) + '';
const haystack = val === 'undefined' || val === 'null' ? '' : val.toLowerCase();
return haystack.indexOf(terms.value) !== -1;
});
}
return rows;
}
interface Search {
value: string;
label: string | undefined;
key: string | undefined;
}
interface Col {
name: string;
label: string;
field: string;
}
export { filter, Search, Col };

View File

@ -0,0 +1,7 @@
function sort(a: string | number, b: string | number) {
if (a > b) return 1;
if (b > a) return -1;
return 0;
}
export { sort };

View File

@ -0,0 +1,84 @@
import { Drink, DrinkPriceVolume, usePricelistStore } from 'src/plugins/pricelist/store';
function calc_volume(volume: DrinkPriceVolume) {
if (volume.ingredients.some((ingredient) => !!ingredient.drink_ingredient)) {
let retVal = 0;
volume.ingredients.forEach((ingredient) => {
if (ingredient.drink_ingredient?.volume) {
retVal += ingredient.drink_ingredient.volume;
}
});
return retVal;
} else {
return volume._volume;
}
}
function calc_cost_per_volume(drink: Drink) {
let retVal = drink._cost_per_volume;
if (!!drink.volume && !!drink.package_size && !!drink.cost_per_package) {
retVal =
((drink.cost_per_package || 0) / ((drink.volume || 0) * (drink.package_size || 0))) * 1.19;
}
return retVal ? Math.round(retVal * 1000) / 1000 : retVal;
}
function calc_all_min_prices(drinks: Array<Drink>, min_prices: Array<number>) {
drinks.forEach((drink) => {
drink.volumes.forEach((volume) => {
volume.min_prices = calc_min_prices(volume, drink.cost_per_volume, min_prices);
});
});
}
function helper(volume: DrinkPriceVolume, min_price: number) {
let retVal = 0;
let extraIngredientPrice = 0;
volume.ingredients.forEach((ingredient) => {
if (ingredient.drink_ingredient) {
const _drink = usePricelistStore().drinks.find(
(a) => a.id === ingredient.drink_ingredient?.ingredient_id
);
retVal += ingredient.drink_ingredient.volume * <number>(<unknown>_drink?.cost_per_volume);
}
if (ingredient.extra_ingredient) {
extraIngredientPrice += ingredient.extra_ingredient.price;
}
});
return (retVal * min_price) / 100 + extraIngredientPrice;
}
function calc_min_prices(
volume: DrinkPriceVolume,
cost_per_volume: number | undefined,
min_prices: Array<number>
) {
const retVal: Array<FG.MinPrices> = [];
volume.min_prices = [];
if (min_prices) {
min_prices.forEach((min_price) => {
let computedMinPrice: number;
if (cost_per_volume) {
computedMinPrice = (cost_per_volume * <number>volume.volume * min_price) / 100;
} else {
computedMinPrice = helper(volume, min_price);
}
retVal.push({ percentage: min_price, price: computedMinPrice });
});
}
return retVal;
}
function clone<T>(o: T): T {
return <T>JSON.parse(JSON.stringify(o));
}
interface DeleteObjects {
prices: Array<FG.DrinkPrice>;
volumes: Array<DrinkPriceVolume>;
ingredients: Array<FG.Ingredient>;
}
export { DeleteObjects };
export { calc_volume, calc_cost_per_volume, calc_all_min_prices, calc_min_prices, clone };

303
yarn.lock
View File

@ -904,7 +904,7 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@electron/get@^1.3.1":
"@electron/get@^1.0.1", "@electron/get@^1.3.1", "@electron/get@^1.6.0":
version "1.12.4"
resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.12.4.tgz#a5971113fc1bf8fa12a8789dc20152a7359f06ab"
integrity sha512-6nr9DbJPUR9Xujw6zD3y+rS95TyItEVM0NVjt1EehY2vUWfIgPiIPVHxCvaTS0xr2B+DRxovYVKbuOWqC35kjg==
@ -1155,7 +1155,7 @@
"@quasar/quasar-app-extension-qcalendar@file:deps/quasar-ui-qcalendar/app-extension":
version "4.0.0-alpha.1"
dependencies:
"@quasar/quasar-ui-qcalendar" "link:../../../../../.cache/yarn/v6/npm-@quasar-quasar-app-extension-qcalendar-4.0.0-alpha.1-58d88196-365a-4cd9-b287-335eb40035ac-1617715095074/node_modules/@quasar/ui"
"@quasar/quasar-ui-qcalendar" "link:../../Library/Caches/Yarn/v6/npm-@quasar-quasar-app-extension-qcalendar-4.0.0-alpha.1-c42f9f4b-6777-438d-bf23-9bdcfb44ae8c-1618669700018/node_modules/@quasar/ui"
"@quasar/quasar-ui-qcalendar@link:deps/quasar-ui-qcalendar/ui":
version "0.0.0"
@ -1318,6 +1318,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.7.tgz#1cb61fd0c85cb87e728c43107b5fd82b69bc9ef8"
integrity sha512-gWL8VUkg8VRaCAUgG9WmhefMqHmMblxe2rVpMF86nZY/+ZysU+BkAp+3cz03AixWDSSz0ks5WX59yAhv/cDwFA==
"@types/node@^14.6.2":
version "14.14.41"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615"
integrity sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -2182,6 +2187,21 @@ arrify@^2.0.1:
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
asar@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/asar/-/asar-2.1.0.tgz#97c6a570408c4e38a18d4a3fb748a621b5a7844e"
integrity sha512-d2Ovma+bfqNpvBzY/KU8oPY67ZworixTpkjSx0PCXnQi67c2cXmssaTxpFDUM0ttopXoGx/KRxNg/GDThYbXQA==
dependencies:
chromium-pickle-js "^0.2.0"
commander "^2.20.0"
cuint "^0.2.2"
glob "^7.1.3"
minimatch "^3.0.4"
mkdirp "^0.5.1"
tmp-promise "^1.0.5"
optionalDependencies:
"@types/glob" "^7.1.1"
asn1.js@^5.2.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
@ -2259,6 +2279,11 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
author-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/author-regex/-/author-regex-1.0.0.tgz#d08885be6b9bbf9439fe087c76287245f0a81450"
integrity sha1-0IiFvmubv5Q5/gh8dihyRfCoFFA=
autoprefixer@9.8.6:
version "9.8.6"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
@ -2412,7 +2437,7 @@ bl@^4.0.3:
inherits "^2.0.4"
readable-stream "^3.4.0"
bluebird@^3.5.0, bluebird@^3.5.5, bluebird@^3.7.2:
bluebird@^3.1.1, bluebird@^3.5.0, bluebird@^3.5.5, bluebird@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
@ -2607,7 +2632,7 @@ buffer-alloc@^1.2.0:
buffer-alloc-unsafe "^1.1.0"
buffer-fill "^1.0.0"
buffer-crc32@^0.2.1, buffer-crc32@^0.2.13:
buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
@ -2911,6 +2936,11 @@ chrome-trace-event@^1.0.2:
dependencies:
tslib "^1.9.0"
chromium-pickle-js@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205"
integrity sha1-BKEGZywYsIWrd02YPfo+oTjyIgU=
ci-info@2.0.0, ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
@ -3160,7 +3190,7 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
concat-stream@^1.5.0:
concat-stream@^1.5.0, concat-stream@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@ -3505,6 +3535,13 @@ cross-spawn@^6.0.0:
shebang-command "^1.2.0"
which "^1.2.9"
cross-zip@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cross-zip/-/cross-zip-3.1.0.tgz#2b7d33f2a893bf83e232ccbabf4c6c706f6b313c"
integrity sha512-aX02l0SD3KE27pMl69gkxDdDM5D3u9Ic4Je+2b1B2fP0dWnlWWY6ns2Vk5DEgCXJRhL3GasSpicNQRNbDkq0+w==
dependencies:
rimraf "^3.0.0"
crypto-browserify@^3.11.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
@ -3680,6 +3717,11 @@ csstype@^2.6.8:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.16.tgz#544d69f547013b85a40d15bff75db38f34fe9c39"
integrity sha512-61FBWoDHp/gRtsoDkq/B1nWrCUG/ok1E3tUrcNbZjsE9Cxd9yzUirjS3+nAATB8U4cTtaQmAHbNndoFz5L6C9Q==
cuint@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=
currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@ -3699,7 +3741,7 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8:
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@ -4047,6 +4089,14 @@ electron-notarize@^0.1.1:
debug "^4.1.1"
fs-extra "^8.0.1"
electron-notarize@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-0.2.1.tgz#759e8006decae19134f82996ed910db26d9192cc"
integrity sha512-oZ6/NhKeXmEKNROiFmRNfytqu3cxqC95sjooG7kBXQVEUSQkZnbiAhxVh5jXngL881G197pbwpeVPJyM7Ikmxw==
dependencies:
debug "^4.1.1"
fs-extra "^8.1.0"
electron-osx-sign@^0.4.11:
version "0.4.17"
resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.17.tgz#2727ca0c79e1e4e5ccd3861fb3da9c3c913b006c"
@ -4059,11 +4109,43 @@ electron-osx-sign@^0.4.11:
minimist "^1.2.0"
plist "^3.0.1"
electron-packager@^14.1.1:
version "14.2.1"
resolved "https://registry.yarnpkg.com/electron-packager/-/electron-packager-14.2.1.tgz#e1884eee608455e71e96342717e0527d25a329df"
integrity sha512-g6y3BVrAOz/iavKD+VMFbehrQcwCWuA3CZvVbmmbQuCfegGA1ytwWn0BNIDDrEdbuz31Fti7mnNHhb5L+3Wq9A==
dependencies:
"@electron/get" "^1.6.0"
asar "^2.0.1"
cross-zip "^3.0.0"
debug "^4.0.1"
electron-notarize "^0.2.0"
electron-osx-sign "^0.4.11"
fs-extra "^8.1.0"
galactus "^0.2.1"
get-package-info "^1.0.0"
junk "^3.1.0"
parse-author "^2.0.0"
plist "^3.0.0"
rcedit "^2.0.0"
resolve "^1.1.6"
sanitize-filename "^1.6.0"
semver "^6.0.0"
yargs-parser "^16.0.0"
electron-to-chromium@^1.3.649:
version "1.3.704"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.704.tgz#894205a237cbe0097d63da8f6d19e605dd13ab51"
integrity sha512-6cz0jvawlUe4h5AbfQWxPzb+8LzVyswGAWiGc32EJEmfj39HTQyNPkLXirc7+L4x5I6RgRkzua8Ryu5QZqc8cA==
electron@^12.0.4:
version "12.0.4"
resolved "https://registry.yarnpkg.com/electron/-/electron-12.0.4.tgz#c2ca4710d0e4da7db6d31c4f55777b08bfcb08e5"
integrity sha512-A8Lq3YMZ1CaO1z5z5nsyFxIwkgwXLHUwL2pf9MVUHpq7fv3XUewCMD98EnLL3DdtiyCvw5KMkeT1WGsZh8qFug==
dependencies:
"@electron/get" "^1.0.1"
"@types/node" "^14.6.2"
extract-zip "^1.0.3"
elementtree@0.1.7, elementtree@^0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/elementtree/-/elementtree-0.1.7.tgz#9ac91be6e52fb6e6244c4e54a4ac3ed8ae8e29c0"
@ -4183,7 +4265,7 @@ errno@^0.1.3, errno@~0.1.7:
dependencies:
prr "~1.0.1"
error-ex@^1.3.1:
error-ex@^1.2.0, error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
@ -4577,6 +4659,16 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
extract-zip@^1.0.3:
version "1.7.0"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
dependencies:
concat-stream "^1.6.2"
debug "^2.6.9"
mkdirp "^0.5.4"
yauzl "^2.10.0"
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@ -4633,6 +4725,13 @@ faye-websocket@^0.11.3:
dependencies:
websocket-driver ">=0.5.1"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
dependencies:
pend "~1.2.0"
figgy-pudding@^3.5.1:
version "3.5.2"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
@ -4733,7 +4832,7 @@ find-cache-dir@^3.3.1:
make-dir "^3.0.2"
pkg-dir "^4.1.0"
find-up@^2.1.0:
find-up@^2.0.0, find-up@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
@ -4776,6 +4875,14 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
flora-colossus@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/flora-colossus/-/flora-colossus-1.0.1.tgz#aba198425a8185341e64f9d2a6a96fd9a3cbdb93"
integrity sha512-d+9na7t9FyH8gBJoNDSi28mE4NgQVGGvxQ4aHtFRetjyh5SXjuus+V5EZaxFmFdXVemSOrx0lsgEl/ZMjnOWJA==
dependencies:
debug "^4.1.1"
fs-extra "^7.0.0"
flush-write-stream@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@ -4875,6 +4982,24 @@ fs-extra@9.1.0, fs-extra@^9.0.0, fs-extra@^9.0.1:
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-extra@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94"
integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^8.0.1, fs-extra@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
@ -4934,6 +5059,15 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
galactus@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/galactus/-/galactus-0.2.1.tgz#cbed2d20a40c1f5679a35908e2b9415733e78db9"
integrity sha1-y+0tIKQMH1Z5o1kI4rlBVzPnjbk=
dependencies:
debug "^3.1.0"
flora-colossus "^1.0.0"
fs-extra "^4.0.0"
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@ -4974,6 +5108,16 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
has "^1.0.3"
has-symbols "^1.0.1"
get-package-info@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/get-package-info/-/get-package-info-1.0.0.tgz#6432796563e28113cd9474dbbd00052985a4999c"
integrity sha1-ZDJ5ZWPigRPNlHTbvQAFKYWkmZw=
dependencies:
bluebird "^3.1.1"
debug "^2.2.0"
lodash.get "^4.0.0"
read-pkg-up "^2.0.0"
get-stream@^4.0.0, get-stream@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
@ -6278,6 +6422,11 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
junk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
keyv@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
@ -6369,6 +6518,16 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
load-json-file@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=
dependencies:
graceful-fs "^4.1.2"
parse-json "^2.2.0"
pify "^2.0.0"
strip-bom "^3.0.0"
loader-runner@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
@ -6455,6 +6614,11 @@ lodash.flatten@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
lodash.get@^4.0.0:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
@ -6907,7 +7071,7 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@ -7119,7 +7283,7 @@ nopt@^5.0.0:
dependencies:
abbrev "1"
normalize-package-data@^2.0.0:
normalize-package-data@^2.0.0, normalize-package-data@^2.3.2:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
@ -7677,6 +7841,20 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5:
pbkdf2 "^3.0.3"
safe-buffer "^5.1.1"
parse-author@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/parse-author/-/parse-author-2.0.0.tgz#d3460bf1ddd0dfaeed42da754242e65fb684a81f"
integrity sha1-00YL8d3Q367tQtp1QkLmX7aEqB8=
dependencies:
author-regex "^1.0.0"
parse-json@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
dependencies:
error-ex "^1.2.0"
parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@ -7763,6 +7941,13 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-type@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=
dependencies:
pify "^2.0.0"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@ -7779,6 +7964,11 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@ -7854,7 +8044,7 @@ pkg-up@^3.1.0:
dependencies:
find-up "^3.0.0"
plist@^3.0.1:
plist@^3.0.0, plist@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.2.tgz#74bbf011124b90421c22d15779cee60060ba95bc"
integrity sha512-MSrkwZBdQ6YapHy87/8hDU8MnIcyxBKjeF+McXnr5A9MtffPewTs7G3hlpodT5TacyfIyFTaJEhh3GGcmasTgQ==
@ -8525,6 +8715,11 @@ rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
rcedit@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/rcedit/-/rcedit-2.3.0.tgz#951685a079db98a4cc8c21ebab75e374d5a0b108"
integrity sha512-h1gNEl9Oai1oijwyJ1WYqYSXTStHnOcv1KYljg/8WM4NAg3H1KBK3azIaKkQ1WQl+d7PoJpcBMscPfLXVKgCLQ==
read-chunk@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-3.2.0.tgz#2984afe78ca9bfbbdb74b19387bf9e86289c16ca"
@ -8551,6 +8746,23 @@ read-package-json-fast@^2.0.1:
normalize-package-data "^2.0.0"
npm-normalize-package-bin "^1.0.0"
read-pkg-up@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=
dependencies:
find-up "^2.0.0"
read-pkg "^2.0.0"
read-pkg@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=
dependencies:
load-json-file "^2.0.0"
normalize-package-data "^2.3.2"
path-type "^2.0.0"
read@1, read@~1.0.1:
version "1.0.7"
resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
@ -8795,7 +9007,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.15.1:
resolve@^1.1.6, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.15.1:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@ -8944,6 +9156,13 @@ safe-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sanitize-filename@^1.6.0:
version "1.6.3"
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378"
integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==
dependencies:
truncate-utf8-bytes "^1.0.0"
sass-loader@10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.1.0.tgz#1727fcc0c32ab3eb197cda61d78adf4e9174a4b3"
@ -9306,6 +9525,11 @@ sort-keys@^1.0.0:
dependencies:
is-plain-obj "^1.0.0"
sortablejs@1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290"
integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
source-list-map@^2.0.0, source-list-map@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
@ -9613,6 +9837,11 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
strip-bom@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
@ -9862,6 +10091,21 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tmp-promise@^1.0.5:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-1.1.0.tgz#bb924d239029157b9bc1d506a6aa341f8b13e64c"
integrity sha512-8+Ah9aB1IRXCnIOxXZ0uFozV1nMU5xiu7hhFVUSxZ3bYu+psD4TzagCzVbexUCgNNGJnsmNDQlS4nG3mTyoNkw==
dependencies:
bluebird "^3.5.0"
tmp "0.1.0"
tmp@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==
dependencies:
rimraf "^2.6.3"
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@ -9950,6 +10194,13 @@ tough-cookie@~2.5.0:
psl "^1.1.28"
punycode "^2.1.1"
truncate-utf8-bytes@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
integrity sha1-QFkjkJWS1W94pYGENLC3hInKXys=
dependencies:
utf8-byte-length "^1.0.1"
ts-loader@8.0.17:
version "8.0.17"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.17.tgz#98f2ccff9130074f4079fd89b946b4c637b1f2fc"
@ -10252,6 +10503,11 @@ use@^3.1.0:
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
utf8-byte-length@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
integrity sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@ -10396,6 +10652,13 @@ vue@3.0.11:
"@vue/runtime-dom" "3.0.11"
"@vue/shared" "3.0.11"
vuedraggable@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-4.0.1.tgz#3bcaab0808b7944030b7d9a29f9a63d59dfa12c5"
integrity sha512-7qN5jhB1SLfx5P+HCm3JUW+pvgA1bSLgYLSVOeLWBDH9z+zbaEH0OlyZBVMLOxFR+JUHJjwDD0oy7T4r9TEgDA==
dependencies:
sortablejs "1.10.2"
vuex@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.0.tgz#ac877aa76a9c45368c979471e461b520d38e6cf5"
@ -10802,6 +11065,14 @@ yargs-parser@^13.1.2:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^16.0.0:
version "16.1.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1"
integrity sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
@ -10843,6 +11114,14 @@ yargs@^13.3.2:
y18n "^4.0.0"
yargs-parser "^13.1.2"
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"