Fully functional

This commit is contained in:
sebivh
2025-09-21 17:41:48 +02:00
parent 09bce907ba
commit 31034cb81d
11 changed files with 693 additions and 51 deletions

View File

@@ -19,8 +19,20 @@ const addToCart = async (barcode: number) => {
'GROCY-API-KEY': "VCPlNborGO2t8rs08cvqodalf3AjecRwgexWzkIk221mtshLoM"
}
});
const userfields = await request2.json();
console.log(`Price of product ${data.product.name} is ${userfields.kuelschrankpreis} EUR`);
return {name: data.product.name, price: userfields.kuelschrankpreis};
if ( data.product.picture_file_name ) {
const image_fetch = await fetch(`https://development.vonhelmersen.online/api/files/productpictures/${btoa(data.product.picture_file_name)}`, {
method: 'GET',
headers: {
'accept': "application/octet-stream",
'GROCY-API-KEY': "VCPlNborGO2t8rs08cvqodalf3AjecRwgexWzkIk221mtshLoM"
}});
const img_blob = await image_fetch.blob()
return {name: data.product.name, price: userfields.kuelschrankpreis, img_blob: img_blob};
} else {
return {name: data.product.name, price: userfields.kuelschrankpreis};
}
}
export default addToCart;

View File

@@ -1,6 +1,7 @@
"use server"
const API_KEY = "sup_sk_t6Jf0FwYUAkbjxzvbs6e3BkIePxm0OR3m"
const HOSTNAME = "kasse.fet.at"
import SumUp from "@sumup/sdk";
@@ -13,10 +14,12 @@ const checkout = async (total: number) => {
const merchantCode = (await client.merchant.getMerchantProfile()).merchant_code || "";
const readerId = (await client.readers.list(merchantCode)).items[0].id || "";
const respomse = await client.readers.createCheckout(merchantCode, readerId, {total_amount: {value: total * 100, currency: "EUR", minor_unit: 2}});
const response = await client.readers.createCheckout(merchantCode, readerId, {
total_amount: {value: total * 100, currency: "EUR", minor_unit: 2},
return_url: `https://${HOSTNAME}/order-status`
});
await new Promise(resolve => setTimeout(resolve, 10000));
const checkout = await client.transactions.get(merchantCode, {client_transaction_id: respomse.data?.client_transaction_id || ""});
console.log("Checkout status:", checkout);
return response.data?.client_transaction_id || "";
}
export default checkout;

View File

@@ -0,0 +1,57 @@
// app/api/order-status/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { EventEmitter } from "stream";
export const orderEvents = new EventEmitter();
export async function GET(req: NextRequest) {
// const orderId = params.id;
const orderId = req.nextUrl.searchParams.get("id");
return new Response(
new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
const listener = (data: { id: string; event_type: string }) => {
// TODO!!!! UNCOMMEND THIS
// if (data.orderId != orderId) {
// return;
// }
console.error("THIS SHOULD BE UNCOMMENTED");
console.log("Sending event:", data);
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
};
// orderEvents.on("update", listener);
orderEvents.addListener("update", listener);
// cleanup when client disconnects
req.signal.addEventListener("abort", () => {
orderEvents.off("update", listener);
controller.close();
});
},
}),
{
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
}
);
}
export async function POST(req: NextRequest) {
const body = await req.json();
console.log("Webhook received:", body);
const event_type = body.event_type || "";
const id = body.id || "";
orderEvents.emit("update", { id, event_type });
return NextResponse.json({ message: 'Webhook received' });
}

View File

@@ -1,26 +1,11 @@
@import "tailwindcss";
@plugin "flyonui";
@import "flyonui/variants.css";
@plugin "@iconify/tailwind4";
@source "./node_modules/flyonui/flyonui.js";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Geist, Geist_Mono, Inter_Tight } from "next/font/google";
import "./globals.css";
import FlyonuiScript from '../components/FlyonuiScript';
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -24,11 +25,15 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
<body>
<div className="p-6">
<div className={`flex w-full flex-row justify-center items-center mb-4 bg-primary h-16 rounded-2xl`}>
<h1 className="text-3xl text-white">Baroness</h1>
</div>
{children}
</div>
<FlyonuiScript />
</body>
</html>
);
}

View File

