Compare commits
2 Commits
d38c5ae91b
...
4416ce3280
| Author | SHA1 | Date | |
|---|---|---|---|
| 4416ce3280 | |||
| 0e18760e5d |
BIN
backend/app.db
BIN
backend/app.db
Binary file not shown.
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -43,10 +44,16 @@ type Order struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FinalizedSummary struct {
|
||||||
|
Pickup []string `json:"pickup"`
|
||||||
|
Kitchen []string `json:"kitchen"`
|
||||||
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
Broker *Broker
|
Broker *Broker
|
||||||
FinalizeUpdate chan struct{} // notify scheduler to reload time
|
FinalizeUpdate chan struct{} // notify scheduler to reload time
|
||||||
|
LastSummary *FinalizedSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
type Broker struct {
|
type Broker struct {
|
||||||
@@ -875,7 +882,7 @@ func finalizeOrders(app *App) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: send summary (log, email, or message)
|
// Step 2: send summary (log, email, or message)
|
||||||
sendSummary(summary)
|
sendSummary(app, summary)
|
||||||
|
|
||||||
// Step 3: archive
|
// Step 3: archive
|
||||||
_, err = app.DB.Exec("UPDATE orders SET status = 'history' WHERE status = 'active'")
|
_, err = app.DB.Exec("UPDATE orders SET status = 'history' WHERE status = 'active'")
|
||||||
@@ -886,17 +893,64 @@ func finalizeOrders(app *App) error {
|
|||||||
if data, err := json.Marshal(msg); err == nil {
|
if data, err := json.Marshal(msg); err == nil {
|
||||||
app.Broker.broadcast <- data
|
app.Broker.broadcast <- data
|
||||||
}
|
}
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendSummary(orders []Order) {
|
func sendSummary(app *App, orders []Order) *FinalizedSummary {
|
||||||
// TODO: replace with email or message
|
// Pickup view
|
||||||
log.Println("Daily summary:")
|
userMap := make(map[string][]string)
|
||||||
for _, o := range orders {
|
for _, o := range orders {
|
||||||
log.Printf("%s: %s, %s, %s", o.Username, o.Soup, o.Main, o.Side)
|
items := []string{}
|
||||||
|
if o.Soup != "" {
|
||||||
|
items = append(items, o.Soup)
|
||||||
}
|
}
|
||||||
|
items = append(items, o.Main, o.Side)
|
||||||
|
userMap[o.Username] = append(userMap[o.Username], items...)
|
||||||
|
}
|
||||||
|
pickup := []string{}
|
||||||
|
for user, items := range userMap {
|
||||||
|
pickup = append(pickup, fmt.Sprintf("%s: [%s]", user, strings.Join(items, ", ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kitchen view
|
||||||
|
kitchenMap := make(map[string]int)
|
||||||
|
for _, o := range orders {
|
||||||
|
if o.Soup != "" {
|
||||||
|
kitchenMap[o.Soup]++
|
||||||
|
}
|
||||||
|
kitchenMap[o.Main]++
|
||||||
|
kitchenMap[o.Side]++
|
||||||
|
}
|
||||||
|
kitchen := []string{}
|
||||||
|
for item, count := range kitchenMap {
|
||||||
|
kitchen = append(kitchen, fmt.Sprintf("%s: %d", item, count))
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := &FinalizedSummary{Pickup: pickup, Kitchen: kitchen}
|
||||||
|
app.LastSummary = summary
|
||||||
|
|
||||||
|
// Log
|
||||||
|
log.Println("=== Pickup view ===")
|
||||||
|
for _, line := range pickup {
|
||||||
|
log.Println(line)
|
||||||
|
}
|
||||||
|
log.Println("=== Kitchen view ===")
|
||||||
|
for _, line := range kitchen {
|
||||||
|
log.Println(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) handleGetLastSummary(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if app.LastSummary == nil {
|
||||||
|
writeJSON(w, map[string]string{"status": "none"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, app.LastSummary)
|
||||||
}
|
}
|
||||||
|
|
||||||
func server(app *App) *http.Server {
|
func server(app *App) *http.Server {
|
||||||
@@ -942,6 +996,7 @@ func server(app *App) *http.Server {
|
|||||||
r.Get("/finalize/time", app.handleGetFinalizeTime)
|
r.Get("/finalize/time", app.handleGetFinalizeTime)
|
||||||
r.Post("/finalize/time", app.handleSetFinalizeTime)
|
r.Post("/finalize/time", app.handleSetFinalizeTime)
|
||||||
r.Post("/finalize/now", app.handleFinalizeNow)
|
r.Post("/finalize/now", app.handleFinalizeNow)
|
||||||
|
r.Get("/finalize/last", app.handleGetLastSummary)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Moderators (role <= 50)
|
// Moderators (role <= 50)
|
||||||
@@ -952,6 +1007,7 @@ func server(app *App) *http.Server {
|
|||||||
r.Get("/finalize/time", app.handleGetFinalizeTime)
|
r.Get("/finalize/time", app.handleGetFinalizeTime)
|
||||||
r.Post("/finalize/time", app.handleSetFinalizeTime)
|
r.Post("/finalize/time", app.handleSetFinalizeTime)
|
||||||
r.Post("/finalize/now", app.handleFinalizeNow)
|
r.Post("/finalize/now", app.handleFinalizeNow)
|
||||||
|
r.Get("/finalize/last", app.handleGetLastSummary)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Return configured server
|
// Return configured server
|
||||||
@@ -973,12 +1029,11 @@ func main() {
|
|||||||
|
|
||||||
// Create server
|
// Create server
|
||||||
srv := server(app)
|
srv := server(app)
|
||||||
|
go startDailyCleanup(app)
|
||||||
|
|
||||||
log.Println("Server listening on", srv.Addr)
|
log.Println("Server listening on", srv.Addr)
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go startDailyCleanup(app)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
feedmee/public/icons/icon-192.png
Normal file
BIN
feedmee/public/icons/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
feedmee/public/icons/icon-512.png
Normal file
BIN
feedmee/public/icons/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
feedmee/public/icons/logo.png
Normal file
BIN
feedmee/public/icons/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
20
feedmee/public/manifest.json
Normal file
20
feedmee/public/manifest.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "FeedMe",
|
||||||
|
"short_name": "FeedMe",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#000000",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -29,6 +29,9 @@ type Order = {
|
|||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FinalizedSummary = { pickup: string[]; kitchen: string[] };
|
||||||
|
|
||||||
|
|
||||||
type MenuItem = {
|
type MenuItem = {
|
||||||
id: number;
|
id: number;
|
||||||
category: string;
|
category: string;
|
||||||
@@ -49,6 +52,8 @@ export default function AdminPage() {
|
|||||||
const [editingItem, setEditingItem] = useState<MenuItem | null>(null);
|
const [editingItem, setEditingItem] = useState<MenuItem | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [finalizeTime, setFinalizeTime] = useState("10:30");
|
const [finalizeTime, setFinalizeTime] = useState("10:30");
|
||||||
|
const [lastSummary, setLastSummary] = useState<FinalizedSummary | null>(null);
|
||||||
|
|
||||||
const auth = (): Record<string, string> => {
|
const auth = (): Record<string, string> => {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
@@ -97,6 +102,10 @@ export default function AdminPage() {
|
|||||||
.catch(() => setMenuItems([]));
|
.catch(() => setMenuItems([]));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadLastSummary();
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function refreshOrders() {
|
async function refreshOrders() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/admin/orders`, { headers: auth() });
|
const res = await fetch(`${API_URL}/api/admin/orders`, { headers: auth() });
|
||||||
@@ -232,11 +241,19 @@ export default function AdminPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadLastSummary() {
|
||||||
|
const res = await fetch(`${API_URL}/api/mod/finalize/last`, { headers: auth() });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.status !== "none") setLastSummary(data);
|
||||||
|
else setLastSummary(null);
|
||||||
|
}
|
||||||
|
|
||||||
async function triggerFinalizeNow() {
|
async function triggerFinalizeNow() {
|
||||||
await fetch(`${API_URL}/api/admin/finalize/now`, {
|
await fetch(`${API_URL}/api/admin/finalize/now`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: auth(),
|
headers: auth(),
|
||||||
});
|
});
|
||||||
|
await loadLastSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -537,7 +554,36 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Finalized Order Summary Panel */}
|
||||||
|
<h2 className="text-white text-2xl sm:text-3xl font-semibold text-center mt-8">
|
||||||
|
Legutóbbi összesítés
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="glass-panel w-full max-w-4xl p-4 space-y-4">
|
||||||
|
{lastSummary ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-2">Átvételi nézet</h3>
|
||||||
|
<ul className="list-disc list-inside text-white space-y-1">
|
||||||
|
{lastSummary.pickup.map((line, idx) => (
|
||||||
|
<li key={idx}>{line}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-2">Konyha nézet</h3>
|
||||||
|
<ul className="list-disc list-inside text-white space-y-1">
|
||||||
|
{lastSummary.kitchen.map((line, idx) => (
|
||||||
|
<li key={idx}>{line}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-white italic">Nincs elérhető összesítés.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export default function LandingPage() {
|
|||||||
{/* Global orders */}
|
{/* Global orders */}
|
||||||
<hr className="w-full border-t border-white/30 my-6" />
|
<hr className="w-full border-t border-white/30 my-6" />
|
||||||
<h2 className="text-white text-2xl sm:text-3xl font-semibold text-center mt-4">
|
<h2 className="text-white text-2xl sm:text-3xl font-semibold text-center mt-4">
|
||||||
Minden rendelés
|
Aktív rendelések
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<Card className="glass-panel w-full max-w-3xl pt-2 pb-2">
|
<Card className="glass-panel w-full max-w-3xl pt-2 pb-2">
|
||||||
@@ -176,7 +176,7 @@ export default function LandingPage() {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|
||||||
<p className="text-center text-white/70">Még senki sem rendelt :(</p>
|
<p className="text-center text-white/70">Nincs aktív rendelés</p>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const geistMono = Geist_Mono({
|
|||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "FeedMe",
|
title: "FeedMe",
|
||||||
description: "Food ordering app",
|
description: "Food ordering app",
|
||||||
|
manifest: "/manifest.json",
|
||||||
|
icons: {
|
||||||
|
apple: "/icons/icon-192.png",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ type MenuItem = {
|
|||||||
name: string
|
name: string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FinalizedSummary = { pickup: string[]; kitchen: string[] };
|
||||||
|
|
||||||
|
|
||||||
export default function ModPage() {
|
export default function ModPage() {
|
||||||
const [menuItems, setMenuItems] = useState<MenuItem[]>([]);
|
const [menuItems, setMenuItems] = useState<MenuItem[]>([]);
|
||||||
const [newItem, setNewItem] = useState("");
|
const [newItem, setNewItem] = useState("");
|
||||||
@@ -22,6 +25,8 @@ export default function ModPage() {
|
|||||||
const [editingItem, setEditingItem] = useState<MenuItem | null>(null);
|
const [editingItem, setEditingItem] = useState<MenuItem | null>(null);
|
||||||
const [finalizeTime, setFinalizeTime] = useState("10:30");
|
const [finalizeTime, setFinalizeTime] = useState("10:30");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [lastSummary, setLastSummary] = useState<FinalizedSummary | null>(null);
|
||||||
|
|
||||||
|
|
||||||
const auth = (): Record<string, string> => {
|
const auth = (): Record<string, string> => {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
@@ -61,6 +66,10 @@ export default function ModPage() {
|
|||||||
.then((data) => setFinalizeTime(data.time));
|
.then((data) => setFinalizeTime(data.time));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadLastSummary();
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function refreshMenu() {
|
async function refreshMenu() {
|
||||||
const res = await fetch(`${API_URL}/api/mod/menu`, { headers: auth() });
|
const res = await fetch(`${API_URL}/api/mod/menu`, { headers: auth() });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -110,8 +119,17 @@ export default function ModPage() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: auth(),
|
headers: auth(),
|
||||||
});
|
});
|
||||||
|
await loadLastSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadLastSummary() {
|
||||||
|
const res = await fetch(`${API_URL}/api/mod/finalize/last`, { headers: auth() });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.status !== "none") setLastSummary(data);
|
||||||
|
else setLastSummary(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="relative flex flex-col min-h-screen items-center p-4 pb-16 space-y-6">
|
<main className="relative flex flex-col min-h-screen items-center p-4 pb-16 space-y-6">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
@@ -238,6 +256,39 @@ export default function ModPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Finalized Order Summary Panel */}
|
||||||
|
<h2 className="text-white text-2xl sm:text-3xl font-semibold text-center mt-8">
|
||||||
|
Legutóbbi összesítés
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="glass-panel w-full max-w-4xl p-4 space-y-4">
|
||||||
|
{lastSummary ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-2">Pickup nézet</h3>
|
||||||
|
<ul className="list-disc list-inside text-white space-y-1">
|
||||||
|
{lastSummary.pickup.map((line, idx) => (
|
||||||
|
<li key={idx}>{line}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-2">Konyha nézet</h3>
|
||||||
|
<ul className="list-disc list-inside text-white space-y-1">
|
||||||
|
{lastSummary.kitchen.map((line, idx) => (
|
||||||
|
<li key={idx}>{line}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-white italic">Nincs elérhető összesítés.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user