color changes, client, realization time

This commit is contained in:
2025-01-04 14:57:53 +01:00
parent 948dbbca61
commit c3f306575f
15 changed files with 247 additions and 144 deletions

View File

@ -0,0 +1,36 @@
# Generated by Django 5.1.4 on 2024-12-21 13:34
import app.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0002_meal'),
]
operations = [
migrations.AlterModelOptions(
name='meal',
options={'verbose_name': 'Danie', 'verbose_name_plural': 'Dania'},
),
migrations.AlterModelOptions(
name='order',
options={'verbose_name': 'Zamówienie', 'verbose_name_plural': 'Zamówienia'},
),
migrations.AlterModelOptions(
name='waiter',
options={'verbose_name': 'Kelner', 'verbose_name_plural': 'Kelnerzy'},
),
migrations.AddField(
model_name='order',
name='client',
field=models.CharField(blank=True, max_length=250, null=True, verbose_name='Klient'),
),
migrations.AddField(
model_name='order',
name='realization_time',
field=models.DateTimeField(default=app.models.default_realization_time, verbose_name='Godzina realizacji'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.1.4 on 2024-12-21 14:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0003_alter_meal_options_alter_order_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='order',
name='realization_time',
field=models.CharField(blank=True, max_length=250, null=True, verbose_name='Godzina realizacji'),
),
migrations.AlterField(
model_name='order',
name='status',
field=models.PositiveSmallIntegerField(choices=[(1, 'Zamówienie złożone'), (2, 'Zamówienie w trakcie przygotowywania'), (3, 'Zamówienie gotowe'), (4, 'Zamówienie zrealizowane')], default=1, verbose_name='Status zamówienia'),
),
]

View File

@ -1,9 +1,14 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone
# Create your models here. # Create your models here.
def default_realization_time():
return timezone.now() # + datetime.timedelta(hours=1)
class Waiter(models.Model): class Waiter(models.Model):
name = models.CharField(_('Imię i nazwisko'), max_length=120) name = models.CharField(_('Imię i nazwisko'), max_length=120)
@ -11,8 +16,8 @@ class Waiter(models.Model):
return self.name return self.name
class Meta: class Meta:
verbose_name = _("Kelner") verbose_name = _('Kelner')
verbose_name_plural = _("Kelnerzy") verbose_name_plural = _('Kelnerzy')
class Meal(models.Model): class Meal(models.Model):
@ -22,23 +27,31 @@ class Meal(models.Model):
return self.name return self.name
class Meta: class Meta:
verbose_name = _("Danie") verbose_name = _('Danie')
verbose_name_plural = _("Dania") verbose_name_plural = _('Dania')
class Order(models.Model): class Order(models.Model):
created_on = models.DateTimeField(_('Utworzono'), auto_now_add=True) created_on = models.DateTimeField(_('Utworzono'), auto_now_add=True)
updated_on = models.DateTimeField(_('Zaktualizowano'), auto_now=True) updated_on = models.DateTimeField(_('Zaktualizowano'), auto_now=True)
waiter = models.ForeignKey( realization_time = models.CharField(
Waiter, models.SET_NULL, related_name='orders', verbose_name=_('Kelner'), null=True _('Godzina realizacji'), max_length=250, null=True, blank=True
) )
waiter = models.ForeignKey(
Waiter,
models.SET_NULL,
related_name='orders',
verbose_name=_('Kelner'),
null=True,
)
client = models.CharField(_('Klient'), max_length=250, null=True, blank=True)
data = models.JSONField(_('Dane zamówienia'), default=dict) data = models.JSONField(_('Dane zamówienia'), default=dict)
class StatusChoices(models.IntegerChoices): class StatusChoices(models.IntegerChoices):
ORDERED = (1, _('Zamówienie złożone')) ORDERED = (1, _('Zamówienie złożone'))
IN_PROGRESS = (2, ('Zamówienie w trakcie przygotowywania')) IN_PROGRESS = (2, ('Zamówienie w trakcie przygotowywania'))
READY = (3, _('Zamówienie gotowe')) READY = (3, _('Zamówienie gotowe'))
FINALIZED = (4, _('Zamówienie skończone')) FINALIZED = (4, _('Zamówienie zrealizowane'))
status = models.PositiveSmallIntegerField( status = models.PositiveSmallIntegerField(
_('Status zamówienia'), _('Status zamówienia'),
@ -47,8 +60,8 @@ class Order(models.Model):
) )
def __str__(self): def __str__(self):
return self.id return str(self.id)
class Meta: class Meta:
verbose_name = _("Zamówienie") verbose_name = _('Zamówienie')
verbose_name_plural = _("Zamówienia") verbose_name_plural = _('Zamówienia')

View File

@ -67,6 +67,8 @@ class OrderSerializer(serializers.ModelSerializer):
'id', 'id',
'created_on', 'created_on',
'updated_on', 'updated_on',
'realization_time',
'client',
'waiter', 'waiter',
'waiter_name', 'waiter_name',
'data', 'data',

View File

@ -4,7 +4,7 @@ FROM node:alpine AS base
FROM base AS deps FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /src
# Install dependencies based on the preferred package manager # Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
@ -18,14 +18,15 @@ RUN \
# Rebuild the source code only when needed # Rebuild the source code only when needed
FROM base AS builder FROM base AS builder
WORKDIR /app WORKDIR /src
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /src/node_modules ./node_modules
COPY . . COPY . .
# Next.js collects completely anonymous telemetry data about general usage. # Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry # Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build. # Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED=1
ENV NEXT_PRIVATE_STANDALONE=true
RUN yarn build RUN yarn build
@ -34,16 +35,16 @@ RUN yarn build
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM base AS runner FROM base AS runner
WORKDIR /app WORKDIR /src
ENV NODE_ENV production ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime. # Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public COPY --from=builder /src/public ./public
# Set the correct permission for prerender cache # Set the correct permission for prerender cache
RUN mkdir .next RUN mkdir .next
@ -51,15 +52,13 @@ RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size # Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing # https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /src/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /src/.next/static ./.next/static
USER nextjs USER nextjs
EXPOSE 3000 EXPOSE 3000
ENV PORT 3000 ENV PORT=3000
# set hostname to localhost # set hostname to localhost
ENV HOSTNAME "0.0.0.0" CMD HOSTNAME="0.0.0.0" node server.js
CMD ["node", "server.js"]

View File

@ -2,7 +2,8 @@
import useSWR from 'swr' import useSWR from 'swr'
export const fetcher = (...args) => fetch(...args).then(res => res.json()) // @ts-ignore // @ts-expect-error ignore ...args type
export const fetcher = (...args) => fetch(...args).then(res => res.json())
export function useUser (id: number) { export function useUser (id: number) {

View File

@ -0,0 +1,15 @@
import { Order } from './tools';
export function OrderID({ order }: { order: Order }) {
return (
<h1 className="text-xl">
<span className="font-mono">
{'['}
{String(order.id % 1000).padStart(3, '0')}
{']'}
</span>{' '}
- Godzina realizacji -{' '}
<span className="font-mono">{order.realization_time}</span>
</h1>
);
}

View File

@ -3,7 +3,7 @@
@tailwind utilities; @tailwind utilities;
form#form p { form#form p, form#form div {
@apply flex flex-col; @apply flex flex-col;
} }

View File

@ -2,19 +2,9 @@
import { fetcher } from '@/app/api/tools'; import { fetcher } from '@/app/api/tools';
import useSWR from 'swr'; import useSWR from 'swr';
import Link from 'next/link';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { getDishBg, getOrderBg, Order } from '../tools';
type Order = { import { OrderID } from '../components';
id: number;
created_on: string;
updated_on: string;
waiter: number;
waiter_name: string;
data: [{ item: string; additional_info: string; finished: boolean }];
status: number;
status_name: string;
};
export default function Home() { export default function Home() {
const audioRef = useRef({} as HTMLAudioElement); const audioRef = useRef({} as HTMLAudioElement);
@ -25,11 +15,7 @@ export default function Home() {
}); });
const play = () => { const play = () => {
if (audioRef.current) { if (audioRef.current) audioRef.current.play();
audioRef.current.play();
} else {
// Throw error
}
}; };
useEffect(() => { useEffect(() => {
@ -48,12 +34,11 @@ export default function Home() {
play(); play();
setPlayedIDs(playedIDs.concat(justPlayed)); setPlayedIDs(playedIDs.concat(justPlayed));
} }
}, [data]); }, [playedIDs, data]);
const update_finished = (o: number, i: number, v: boolean) => { const update_finished = (o: number, i: number, v: boolean) => {
let d = data; const d = data;
d[o].data[i].finished = v; d[o].data[i].finished = v;
console.log(v);
fetch(`/api/orders?id=${d[o].id}`, { fetch(`/api/orders?id=${d[o].id}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify(d[o]), body: JSON.stringify(d[o]),
@ -64,9 +49,8 @@ export default function Home() {
}; };
const update_status = (o: number, v: number) => { const update_status = (o: number, v: number) => {
let d = data; const d = data;
d[o].status = v; d[o].status = v;
console.log(v);
fetch(`/api/orders?id=${d[o].id}`, { fetch(`/api/orders?id=${d[o].id}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify(d[o]), body: JSON.stringify(d[o]),
@ -87,36 +71,23 @@ export default function Home() {
{data?.map((order: Order, oi: number) => ( {data?.map((order: Order, oi: number) => (
<li <li
key={order.id} key={order.id}
className={`m-2 p-2 rounded-md border-2 ${ className={`m-2 p-2 rounded-md border-2 ${getOrderBg(order)}`}
{
1: 'bg-red-200 border-red-400',
2: 'bg-purple-200 border-purple-400',
3: 'bg-green-200 border-green-400',
4: 'bg-gray-100 border-gray-400',
}[order.status]
}`}
> >
<h1 className="text-xl"> <OrderID order={order} />
<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> <h2 className="text-xl">{order.waiter_name}</h2>
{ (order.client) ? <h2 className="text-xl">Klient: {order.client}</h2> : <></>}
<h3> <h3>
<select <select
value={order.status} value={order.status}
onChange={(e) => onChange={(e) =>
update_status(oi, e.target.value as unknown as number) update_status(oi, e.target.value as unknown as number)
} }
className="form border-2 border-gray-500" className="form border-2 my-2 border-gray-500"
> >
<option value="1">Zamówienie złożone</option> <option value="1">Zamówienie złożone</option>
<option value="2">Zamówienie w trakcie przygotowywania</option> <option value="2">Zamówienie w trakcie przygotowywania</option>
<option value="3">Zamówienie gotowe</option> <option value="3">Zamówienie gotowe</option>
<option value="4">Zamówienie skończone</option> <option value="4">Zamówienie zrealizowane</option>
</select> </select>
</h3> </h3>
<div className=""> <div className="">
@ -124,7 +95,7 @@ export default function Home() {
{order.data.map((dish, i) => ( {order.data.map((dish, i) => (
<li <li
key={dish.item} key={dish.item}
className={`p-2 m-2 flex gap-4 border-2 bg-white border-gray-400`} className={`p-2 m-2 flex gap-4 border-2 ${getDishBg(dish)}`}
> >
<input <input
type="checkbox" type="checkbox"
@ -132,7 +103,7 @@ export default function Home() {
onChange={(e) => update_finished(oi, i, e.target.checked)} onChange={(e) => update_finished(oi, i, e.target.checked)}
/> />
<div> <div>
<p className="text-lg">{dish.item}</p> <p className="text-lg">{dish.item} - {dish.takeout ? 'Na wynos' : 'Na miejscu'}</p>
<div>{dish.additional_info}</div> <div>{dish.additional_info}</div>
</div> </div>
</li> </li>

View File

@ -1,9 +1,8 @@
import type { Metadata } from "next"; import type { Metadata } from 'next';
import "./globals.css"; import './globals.css';
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: 'Bistro',
description: "Generated by create next app",
}; };
export default function RootLayout({ export default function RootLayout({
@ -13,9 +12,7 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="pl"> <html lang="pl">
<body <body className="antialiased p-4 flex flex-col gap-2 items-center">
className={`antialiased p-4 flex flex-col gap-2 items-center`}
>
{children} {children}
</body> </body>
</html> </html>

View File

@ -1,4 +1,3 @@
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
export default function Home() { export default function Home() {

35
frontend/app/tools.ts Normal file
View File

@ -0,0 +1,35 @@
export type Dish = {
item: string;
additional_info: string;
finished: boolean;
takeout: boolean;
};
export type Order = {
id: number;
created_on: string;
updated_on: string;
realization_time: string;
client: string;
waiter: number;
waiter_name: string;
data: Dish[];
status: number;
status_name: string;
};
export function getOrderBg(order: Order): string {
const nonTakeouts = order.data.filter((dish) => !dish.takeout);
if (order.status == 4) return 'bg-red-200 border-red-400';
return nonTakeouts.length > 0
? 'bg-green-200 border-green-400'
: 'bg-blue-200 border-blue-400';
}
export function getDishBg(dish: Dish): string {
return dish.takeout
? 'bg-blue-300 border-blue-500'
: 'bg-green-300 border-green-500';
}

View File

@ -3,16 +3,15 @@
import Link from 'next/link'; import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import * as React from 'react'; import * as React from 'react';
import { useForm, useFieldArray, useWatch, Control } from 'react-hook-form'; import { useForm, useFieldArray } from 'react-hook-form';
import { fetcher } from '@/app/api/tools'; import { fetcher } from '@/app/api/tools';
import useSWR from 'swr'; import useSWR from 'swr';
import { Dish } from '@/app/tools';
type FormValues = { type FormValues = {
data: { client: string;
item: string; realization_time: string;
additional_info: string; data: Dish[];
finished: boolean;
}[];
}; };
export default function App() { export default function App() {
@ -27,9 +26,13 @@ export default function App() {
formState: { errors }, formState: { errors },
} = useForm<FormValues>({ } = useForm<FormValues>({
defaultValues: { defaultValues: {
data: [{ item: '', additional_info: '', finished: false }], client: '',
realization_time: new Date().toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }),
data: [
{ item: 'Inne', additional_info: '', finished: false, takeout: false },
],
}, },
mode: 'onBlur', mode: 'all',
}); });
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
name: 'data', name: 'data',
@ -45,7 +48,7 @@ export default function App() {
}, },
}) })
.then(({ status }) => console.log(status)) .then(({ status }) => console.log(status))
.then((_) => router.push(`/waiter/${id}`)); .then(() => router.push(`/waiter/${id}`));
}; };
if (error) return <div>Błąd przy ładowaniu danych</div>; if (error) return <div>Błąd przy ładowaniu danych</div>;
@ -59,6 +62,26 @@ export default function App() {
className="flex flex-col max-w-screen-md w-full my-4" className="flex flex-col max-w-screen-md w-full my-4"
> >
<p className="text-2xl text-center">Nowe zamówienie</p> <p className="text-2xl text-center">Nowe zamówienie</p>
<p>
<label htmlFor={`client`}>Klient</label>
<input
placeholder="Klient"
type="text"
{...register(`client` as const, {
required: false,
})}
className={errors?.client ? 'error' : ''}
/>
</p>
<p>
<label htmlFor={`realization_time`}>Godzina realizacji</label>
<input
{...register(`realization_time` as const, {
required: false,
})}
className={errors?.realization_time ? 'error' : ''}
/>
</p>
{fields.map((field, index) => { {fields.map((field, index) => {
return ( return (
<div <div
@ -72,7 +95,7 @@ export default function App() {
required: false, required: false,
})} })}
/> />
<p className="basis-1/3"> <div className="basis-1/3">
<label htmlFor={`data.${index}.item`}>Danie</label> <label htmlFor={`data.${index}.item`}>Danie</label>
<select <select
{...register(`data.${index}.item` as const, { {...register(`data.${index}.item` as const, {
@ -87,8 +110,18 @@ export default function App() {
</option> </option>
))} ))}
</select> </select>
</p> <section className="flex flex-row my-2 gap-2">
<p className="basis-1/3"> <label htmlFor={`data.${index}.takeout`}>Na wynos</label>
<input
{...register(`data.${index}.takeout` as const, {
required: false,
})}
type="checkbox"
className={errors?.data?.[index]?.takeout ? 'error' : ''}
/>
</section>
</div>
<div className="basis-1/3">
<label htmlFor={`data.${index}.additional_info`}> <label htmlFor={`data.${index}.additional_info`}>
Dodatkowe informacje Dodatkowe informacje
</label> </label>
@ -101,7 +134,7 @@ export default function App() {
errors?.data?.[index]?.additional_info ? 'error' : '' errors?.data?.[index]?.additional_info ? 'error' : ''
} }
/> />
</p> </div>
{fields.length > 1 ? ( {fields.length > 1 ? (
<p className="basis-1/3 flex items-center justify-center"> <p className="basis-1/3 flex items-center justify-center">
<button type="button" onClick={() => remove(index)}> <button type="button" onClick={() => remove(index)}>
@ -123,6 +156,7 @@ export default function App() {
item: '', item: '',
additional_info: '', additional_info: '',
finished: false, finished: false,
takeout: false,
}) })
} }
> >
@ -141,7 +175,9 @@ export default function App() {
Złóż zamówienie Złóż zamówienie
</button> </button>
</form> </form>
<Link href={`/waiter/${id}`} className='mt-10'><button>Powrót</button></Link> <Link href={`/waiter/${id}`} className="mt-10">
<button>Powrót</button>
</Link>
</> </>
); );
} }

View File

@ -4,6 +4,8 @@ import { fetcher } from '@/app/api/tools';
import useSWR from 'swr'; import useSWR from 'swr';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { getDishBg, getOrderBg, Order } from '@/app/tools';
import { OrderID } from '@/app/components';
export default function Home() { export default function Home() {
const { id } = useParams(); const { id } = useParams();
@ -27,59 +29,33 @@ export default function Home() {
</p> </p>
<p className="text-4xl">Zamówienia:</p> <p className="text-4xl">Zamówienia:</p>
<ul className="max-w-screen-md w-full"> <ul className="max-w-screen-md w-full">
{data?.map( {data?.map((order: Order) => (
(order: { <li
id: number; key={order.id}
created_on: string; className={`m-2 p-2 rounded-md border-2 ${getOrderBg(order)}`}
updated_on: string; >
waiter: number; <OrderID order={order} />
waiter_name: string; <h2 className="text-xl">Kelner: {order.waiter_name}</h2>
data: [ { (order.client) ? <h2 className="text-xl">Klient: {order.client}</h2> : <></>}
{ item: string; additional_info: string; finished: boolean } <h3>{order.status_name}</h3>
]; <div className="">
status: string; <ul>
status_name: string; {order.data.map((dish) => (
}) => ( <li
<li key={dish.item}
key={order.id} className={`p-2 m-2 flex gap-4 border-2 ${getDishBg(dish)}`}
className={`m-2 p-2 rounded-md border-2 ${ >
{ <input type="checkbox" checked={dish.finished} readOnly />
1: 'bg-red-200 border-red-400', <div>
2: 'bg-purple-200 border-purple-400', <p className="text-lg">{dish.item} - {dish.takeout ? 'Na wynos' : 'Na miejscu'}</p>
3: 'bg-green-200 border-green-400', <div>{dish.additional_info}</div>
4: 'bg-gray-100 border-gray-400', </div>
}[order.status] </li>
}`} ))}
> </ul>
<h1 className="text-xl"> </div>
<span className="font-mono"> </li>
{'['} ))}
{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-white 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> </ul>
</> </>
); );

View File

@ -1,7 +1,7 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ output: 'standalone'
}; };
export default nextConfig; export default nextConfig;