@@ -1,10 +1,20 @@
"use client"
import addToCart from "@/actions/add-to-cart";
import checkout from "@/actions/checkout";
import { useState } from "react";
import { useEffect, useState } from "react";
const createTableImage = (img_blob: Blob | undefined) => {
if (!img_blob) {
return <div className="flex h-full w-full items-center justify-center text-3xl">?</div>
} else {
return <img src={URL.createObjectURL(img_blob || "")} alt="product image" />
}
}
const quick_products = [ {name: "Cola"}, {name: "Fanta"}, {name: "Wasser"}, {name: "Red Bull"}, {name: "Bier"}, {name: "Wein"} ];
export default function Home() {
const [ cart, setCart ] = useState<{name: string, price: number}[]>([]);
const [ cart, setCart ] = useState<{name: string, price: number, img_blob?: Blob | undefined}[]>([]);
const handleSubmit = async (formData: FormData) => {
const barcode = formData.get("barcode")? Number(formData.get("barcode")) : 0;
addToCart(barcode).then((item) => {
@@ -17,25 +27,197 @@ export default function Home() {
cart.forEach((item) => {
total += (Number(item.price));
});
checkout(total);
setCart([]);
const client_checkout_id = checkout(total);
setStatus("Initializing");
window.HSOverlay.open('#transparent-modal');
}
const [status, setStatus] = useState("NONE");
useEffect(() => {
const but = document.getElementById("open-modal");
but?.addEventListener("click", () => {
window.HSOverlay.open('#failed-modal');
});
}, []);
useEffect(() => {
const es = new EventSource(`/api/order-status/`);
console.log("EventSource created");
es.onmessage = (event) => {
const data = JSON.parse(event.data);
setStatus(data.event_type);
console.log("Received event:", data);
switch(data.event_type) {
case "PENDING":
setStatus("Verarbeiteung...");
break;
case "PAID":
// Order is completed
setStatus("Erfolgreich bezahlt!");
window.HSOverlay.close('#transparent-modal')
window.HSOverlay.open('#success-modal');
setStatus("NONE");
setCart([]);
break;
case "FAILED":
window.HSOverlay.open('#failed-modal');
setStatus("NONE");
break;
default:
// Unknown event type
break;
}
};
return () => es.close();
}, []);
return (
<>
<h1>Barcode:</h1>
<form action={handleSubmit}>
<input type="number" name="barcode" placeholder="Enter barcode" />
<button type="submit">Submit</button>
</form>
<h2>Cart:</h2>
<ul>
{cart.map((item, index) => (
<li key={index}>{item.name}: {item.price} EUR</li>
))}
</ul>
<form action={handleCheckout}>
<button type="submit">Checkout</button>
</form>
<div className="flex flex-row px-6 gap-6">
<div className="w-3/5">
<div className="border-base-content/25 w-full rounded-lg border">
<div className="overflow-x-auto">
<table className="table">
<tbody>
<tr>
<td><span className="icon-[tabler--barcode] size-16"></span></td>
<td>Scanne den Barcode deines Produkts um es dem Warenkorb hinzuzufügen.</td>
</tr>
<tr>
<td><span className="icon-[tabler--shopping-cart] size-16"></span></td>
<td><span>Überprüfe deinen Warenkorb und entferne Produkte, die du nicht kaufen möchtest.</span><br /><span> Klicke anschließend auf Bezahlen.</span></td>
</tr>
<tr>
<td><span className="icon-[tabler--credit-card] size-16"></span></td>
<td>Bezahlen mit Karte am Karten Terminal</td>
</tr>
</tbody>
</table>
</div>
</div>
<h2 className="mt-6 text-xl">Schnellauswahl:</h2>
<div className="grid grid-cols-4 gap-4">
{quick_products.map((item, index) => (
<button key={index} className="btn btn-primary" onClick={async () => {
const new_item = await addToCart(item.name === "Bier" ? 400014 : item.name === "Wein" ? 400015 : item.name === "Cola" ? 400000 : item.name === "Fanta" ? 400001 : item.name === "Wasser" ? 400002 : item.name === "Red Bull" ? 400003 : 0);
const newCart = [...cart, new_item];
setCart(newCart);
}}>{item.name}</button>
))}
</div>
<h2 className="text-xl">Barcode:</h2>
<form action={handleSubmit}>
<div className="input max-w-sm" data-input-number>
<input type="text" name="barcode" aria-label="Input barcode" data-input-text-input />
<span className="my-auto flex gap-3">
<button type="button" className="btn btn-primary btn-soft size-5.5 min-h-0 rounded-sm p-0" aria-label="Decrement button" data-input-number-decrement >
<span className="icon-[tabler--minus] size-3.5 shrink-0"></span>
</button>
<button type="button" className="btn btn-primary btn-soft size-5.5 min-h-0 rounded-sm p-0" aria-label="Increment button" data-input-number-increment >
<span className="icon-[tabler--plus] size-3.5 shrink-0"></span>
</button>
</span>
</div>
<button className="btn btn-primary" type="submit">Submit</button>
</form>
<button id="open-modal" className="btn btn-primary">Show Modal</button>
</div>
<div className="w-2/5">
<h2>Cart:</h2>
<table className="table-borderless table-striped table ">
<thead>
<tr className="border-0 bg-base-300/20 *:first:rounded-s-md *:last:rounded-e-md">
<th>Bild</th>
<th>Name</th>
<th>Preis</th>
<th></th>
</tr>
</thead>
<tbody>
{cart.map((item, index) => (
<tr key={index}>
<td className="avatar">
<div className="bg-base-content/10 h-10 w-10 rounded-md">
{ createTableImage(item.img_blob) }
</div>
</td>
<td>{item.name}</td>
<td>{item.price}</td>
<td><button className="btn btn-square bg-error" onClick={
() => {
const newCart = cart.filter((_, i) => i !== index);
setCart(newCart);
}
}><span className="icon-[tabler--trash] size-4.5 shrink-0"></span></button></td>
</tr>
))}
</tbody>
</table>
<form action={handleCheckout}>
<button className="btn btn-square btn-primary" aria-label="Icon Button" type="submit"><span className="icon-[tabler--shopping-cart] size-4.5 shrink-0"></span></button>
</form>
</div>
</div>
<div id="transparent-modal" className="overlay modal overlay-open:opacity-100 overlay-open:duration-300 hidden place-items-center" role="dialog" tabIndex={-1}>
<div className="modal-dialog">
<div className="modal-content text-white bg-primary shadow-none">
<div className="modal-header">
<h3 className="modal-title text-white">Bitte bezahle auf dem Karten Terminal</h3>
</div>
<div className="modal-body">
<div className="flex w-full flex-col justify-center items-center">
<span className="loading loading-infinity size-56"></span>
<span>Status: {status}</span>
</div>
</div>
<div className="modal-footer">
</div>
</div>
</div>
</div>
<div id="success-modal" className="overlay modal overlay-open:opacity-100 overlay-open:duration-300 hidden place-items-center" role="dialog" tabIndex={-1}>
<div className="modal-dialog">
<div className="modal-content text-white bg-success shadow-none">
<div className="modal-header">
<h3 className="modal-title text-white">Danke für die Spende!</h3>
</div>
<div className="modal-body">
<div className="flex w-full flex-col justify-center items-center">
<span className="icon-[tabler--check] size-56"></span>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-overlay="#success-modal">Close</button>
</div>
</div>
</div>
</div>
<div id="failed-modal" className="overlay modal overlay-open:opacity-100 overlay-open:duration-300 hidden place-items-center" role="dialog" tabIndex={-1}>
<div className="modal-dialog">
<div className="modal-content text-white bg-red-800 shadow-none">
<div className="modal-header">
<h3 className="modal-title text-white">Das hat leider nicht funktioniert! Bitte versuche es erneut!</h3>
</div>
<div className="modal-body">
<div className="flex w-full flex-col justify-center items-center">
<span className="icon-[tabler--poo] size-56"></span>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-overlay="#failed-modal">Close</button>
</div>
</div>
</div>
</div>
</>
)
}

View File

@@ -2,10 +2,15 @@
import getReaderList from "@/actions/getReaderList";
import registerReader from "@/actions/registerReader";
import { useState } from "react";
import { useEffect, useState } from "react";
const Page = () => {
const [ readers, setReaders ] = useState<string[]>();
useEffect(() => {
getReaderList().then((readerList) => {
setReaders(readerList);
});
}, readers);
const handleRegisterReader = async (formData: FormData) => {
const readername = formData.get("readername")?.toString() || "";
const pairingcode = formData.get("pairingcode")?.toString() || "";

View File

@@ -0,0 +1,47 @@
// FlyonuiScript.tsx
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
// Optional third-party libraries
// import $ from 'jquery';
// import _ from 'lodash';
// import noUiSlider from 'nouislider';
// import 'datatables.net';
// import 'dropzone/dist/dropzone-min.js';
// window.$ = $;
// window._ = _;
// window.jQuery = $;
// window.DataTable = $.fn.dataTable;
// window.noUiSlider = noUiSlider;
async function loadFlyonUI() {
return import('flyonui/flyonui');
}
export default function FlyonuiScript() {
const path = usePathname();
useEffect(() => {
const initFlyonUI = async () => {
await loadFlyonUI();
};
initFlyonUI();
}, []);
useEffect(() => {
setTimeout(() => {
if (
window.HSStaticMethods &&
typeof window.HSStaticMethods.autoInit === 'function'
) {
window.HSStaticMethods.autoInit();
}
}, 100);
}, [path]);
return null;
}