feat: zvýraznění dnů v historii obsahujících objednávky
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 20s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 36s
CI / Playwright E2E tests (push) Successful in 1m17s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 20s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 36s
CI / Playwright E2E tests (push) Successful in 1m17s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
|
"react-datepicker": "^9.1.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-jwt": "^1.3.0",
|
"react-jwt": "^1.3.0",
|
||||||
"react-modal": "^3.16.3",
|
"react-modal": "^3.16.3",
|
||||||
|
|||||||
+91
-3
@@ -284,9 +284,97 @@ body {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
input[type="date"] {
|
// react-datepicker obaluje input do wrapperu – necháme ho zabrat jen potřebnou šířku
|
||||||
text-align: center;
|
.react-datepicker-wrapper {
|
||||||
font-weight: 600;
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-date-input {
|
||||||
|
width: 160px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zvýraznění dnů, ve kterých existuje alespoň jedna objednávka – tečka pod číslem dne
|
||||||
|
.react-datepicker__day.luncher-order-day {
|
||||||
|
position: relative;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 2px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--luncher-primary, #0d6efd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// U vybraného dne (tmavé pozadí) je tečka světlá, aby byla vidět
|
||||||
|
&.react-datepicker__day--selected::after,
|
||||||
|
&.react-datepicker__day--keyboard-selected::after {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vybraný den používá akcentovou barvu aplikace (v obou režimech), místo výchozí modré
|
||||||
|
.react-datepicker__day--selected,
|
||||||
|
.react-datepicker__day--keyboard-selected {
|
||||||
|
background-color: var(--luncher-primary);
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--luncher-primary-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tmavý režim kalendáře (react-datepicker) – navázáno na CSS proměnné motivu
|
||||||
|
[data-bs-theme="dark"] {
|
||||||
|
.react-datepicker {
|
||||||
|
background-color: var(--luncher-bg-card);
|
||||||
|
border-color: var(--luncher-border);
|
||||||
|
color: var(--luncher-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker__header {
|
||||||
|
background-color: var(--luncher-bg-hover);
|
||||||
|
border-bottom-color: var(--luncher-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker__current-month,
|
||||||
|
.react-datepicker__day-name,
|
||||||
|
.react-datepicker__day,
|
||||||
|
.react-datepicker-year-header {
|
||||||
|
color: var(--luncher-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker__day:hover,
|
||||||
|
.react-datepicker__month-text:hover {
|
||||||
|
background-color: var(--luncher-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker__day--today {
|
||||||
|
color: var(--luncher-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker__day--disabled,
|
||||||
|
.react-datepicker__day--outside-month {
|
||||||
|
color: var(--luncher-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Šipky pro přepínání měsíců
|
||||||
|
.react-datepicker__navigation-icon::before {
|
||||||
|
border-color: var(--luncher-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Špička popoveru (SVG) míří do hlavičky – sladíme barvy.
|
||||||
|
// !important kvůli vyšší specificitě knihovního pravidla [data-placement].
|
||||||
|
.react-datepicker__triangle {
|
||||||
|
fill: var(--luncher-bg-hover) !important;
|
||||||
|
color: var(--luncher-bg-hover) !important;
|
||||||
|
stroke: var(--luncher-border) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import { Alert, Badge, Button, Card, Form, Modal, OverlayTrigger, Table, Tooltip
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
import { faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { faBasketShopping, faChevronLeft, faChevronRight, faCircleCheck, faClockRotateLeft, faGear, faLock, faLockOpen, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
import { faBasketShopping, faChevronLeft, faChevronRight, faCircleCheck, faClockRotateLeft, faGear, faLock, faLockOpen, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import DatePicker, { registerLocale } from 'react-datepicker';
|
||||||
|
import { cs } from 'date-fns/locale';
|
||||||
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import {
|
import {
|
||||||
ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember, PendingQr,
|
ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember, PendingQr,
|
||||||
getData, createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes,
|
getData, createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, getOrderDates,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
import { computeFeeShare, computeMemberTotal, countActiveMembers } from '../utils/groupFees';
|
import { computeFeeShare, computeMemberTotal, countActiveMembers } from '../utils/groupFees';
|
||||||
import { EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from '../context/socket';
|
import { EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from '../context/socket';
|
||||||
@@ -24,6 +27,9 @@ import PendingPayments from '../components/PendingPayments';
|
|||||||
const SLOT = MealSlot.EXTRA;
|
const SLOT = MealSlot.EXTRA;
|
||||||
const TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;
|
const TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;
|
||||||
|
|
||||||
|
// Český lokál pro date picker (názvy měsíců/dnů, pondělí jako první den)
|
||||||
|
registerLocale('cs', cs);
|
||||||
|
|
||||||
/** Vrátí ISO datum (YYYY-MM-DD) posunuté o předaný počet dní. */
|
/** Vrátí ISO datum (YYYY-MM-DD) posunuté o předaný počet dní. */
|
||||||
function shiftIsoDate(iso: string, days: number): string {
|
function shiftIsoDate(iso: string, days: number): string {
|
||||||
const date = new Date(`${iso}T00:00:00`);
|
const date = new Date(`${iso}T00:00:00`);
|
||||||
@@ -31,6 +37,11 @@ function shiftIsoDate(iso: string, days: number): string {
|
|||||||
return formatDate(date);
|
return formatDate(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Převede ISO datum (YYYY-MM-DD) na lokální Date (půlnoc), nebo null. */
|
||||||
|
function isoToDate(iso?: string): Date | null {
|
||||||
|
return iso ? new Date(`${iso}T00:00:00`) : null;
|
||||||
|
}
|
||||||
|
|
||||||
function stateBadge(state: GroupState) {
|
function stateBadge(state: GroupState) {
|
||||||
const map: Record<GroupState, { bg: string; label: string }> = {
|
const map: Record<GroupState, { bg: string; label: string }> = {
|
||||||
[GroupState.OPEN]: { bg: 'success', label: 'Otevřeno' },
|
[GroupState.OPEN]: { bg: 'success', label: 'Otevřeno' },
|
||||||
@@ -53,6 +64,8 @@ export default function OrderGroupsPage() {
|
|||||||
const [todayIso, setTodayIso] = useState<string | undefined>();
|
const [todayIso, setTodayIso] = useState<string | undefined>();
|
||||||
// Ref pro socket handler – aby věděl, zda se zobrazuje historie (na ní se živé aktualizace neaplikují)
|
// Ref pro socket handler – aby věděl, zda se zobrazuje historie (na ní se živé aktualizace neaplikují)
|
||||||
const selectedDateRef = useRef<string | undefined>(undefined);
|
const selectedDateRef = useRef<string | undefined>(undefined);
|
||||||
|
// ISO data dnů, ve kterých existuje aspoň jedna objednávka (pro zvýraznění v date pickeru)
|
||||||
|
const [orderDates, setOrderDates] = useState<string[]>([]);
|
||||||
const [newGroupName, setNewGroupName] = useState('');
|
const [newGroupName, setNewGroupName] = useState('');
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
const [adminModalOpen, setAdminModalOpen] = useState(false);
|
const [adminModalOpen, setAdminModalOpen] = useState(false);
|
||||||
@@ -78,6 +91,12 @@ export default function OrderGroupsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Načte dny s objednávkou pro zvýraznění v date pickeru
|
||||||
|
const fetchOrderDates = async () => {
|
||||||
|
const r = await getOrderDates();
|
||||||
|
if (r.data?.dates) setOrderDates(r.data.dates);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedDateRef.current = selectedDate;
|
selectedDateRef.current = selectedDate;
|
||||||
}, [selectedDate]);
|
}, [selectedDate]);
|
||||||
@@ -87,6 +106,11 @@ export default function OrderGroupsPage() {
|
|||||||
fetchData(selectedDate);
|
fetchData(selectedDate);
|
||||||
}, [auth?.login, selectedDate]);
|
}, [auth?.login, selectedDate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!auth?.login) return;
|
||||||
|
fetchOrderDates();
|
||||||
|
}, [auth?.login]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on(EVENT_MESSAGE, (newData: ClientData) => {
|
socket.on(EVENT_MESSAGE, (newData: ClientData) => {
|
||||||
// Živé aktualizace se týkají vždy dneška – při zobrazení historie je ignorujeme
|
// Živé aktualizace se týkají vždy dneška – při zobrazení historie je ignorujeme
|
||||||
@@ -139,6 +163,8 @@ export default function OrderGroupsPage() {
|
|||||||
socket.emit?.('message', result.data as ClientData);
|
socket.emit?.('message', result.data as ClientData);
|
||||||
}
|
}
|
||||||
await fetchData();
|
await fetchData();
|
||||||
|
// Sada dnů s objednávkou se mohla změnit (vytvoření/smazání skupiny)
|
||||||
|
fetchOrderDates();
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -265,6 +291,11 @@ export default function OrderGroupsPage() {
|
|||||||
setSelectedDate(todayIso != null && value >= todayIso ? undefined : value);
|
setSelectedDate(todayIso != null && value >= todayIso ? undefined : value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Dny s objednávkou jako Date objekty pro zvýraznění v kalendáři
|
||||||
|
const highlightedOrderDates = orderDates
|
||||||
|
.map(d => isoToDate(d))
|
||||||
|
.filter((d): d is Date => d != null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-container">
|
<div className="app-container">
|
||||||
<Header choices={data.choices} />
|
<Header choices={data.choices} />
|
||||||
@@ -282,13 +313,16 @@ export default function OrderGroupsPage() {
|
|||||||
<span title="Předchozí den">
|
<span title="Předchozí den">
|
||||||
<FontAwesomeIcon icon={faChevronLeft} onClick={() => goToDay(-1)} />
|
<FontAwesomeIcon icon={faChevronLeft} onClick={() => goToDay(-1)} />
|
||||||
</span>
|
</span>
|
||||||
<Form.Control
|
<DatePicker
|
||||||
type="date"
|
selected={isoToDate(displayedIso)}
|
||||||
value={displayedIso ?? ''}
|
onChange={(d: Date | null) => handleDatePick(d ? formatDate(d) : '')}
|
||||||
max={todayIso}
|
maxDate={isoToDate(todayIso) ?? undefined}
|
||||||
onChange={e => handleDatePick(e.target.value)}
|
highlightDates={[{ 'luncher-order-day': highlightedOrderDates }]}
|
||||||
className={isReadOnly ? 'text-muted' : ''}
|
locale="cs"
|
||||||
style={{ maxWidth: 200 }}
|
dateFormat="d. M. yyyy"
|
||||||
|
calendarStartDay={1}
|
||||||
|
popperPlacement="bottom"
|
||||||
|
className={`form-control text-center fw-semibold order-date-input ${isReadOnly ? 'text-muted' : ''}`}
|
||||||
/>
|
/>
|
||||||
<span title="Následující den">
|
<span title="Následující den">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
|||||||
@@ -428,6 +428,42 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b"
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b"
|
||||||
integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==
|
integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==
|
||||||
|
|
||||||
|
"@floating-ui/core@^1.7.5":
|
||||||
|
version "1.7.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.5.tgz#d4af157a03330af5a60e69da7a4692507ada0622"
|
||||||
|
integrity sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/utils" "^0.2.11"
|
||||||
|
|
||||||
|
"@floating-ui/dom@^1.7.6":
|
||||||
|
version "1.7.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.6.tgz#f915bba5abbb177e1f227cacee1b4d0634b187bf"
|
||||||
|
integrity sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/core" "^1.7.5"
|
||||||
|
"@floating-ui/utils" "^0.2.11"
|
||||||
|
|
||||||
|
"@floating-ui/react-dom@^2.1.8":
|
||||||
|
version "2.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.8.tgz#5fb5a20d10aafb9505f38c24f38d00c8e1598893"
|
||||||
|
integrity sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/dom" "^1.7.6"
|
||||||
|
|
||||||
|
"@floating-ui/react@^0.27.15":
|
||||||
|
version "0.27.19"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.19.tgz#d8d5d895b7cb97dac370bfbf55f3e630878fdf1f"
|
||||||
|
integrity sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/react-dom" "^2.1.8"
|
||||||
|
"@floating-ui/utils" "^0.2.11"
|
||||||
|
tabbable "^6.0.0"
|
||||||
|
|
||||||
|
"@floating-ui/utils@^0.2.11":
|
||||||
|
version "0.2.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.11.tgz#a269e055e40e2f45873bae9d1a2fdccbd314ea3f"
|
||||||
|
integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==
|
||||||
|
|
||||||
"@fortawesome/fontawesome-common-types@7.1.0":
|
"@fortawesome/fontawesome-common-types@7.1.0":
|
||||||
version "7.1.0"
|
version "7.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz#a4e0b7e40073d5fdef41182da1bc216a05875659"
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz#a4e0b7e40073d5fdef41182da1bc216a05875659"
|
||||||
@@ -1216,6 +1252,11 @@ d3-timer@^3.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
|
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
|
||||||
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
|
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
|
||||||
|
|
||||||
|
date-fns@^4.1.0:
|
||||||
|
version "4.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.4.0.tgz#806539edf45c616b2b76b5f78b88c56ed3c7e036"
|
||||||
|
integrity sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==
|
||||||
|
|
||||||
debug@^4.1.0, debug@^4.3.1, debug@~4.4.1:
|
debug@^4.1.0, debug@^4.3.1, debug@~4.4.1:
|
||||||
version "4.4.3"
|
version "4.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
||||||
@@ -1626,6 +1667,15 @@ react-bootstrap@^2.10.10:
|
|||||||
uncontrollable "^7.2.1"
|
uncontrollable "^7.2.1"
|
||||||
warning "^4.0.3"
|
warning "^4.0.3"
|
||||||
|
|
||||||
|
react-datepicker@^9.1.0:
|
||||||
|
version "9.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-9.1.0.tgz#638c636780ae98f1930f87e1f76850de5fc37d5c"
|
||||||
|
integrity sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/react" "^0.27.15"
|
||||||
|
clsx "^2.1.1"
|
||||||
|
date-fns "^4.1.0"
|
||||||
|
|
||||||
react-dom@^19.2.0:
|
react-dom@^19.2.0:
|
||||||
version "19.2.3"
|
version "19.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17"
|
||||||
@@ -1881,6 +1931,11 @@ supports-color@^7.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-flag "^4.0.0"
|
has-flag "^4.0.0"
|
||||||
|
|
||||||
|
tabbable@^6.0.0:
|
||||||
|
version "6.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.4.0.tgz#36eb7a06d80b3924a22095daf45740dea3bf5581"
|
||||||
|
integrity sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==
|
||||||
|
|
||||||
tiny-invariant@^1.3.3:
|
tiny-invariant@^1.3.3:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
|
||||||
|
|||||||
@@ -28,6 +28,24 @@ function findGroup(data: ClientData, id: string): OrderGroup | undefined {
|
|||||||
return data.groups?.find(g => g.id === id);
|
return data.groups?.find(g => g.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrátí seznam ISO dat (YYYY-MM-DD), pro která existuje alespoň jedna objednávková skupina.
|
||||||
|
* Slouží ke zvýraznění dnů v date pickeru na stránce objednávání.
|
||||||
|
*/
|
||||||
|
export async function getOrderDates(): Promise<string[]> {
|
||||||
|
const EXTRA_SUFFIX = '_extra';
|
||||||
|
const keys = await storage.listKeys(EXTRA_SUFFIX);
|
||||||
|
const dates: string[] = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!key.endsWith(EXTRA_SUFFIX)) continue;
|
||||||
|
const data = await storage.getData<ClientData>(key);
|
||||||
|
if (data?.groups && data.groups.length > 0) {
|
||||||
|
dates.push(key.slice(0, -EXTRA_SUFFIX.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dates.sort();
|
||||||
|
}
|
||||||
|
|
||||||
export async function createGroup(creatorLogin: string, name: string, date?: Date): Promise<ClientData> {
|
export async function createGroup(creatorLogin: string, name: string, date?: Date): Promise<ClientData> {
|
||||||
const stores = await getStores();
|
const stores = await getStores();
|
||||||
if (!stores.some(s => s.toLowerCase() === name.trim().toLowerCase())) {
|
if (!stores.some(s => s.toLowerCase() === name.trim().toLowerCase())) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import express, { Request } from "express";
|
|||||||
import { getLogin } from "../auth";
|
import { getLogin } from "../auth";
|
||||||
import { parseToken } from "../utils";
|
import { parseToken } from "../utils";
|
||||||
import { getWebsocket } from "../websocket";
|
import { getWebsocket } from "../websocket";
|
||||||
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, updateGroupFees } from "../groups";
|
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, updateGroupFees, getOrderDates } from "../groups";
|
||||||
import { GroupState } from "../../../types/gen/types.gen";
|
import { GroupState } from "../../../types/gen/types.gen";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -11,6 +11,13 @@ function broadcastExtra(data: any) {
|
|||||||
getWebsocket().emit("message", data);
|
getWebsocket().emit("message", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.get("/dates", async (_req, res, next) => {
|
||||||
|
try {
|
||||||
|
const dates = await getOrderDates();
|
||||||
|
res.status(200).json({ dates });
|
||||||
|
} catch (e: any) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/create", async (req: Request, res, next) => {
|
router.post("/create", async (req: Request, res, next) => {
|
||||||
const login = getLogin(parseToken(req));
|
const login = getLogin(parseToken(req));
|
||||||
const { name } = req.body ?? {};
|
const { name } = req.body ?? {};
|
||||||
|
|||||||
@@ -29,4 +29,10 @@ export interface StorageInterface {
|
|||||||
* @param data data pro uložení
|
* @param data data pro uložení
|
||||||
*/
|
*/
|
||||||
setData<Type>(key: string, data: Type): Promise<void>;
|
setData<Type>(key: string, data: Type): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrátí seznam všech klíčů, případně jen těch obsahujících předaný podřetězec.
|
||||||
|
* @param contains volitelný podřetězec, který musí klíč obsahovat (např. '_extra')
|
||||||
|
*/
|
||||||
|
listKeys(contains?: string): Promise<string[]>;
|
||||||
}
|
}
|
||||||
@@ -29,4 +29,9 @@ export default class JsonStorage implements StorageInterface {
|
|||||||
db.set(key, data);
|
db.set(key, data);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listKeys(contains?: string): Promise<string[]> {
|
||||||
|
const keys = Object.keys(db.JSON());
|
||||||
|
return Promise.resolve(contains ? keys.filter(k => k.includes(contains)) : keys);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -24,4 +24,9 @@ export default class MemoryStorage implements StorageInterface {
|
|||||||
store.set(key, data);
|
store.set(key, data);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listKeys(contains?: string): Promise<string[]> {
|
||||||
|
const keys = Array.from(store.keys());
|
||||||
|
return Promise.resolve(contains ? keys.filter(k => k.includes(contains)) : keys);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,16 @@ export default class RedisStorage implements StorageInterface {
|
|||||||
await client.json.set(key, '.', data as any);
|
await client.json.set(key, '.', data as any);
|
||||||
await client.json.get(key);
|
await client.json.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listKeys(contains?: string): Promise<string[]> {
|
||||||
|
// SCAN je bezpečnější než KEYS na produkci (neblokuje server)
|
||||||
|
const match = contains ? `*${contains}*` : '*';
|
||||||
|
const keys: string[] = [];
|
||||||
|
for await (const key of client.scanIterator({ MATCH: match, COUNT: 100 })) {
|
||||||
|
// node-redis v4 vrací buď string, nebo (novější verze) pole stringů
|
||||||
|
if (Array.isArray(key)) keys.push(...key);
|
||||||
|
else keys.push(key);
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,10 @@ const implementations: [string, () => StorageInterface, () => void][] = [
|
|||||||
inst.hasData = async (key: string) => Promise.resolve((inst as any).db.has(key));
|
inst.hasData = async (key: string) => Promise.resolve((inst as any).db.has(key));
|
||||||
inst.getData = async (key: string) => (inst as any).db.get(key);
|
inst.getData = async (key: string) => (inst as any).db.get(key);
|
||||||
inst.setData = async (key: string, data: any) => { (inst as any).db.set(key, data); return Promise.resolve(); };
|
inst.setData = async (key: string, data: any) => { (inst as any).db.set(key, data); return Promise.resolve(); };
|
||||||
|
inst.listKeys = async (contains?: string) => {
|
||||||
|
const keys = Object.keys((inst as any).db.JSON());
|
||||||
|
return contains ? keys.filter((k: string) => k.includes(contains)) : keys;
|
||||||
|
};
|
||||||
return inst;
|
return inst;
|
||||||
}, () => {
|
}, () => {
|
||||||
if (fs.existsSync(tempDbPath)) {
|
if (fs.existsSync(tempDbPath)) {
|
||||||
@@ -76,6 +80,22 @@ describe.each(implementations)('%s splňuje StorageInterface kontrakt', (name, f
|
|||||||
expect((await storage.getData<{ val: string }>('a'))?.val).toBe('A');
|
expect((await storage.getData<{ val: string }>('a'))?.val).toBe('A');
|
||||||
expect((await storage.getData<{ val: string }>('b'))?.val).toBe('B');
|
expect((await storage.getData<{ val: string }>('b'))?.val).toBe('B');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('listKeys vrátí všechny uložené klíče', async () => {
|
||||||
|
await storage.setData('2024-01-01_extra', {});
|
||||||
|
await storage.setData('2024-01-02', {});
|
||||||
|
const keys = await storage.listKeys();
|
||||||
|
expect(keys).toContain('2024-01-01_extra');
|
||||||
|
expect(keys).toContain('2024-01-02');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('listKeys filtruje podle podřetězce', async () => {
|
||||||
|
await storage.setData('2024-01-01_extra', {});
|
||||||
|
await storage.setData('2024-01-02_extra', {});
|
||||||
|
await storage.setData('2024-01-02', {});
|
||||||
|
const keys = await storage.listKeys('_extra');
|
||||||
|
expect(keys.sort()).toEqual(['2024-01-01_extra', '2024-01-02_extra']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ paths:
|
|||||||
$ref: "./paths/changelogs/getChangelogs.yml"
|
$ref: "./paths/changelogs/getChangelogs.yml"
|
||||||
|
|
||||||
# Skupiny objednávek (/api/groups)
|
# Skupiny objednávek (/api/groups)
|
||||||
|
/groups/dates:
|
||||||
|
$ref: "./paths/groups/getOrderDates.yml"
|
||||||
/groups/create:
|
/groups/create:
|
||||||
$ref: "./paths/groups/createGroup.yml"
|
$ref: "./paths/groups/createGroup.yml"
|
||||||
/groups/delete:
|
/groups/delete:
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
get:
|
||||||
|
operationId: getOrderDates
|
||||||
|
summary: Vrátí seznam dnů, pro které existuje alespoň jedna objednávková skupina.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Seznam dnů s objednávkou
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- dates
|
||||||
|
properties:
|
||||||
|
dates:
|
||||||
|
description: Pole ISO dat (YYYY-MM-DD) s alespoň jednou skupinou
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
Reference in New Issue
Block a user