First version
This commit is contained in:
10
frontend/app/api/meals/route.ts
Normal file
10
frontend/app/api/meals/route.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export async function GET() {
|
||||
const res = await fetch('http://backend:8000/api/meals', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
return Response.json(data);
|
||||
}
|
42
frontend/app/api/orders/route.ts
Normal file
42
frontend/app/api/orders/route.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const query = searchParams.get('waiter')
|
||||
const res = await fetch(!!query ? `http://backend:8000/api/orders/?waiter=${query}` : 'http://backend:8000/api/orders/', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
return Response.json(data);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const res = await fetch('http://backend:8000/api/orders/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json({ status: res.status });
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const query = searchParams.get('id')
|
||||
const body = await request.json()
|
||||
const res = await fetch(`http://backend:8000/api/orders/${query}/`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json({ status: res.status });
|
||||
}
|
16
frontend/app/api/tools.ts
Normal file
16
frontend/app/api/tools.ts
Normal file
@ -0,0 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import useSWR from 'swr'
|
||||
|
||||
export const fetcher = (...args) => fetch(...args).then(res => res.json()) // @ts-ignore
|
||||
|
||||
|
||||
export function useUser (id: number) {
|
||||
const { data, error, isLoading } = useSWR(`/api/waiters/${id}`, fetcher)
|
||||
|
||||
return {
|
||||
user: data,
|
||||
isLoading,
|
||||
isError: error
|
||||
}
|
||||
}
|
10
frontend/app/api/waiters/route.ts
Normal file
10
frontend/app/api/waiters/route.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export async function GET() {
|
||||
const res = await fetch('http://backend:8000/api/waiters', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
return Response.json(data);
|
||||
}
|
BIN
frontend/app/favicon.ico
Normal file
BIN
frontend/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
48
frontend/app/globals.css
Normal file
48
frontend/app/globals.css
Normal file
@ -0,0 +1,48 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
form#form p {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
|
||||
form#form label {
|
||||
@apply text-gray-700 font-bold text-sm;
|
||||
}
|
||||
|
||||
form#form input:not([type='checkbox']),
|
||||
textarea {
|
||||
@apply p-1 ps-2 transition ease-in-out focus:outline-none border-b-2 border-b-gray-400 mb-2 focus:bg-gray-100 focus:border-gray-600 appearance-none;
|
||||
}
|
||||
|
||||
form#form select {
|
||||
@apply p-2 ps-2 rounded-md bg-gray-100;
|
||||
}
|
||||
|
||||
select.form {
|
||||
@apply p-2 ps-2 rounded-md bg-gray-200;
|
||||
}
|
||||
|
||||
.dropdown:focus-within .dropdown-menu {
|
||||
/* @apply block; */
|
||||
display: block;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
button {
|
||||
@apply std-btn
|
||||
}
|
||||
|
||||
.prose-with-table {
|
||||
@apply prose-table:max-w-screen-2xl prose-table:text-center max-w-screen-xl w-full prose-td:py-1 prose-td:px-2 prose-th:py-1 prose-th:px-2 prose-td:align-middle prose-th:align-middle prose-thead:mx-auto;
|
||||
}
|
||||
|
||||
.std-btn {
|
||||
@apply inline-flex text-center justify-center items-center no-underline px-3 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-700 focus:outline-none focus:border-blue-300 active:bg-gray-50 active:text-gray-800;
|
||||
}
|
||||
|
||||
.std-menu-item {
|
||||
@apply text-gray-700 flex justify-between w-full px-4 py-2 text-sm leading-5 text-left
|
||||
}
|
||||
}
|
106
frontend/app/kitchen/page.tsx
Normal file
106
frontend/app/kitchen/page.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import { fetcher } from '@/app/api/tools';
|
||||
import useSWR from 'swr';
|
||||
import Link from 'next/link';
|
||||
|
||||
type Order = {
|
||||
id: number;
|
||||
created_on: string;
|
||||
updated_on: string;
|
||||
waiter: number;
|
||||
waiter_name: string;
|
||||
data: [{ item: string; additional_info: string; finished: boolean }];
|
||||
status: string;
|
||||
status_name: string;
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const { data, mutate, error, isLoading } = useSWR(`/api/orders`, fetcher, {
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
|
||||
const update_finished = (o: number, i: number, v: boolean) => {
|
||||
let d = data;
|
||||
d[o].data[i].finished = v;
|
||||
console.log(v);
|
||||
fetch(`/api/orders?id=${d[o].id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(d[o]),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(() => mutate(d));
|
||||
};
|
||||
|
||||
const update_status = (o: number, v: number) => {
|
||||
let d = data;
|
||||
d[o].status = v;
|
||||
console.log(v);
|
||||
fetch(`/api/orders?id=${d[o].id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(d[o]),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(() => mutate(d));
|
||||
};
|
||||
|
||||
if (error) return <div>Błąd przy ładowaniu danych</div>;
|
||||
if (isLoading) return <div>Ładowanie</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-4xl">Zamówienia:</p>
|
||||
<ul className="max-w-screen-md w-full">
|
||||
{data?.map((order: Order, oi: number) => (
|
||||
<li key={order.id} className="m-2 p-2 border-2 bg-gray-100">
|
||||
<h1 className="text-xl">
|
||||
<span className="font-mono">
|
||||
{'['}
|
||||
{String(order.id % 1000).padStart(3, '0')}
|
||||
{']'}
|
||||
</span>{' '}
|
||||
- {new Date(order.updated_on).toLocaleTimeString('pl-PL')}
|
||||
</h1>
|
||||
<h2 className="text-xl">{order.waiter_name}</h2>
|
||||
<h3>
|
||||
<select
|
||||
value={order.status}
|
||||
onChange={(e) =>
|
||||
update_status(oi, e.target.value as unknown as number)
|
||||
}
|
||||
className='form'
|
||||
>
|
||||
<option value="1">Zamówienie złożone</option>
|
||||
<option value="2">Zamówienie w trakcie przygotowywania</option>
|
||||
<option value="3">Zamówienie gotowe</option>
|
||||
<option value="4">Zamówienie skończone</option>
|
||||
</select>
|
||||
</h3>
|
||||
<div className="">
|
||||
<ul>
|
||||
{order.data.map((dish, i) => (
|
||||
<li
|
||||
key={dish.item}
|
||||
className="p-2 m-2 flex gap-4 bg-gray-200 border-2 border-gray-400"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={dish.finished}
|
||||
onChange={(e) => update_finished(oi, i, e.target.checked)}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-lg">{dish.item}</p>
|
||||
<div>{dish.additional_info}</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
23
frontend/app/layout.tsx
Normal file
23
frontend/app/layout.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="pl">
|
||||
<body
|
||||
className={`antialiased p-4 flex flex-col gap-2 items-center`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
18
frontend/app/page.tsx
Normal file
18
frontend/app/page.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<p className='text-xl'>Witamy</p>
|
||||
<div className="flex gap-4">
|
||||
<Link href={'/kitchen/'}>
|
||||
<button>Kuchnia</button>
|
||||
</Link>
|
||||
<Link href={'/waiter/'}>
|
||||
<button>Kelner</button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
147
frontend/app/waiter/[id]/new/page.tsx
Normal file
147
frontend/app/waiter/[id]/new/page.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { useForm, useFieldArray, useWatch, Control } from 'react-hook-form';
|
||||
import { fetcher } from '@/app/api/tools';
|
||||
import useSWR from 'swr';
|
||||
|
||||
type FormValues = {
|
||||
data: {
|
||||
item: string;
|
||||
additional_info: string;
|
||||
finished: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const { data, error, isLoading } = useSWR('/api/meals', fetcher);
|
||||
|
||||
const { id } = useParams();
|
||||
const router = useRouter();
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
data: [{ item: '', additional_info: '', finished: false }],
|
||||
},
|
||||
mode: 'onBlur',
|
||||
});
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'data',
|
||||
control,
|
||||
});
|
||||
const onSubmit = (data: FormValues) => {
|
||||
console.log(data);
|
||||
fetch('/api/orders', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ waiter: id, ...data }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(({ status }) => console.log(status))
|
||||
.then((_) => router.push(`/waiter/${id}`));
|
||||
};
|
||||
|
||||
if (error) return <div>Błąd przy ładowaniu danych</div>;
|
||||
if (isLoading) return <div>Ładowanie</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
id="form"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex flex-col max-w-screen-md w-full my-4"
|
||||
>
|
||||
<p className="text-2xl text-center">Nowe zamówienie</p>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<div
|
||||
key={field.id}
|
||||
className="border-2 m-2 p-2 gap-4 flex flex-col items-stretch justify-around content-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="hidden"
|
||||
{...register(`data.${index}.finished` as const, {
|
||||
required: false,
|
||||
})}
|
||||
/>
|
||||
<p className="basis-1/3">
|
||||
<label htmlFor={`data.${index}.item`}>Danie</label>
|
||||
<select
|
||||
{...register(`data.${index}.item` as const, {
|
||||
required: true,
|
||||
})}
|
||||
className={errors?.data?.[index]?.item ? 'error' : ''}
|
||||
>
|
||||
<option value="Inne">Inne</option>
|
||||
{data?.map((data: { id: number; name: string }) => (
|
||||
<option key={data.name} value={data.name}>
|
||||
{data.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</p>
|
||||
<p className="basis-1/3">
|
||||
<label htmlFor={`data.${index}.additional_info`}>
|
||||
Dodatkowe informacje
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Dodatkowe informacje"
|
||||
{...register(`data.${index}.additional_info` as const, {
|
||||
required: false,
|
||||
})}
|
||||
className={
|
||||
errors?.data?.[index]?.additional_info ? 'error' : ''
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
{fields.length > 1 ? (
|
||||
<p className="basis-1/3 flex items-center justify-center">
|
||||
<button type="button" onClick={() => remove(index)}>
|
||||
Usuń
|
||||
</button>
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mx-auto"
|
||||
onClick={() =>
|
||||
append({
|
||||
item: '',
|
||||
additional_info: '',
|
||||
finished: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
Dodaj kolejne danie
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className={
|
||||
'mt-10 mx-auto p-2 border-2 text-black hover:text-black ' +
|
||||
(!!errors.data
|
||||
? 'bg-red-300 border-red-500'
|
||||
: 'bg-green-300 border-green-500')
|
||||
}
|
||||
disabled={!!errors.data}
|
||||
>
|
||||
Złóż zamówienie
|
||||
</button>
|
||||
</form>
|
||||
<Link href={`/waiter/${id}`} className='mt-10'><button>Powrót</button></Link>
|
||||
</>
|
||||
);
|
||||
}
|
76
frontend/app/waiter/[id]/page.tsx
Normal file
76
frontend/app/waiter/[id]/page.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import { fetcher } from '@/app/api/tools';
|
||||
import useSWR from 'swr';
|
||||
import { useParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Home() {
|
||||
const { id } = useParams();
|
||||
const { data, error, isLoading } = useSWR(
|
||||
`/api/orders?waiter=${id}`,
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: 1000,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) return <div>Błąd przy ładowaniu danych</div>;
|
||||
if (isLoading) return <div>Ładowanie</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-lg">
|
||||
<Link href={`/waiter/${id}/new`}>
|
||||
<button>Nowe zamówienie</button>
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-4xl">Zamówienia:</p>
|
||||
<ul className="max-w-screen-md w-full">
|
||||
{data?.map(
|
||||
(order: {
|
||||
id: number;
|
||||
created_on: string;
|
||||
updated_on: string;
|
||||
waiter: number;
|
||||
waiter_name: string;
|
||||
data: [
|
||||
{ item: string; additional_info: string; finished: boolean }
|
||||
];
|
||||
status: string;
|
||||
status_name: string;
|
||||
}) => (
|
||||
<li key={order.id} className="m-2 p-2 border-2 bg-gray-100">
|
||||
<h1 className="text-xl">
|
||||
<span className="font-mono">
|
||||
{'['}
|
||||
{String(order.id % 1000).padStart(3, '0')}
|
||||
{']'}
|
||||
</span>{' '}
|
||||
- {new Date(order.updated_on).toLocaleTimeString('pl-PL')}
|
||||
</h1>
|
||||
<h2 className="text-xl">{order.waiter_name}</h2>
|
||||
<h3>{order.status_name}</h3>
|
||||
<div className="">
|
||||
<ul>
|
||||
{order.data.map((dish) => (
|
||||
<li
|
||||
key={dish.item}
|
||||
className="p-2 m-2 flex gap-4 bg-gray-200 border-2 border-gray-400"
|
||||
>
|
||||
<input type="checkbox" checked={dish.finished} readOnly />
|
||||
<div>
|
||||
<p className="text-lg">{dish.item}</p>
|
||||
<div>{dish.additional_info}</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
27
frontend/app/waiter/page.tsx
Normal file
27
frontend/app/waiter/page.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { fetcher } from '@/app/api/tools';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export default function Home() {
|
||||
const { data, error, isLoading } = useSWR('/api/waiters', fetcher);
|
||||
|
||||
if (error) return <div>Błąd przy ładowaniu danych</div>;
|
||||
if (isLoading) return <div>Ładowanie</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-xl">Kelnerzy:</p>
|
||||
<ul className="text-lg flex flex-col items-center gap-2">
|
||||
{data?.map((data: { id: number; name: string }) => (
|
||||
<li key={data.id}>
|
||||
<Link href={`/waiter/${data.id}`}>
|
||||
<button>{data.name}</button>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user