feat: nový stav "Doručeno" pro objednávky
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 26s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 42s
CI / Playwright E2E tests (push) Successful in 1m43s
CI / Build and push Docker image (push) Successful in 48s
CI / Notify (push) Successful in 1s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 26s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 42s
CI / Playwright E2E tests (push) Successful in 1m43s
CI / Build and push Docker image (push) Successful in 48s
CI / Notify (push) Successful in 1s
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Modal, Button, Form, Table, Alert } from "react-bootstrap";
|
import { Modal, Button, Form, Table, Alert } from "react-bootstrap";
|
||||||
import { generateQr, OrderGroup, OrderGroupMember, QrRecipient } from "../../../../types";
|
import { generateQr, GroupState, OrderGroup, OrderGroupMember, QrRecipient } from "../../../../types";
|
||||||
import { sanitizeQrMessage } from "../../Utils";
|
import { sanitizeQrMessage } from "../../Utils";
|
||||||
import { computeFeeShare, computeMemberTotal, countActiveMembers, isActiveMember } from "../../utils/groupFees";
|
import { computeFeeShare, computeMemberTotal, countActiveMembers, isActiveMember } from "../../utils/groupFees";
|
||||||
|
|
||||||
@@ -102,6 +102,8 @@ export default function PayForGroupModal({ isOpen, onClose, onSuccess, group, pa
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasFees = totalFees > 0;
|
const hasFees = totalFees > 0;
|
||||||
|
// Generování QR je poslední krok objednávky — server skupinu zároveň překlopí do stavu Doručeno
|
||||||
|
const willBeMarkedDelivered = group.state !== GroupState.DELIVERED;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show={isOpen} onHide={onClose} size="lg">
|
<Modal show={isOpen} onHide={onClose} size="lg">
|
||||||
@@ -115,6 +117,12 @@ export default function PayForGroupModal({ isOpen, onClose, onSuccess, group, pa
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{willBeMarkedDelivered && (
|
||||||
|
<Alert variant="info">
|
||||||
|
Objednávka není ve stavu <strong>Doručeno</strong> — vygenerováním QR kódů se do tohoto stavu automaticky přepne.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<p>Zaplatili jste za skupinu. Vyberte, komu vygenerovat QR kód k úhradě.</p>
|
<p>Zaplatili jste za skupinu. Vyberte, komu vygenerovat QR kód k úhradě.</p>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|||||||
import { Alert, Badge, Button, Card, Form, Modal, OverlayTrigger, Table, Tooltip } from 'react-bootstrap';
|
import { Alert, Badge, Button, Card, Form, Modal, OverlayTrigger, Table, Tooltip } from 'react-bootstrap';
|
||||||
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, faPen, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
import { faBasketShopping, faChevronLeft, faChevronRight, faCircleCheck, faClockRotateLeft, faGear, faLock, faLockOpen, faPen, faSearch, faTruck, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
import DatePicker, { registerLocale } from 'react-datepicker';
|
import DatePicker, { registerLocale } from 'react-datepicker';
|
||||||
import { cs } from 'date-fns/locale';
|
import { cs } from 'date-fns/locale';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
@@ -102,11 +102,17 @@ function isoToDate(iso?: string): Date | null {
|
|||||||
return iso ? new Date(`${iso}T00:00:00`) : null;
|
return iso ? new Date(`${iso}T00:00:00`) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stavy, ve kterých je objednávka již odeslána do podniku (objednáno/doručeno) — skupina se nesmí upravovat. */
|
||||||
|
function isSubmittedState(state: GroupState): boolean {
|
||||||
|
return state === GroupState.ORDERED || state === GroupState.DELIVERED;
|
||||||
|
}
|
||||||
|
|
||||||
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' },
|
||||||
[GroupState.LOCKED]: { bg: 'warning', label: 'Uzamčeno' },
|
[GroupState.LOCKED]: { bg: 'warning', label: 'Uzamčeno' },
|
||||||
[GroupState.ORDERED]: { bg: 'secondary', label: 'Objednáno' },
|
[GroupState.ORDERED]: { bg: 'secondary', label: 'Objednáno' },
|
||||||
|
[GroupState.DELIVERED]: { bg: 'info', label: 'Doručeno' },
|
||||||
};
|
};
|
||||||
const { bg, label } = map[state] ?? { bg: 'light', label: state };
|
const { bg, label } = map[state] ?? { bg: 'light', label: state };
|
||||||
return <Badge bg={bg}>{label}</Badge>;
|
return <Badge bg={bg}>{label}</Badge>;
|
||||||
@@ -281,6 +287,12 @@ export default function OrderGroupsPage() {
|
|||||||
const handleRevertOrdered = (group: OrderGroup) =>
|
const handleRevertOrdered = (group: OrderGroup) =>
|
||||||
refresh(() => setGroupState({ body: { id: group.id, state: GroupState.LOCKED } }));
|
refresh(() => setGroupState({ body: { id: group.id, state: GroupState.LOCKED } }));
|
||||||
|
|
||||||
|
const handleSetDelivered = (group: OrderGroup) =>
|
||||||
|
refresh(() => setGroupState({ body: { id: group.id, state: GroupState.DELIVERED } }));
|
||||||
|
|
||||||
|
const handleRevertDelivered = (group: OrderGroup) =>
|
||||||
|
refresh(() => setGroupState({ body: { id: group.id, state: GroupState.ORDERED } }));
|
||||||
|
|
||||||
const handleDelete = (groupId: string) =>
|
const handleDelete = (groupId: string) =>
|
||||||
refresh(() => deleteGroup({ body: { id: groupId } }));
|
refresh(() => deleteGroup({ body: { id: groupId } }));
|
||||||
|
|
||||||
@@ -346,7 +358,7 @@ export default function OrderGroupsPage() {
|
|||||||
// Historie (jiný než aktuální den) je vždy read-only.
|
// Historie (jiný než aktuální den) je vždy read-only.
|
||||||
const canEditMember = (group: OrderGroup, targetLogin: string) => {
|
const canEditMember = (group: OrderGroup, targetLogin: string) => {
|
||||||
if (selectedDate) return false;
|
if (selectedDate) return false;
|
||||||
if (group.state === GroupState.ORDERED) return false;
|
if (isSubmittedState(group.state)) return false;
|
||||||
if (auth?.login === group.creatorLogin) return true;
|
if (auth?.login === group.creatorLogin) return true;
|
||||||
if (auth?.login === targetLogin && group.state === GroupState.OPEN) return true;
|
if (auth?.login === targetLogin && group.state === GroupState.OPEN) return true;
|
||||||
return false;
|
return false;
|
||||||
@@ -354,7 +366,7 @@ export default function OrderGroupsPage() {
|
|||||||
|
|
||||||
const canManageMembers = (group: OrderGroup) => {
|
const canManageMembers = (group: OrderGroup) => {
|
||||||
if (selectedDate) return false;
|
if (selectedDate) return false;
|
||||||
if (group.state === GroupState.ORDERED) return false;
|
if (isSubmittedState(group.state)) return false;
|
||||||
if (auth?.login === group.creatorLogin) return true;
|
if (auth?.login === group.creatorLogin) return true;
|
||||||
return group.state === GroupState.OPEN;
|
return group.state === GroupState.OPEN;
|
||||||
};
|
};
|
||||||
@@ -520,6 +532,9 @@ export default function OrderGroupsPage() {
|
|||||||
const isCreator = login === group.creatorLogin;
|
const isCreator = login === group.creatorLogin;
|
||||||
const isMember = login in group.members;
|
const isMember = login in group.members;
|
||||||
const isOrdered = group.state === GroupState.ORDERED;
|
const isOrdered = group.state === GroupState.ORDERED;
|
||||||
|
const isDelivered = group.state === GroupState.DELIVERED;
|
||||||
|
// Objednáno i Doručeno = objednávka je odeslána do podniku, skupina se už needituje
|
||||||
|
const isSubmitted = isOrdered || isDelivered;
|
||||||
const isLocked = group.state === GroupState.LOCKED;
|
const isLocked = group.state === GroupState.LOCKED;
|
||||||
const memberEntries = Object.entries(group.members) as [string, OrderGroupMember][];
|
const memberEntries = Object.entries(group.members) as [string, OrderGroupMember][];
|
||||||
const editingTimes = group.id in editTimes;
|
const editingTimes = group.id in editTimes;
|
||||||
@@ -556,7 +571,7 @@ export default function OrderGroupsPage() {
|
|||||||
<small className="text-muted">zakladatel: {group.creatorLogin}</small>
|
<small className="text-muted">zakladatel: {group.creatorLogin}</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
{!isReadOnly && isCreator && !isOrdered && (
|
{!isReadOnly && isCreator && !isSubmitted && (
|
||||||
<>
|
<>
|
||||||
<Button variant="outline-info" size="sm" onClick={() => setFeesModal(group)} title="Upravit poplatky a slevu">
|
<Button variant="outline-info" size="sm" onClick={() => setFeesModal(group)} title="Upravit poplatky a slevu">
|
||||||
Poplatky
|
Poplatky
|
||||||
@@ -574,20 +589,32 @@ export default function OrderGroupsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isReadOnly && isCreator && isOrdered && (
|
{!isReadOnly && isCreator && isSubmitted && (
|
||||||
<>
|
<>
|
||||||
|
{isOrdered && (
|
||||||
|
<Button variant="outline-success" size="sm" onClick={() => handleSetDelivered(group)} title="Označit objednávku jako doručenou">
|
||||||
|
<FontAwesomeIcon icon={faTruck} className="me-1" />
|
||||||
|
Doručeno
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{settings?.bankAccount && settings?.holderName && !group.qrGenerated && (
|
{settings?.bankAccount && settings?.holderName && !group.qrGenerated && (
|
||||||
<Button variant="primary" size="sm" onClick={() => setPayModal(group)}>
|
<Button variant="primary" size="sm" onClick={() => setPayModal(group)}>
|
||||||
<FontAwesomeIcon icon={faBasketShopping} className="me-1" />
|
<FontAwesomeIcon icon={faBasketShopping} className="me-1" />
|
||||||
Generovat QR
|
Generovat QR
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button variant="outline-warning" size="sm" onClick={() => handleRevertOrdered(group)} title="Vrátit na Uzamčeno (smaže QR kódy)">
|
{isDelivered ? (
|
||||||
<FontAwesomeIcon icon={faLockOpen} />
|
<Button variant="outline-warning" size="sm" onClick={() => handleRevertDelivered(group)} title="Vrátit na Objednáno">
|
||||||
</Button>
|
<FontAwesomeIcon icon={faClockRotateLeft} />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="outline-warning" size="sm" onClick={() => handleRevertOrdered(group)} title="Vrátit na Uzamčeno (smaže QR kódy)">
|
||||||
|
<FontAwesomeIcon icon={faLockOpen} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isReadOnly && !isMember && !isOrdered && !isLocked && (
|
{!isReadOnly && !isMember && !isSubmitted && !isLocked && (
|
||||||
<Button variant="outline-success" size="sm" onClick={() => handleJoin(group.id)}>
|
<Button variant="outline-success" size="sm" onClick={() => handleJoin(group.id)}>
|
||||||
<FontAwesomeIcon icon={faUserPlus} className="me-1" />
|
<FontAwesomeIcon icon={faUserPlus} className="me-1" />
|
||||||
Přidat se
|
Přidat se
|
||||||
@@ -773,7 +800,7 @@ export default function OrderGroupsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Časy objednání a doručení */}
|
{/* Časy objednání a doručení */}
|
||||||
{isOrdered && (
|
{isSubmitted && (
|
||||||
<div className="px-3 py-2 border-top">
|
<div className="px-3 py-2 border-top">
|
||||||
{!isReadOnly && isCreator && editingTimes ? (
|
{!isReadOnly && isCreator && editingTimes ? (
|
||||||
<div className="d-flex align-items-center gap-3 flex-wrap">
|
<div className="d-flex align-items-center gap-3 flex-wrap">
|
||||||
|
|||||||
+36
-10
@@ -29,6 +29,26 @@ function findGroup(data: ClientData, id: string): OrderGroup | undefined {
|
|||||||
return data.groups?.find(g => g.id === id);
|
return data.groups?.find(g => g.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** České názvy stavů pro chybová hlášení. */
|
||||||
|
const STATE_LABEL: Record<GroupState, string> = {
|
||||||
|
[GroupState.OPEN]: 'otevřeno',
|
||||||
|
[GroupState.LOCKED]: 'uzamčeno',
|
||||||
|
[GroupState.ORDERED]: 'objednáno',
|
||||||
|
[GroupState.DELIVERED]: 'doručeno',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Stavy, ve kterých je objednávka již odeslána do podniku a nelze ji tedy upravovat. */
|
||||||
|
function isSubmitted(state: GroupState): boolean {
|
||||||
|
return state === GroupState.ORDERED || state === GroupState.DELIVERED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ověří, že skupina není v odeslaném stavu (objednáno/doručeno), jinak vyhodí chybu. */
|
||||||
|
function assertNotSubmitted(group: OrderGroup): void {
|
||||||
|
if (isSubmitted(group.state)) {
|
||||||
|
throw new Error(`Skupinu ve stavu "${STATE_LABEL[group.state]}" nelze upravovat`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vrátí seznam ISO dat (YYYY-MM-DD), pro která existuje alespoň jedna objednávková skupina.
|
* 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í.
|
* Slouží ke zvýraznění dnů v date pickeru na stránce objednávání.
|
||||||
@@ -100,7 +120,7 @@ export async function addGroupMember(login: string, groupId: string, targetLogin
|
|||||||
const data = await getExtraData(date);
|
const data = await getExtraData(date);
|
||||||
const group = findGroup(data, groupId);
|
const group = findGroup(data, groupId);
|
||||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||||
if (group.state === GroupState.ORDERED) throw new Error('Skupinu ve stavu "objednáno" nelze upravovat');
|
assertNotSubmitted(group);
|
||||||
if (login !== group.creatorLogin && login !== targetLogin) {
|
if (login !== group.creatorLogin && login !== targetLogin) {
|
||||||
throw new Error('Přidat jiného uživatele může pouze zakladatel');
|
throw new Error('Přidat jiného uživatele může pouze zakladatel');
|
||||||
}
|
}
|
||||||
@@ -116,7 +136,7 @@ export async function removeGroupMember(login: string, groupId: string, targetLo
|
|||||||
const data = await getExtraData(date);
|
const data = await getExtraData(date);
|
||||||
const group = findGroup(data, groupId);
|
const group = findGroup(data, groupId);
|
||||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||||
if (group.state === GroupState.ORDERED) throw new Error('Skupinu ve stavu "objednáno" nelze upravovat');
|
assertNotSubmitted(group);
|
||||||
if (login !== group.creatorLogin && login !== targetLogin) {
|
if (login !== group.creatorLogin && login !== targetLogin) {
|
||||||
throw new Error('Odebrat jiného uživatele může pouze zakladatel');
|
throw new Error('Odebrat jiného uživatele může pouze zakladatel');
|
||||||
}
|
}
|
||||||
@@ -133,7 +153,7 @@ export async function updateGroupMember(login: string, groupId: string, targetLo
|
|||||||
const data = await getExtraData(date);
|
const data = await getExtraData(date);
|
||||||
const group = findGroup(data, groupId);
|
const group = findGroup(data, groupId);
|
||||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||||
if (group.state === GroupState.ORDERED) throw new Error('Skupinu ve stavu "objednáno" nelze upravovat');
|
assertNotSubmitted(group);
|
||||||
const isSelf = login === targetLogin;
|
const isSelf = login === targetLogin;
|
||||||
const isCreator = login === group.creatorLogin;
|
const isCreator = login === group.creatorLogin;
|
||||||
if (!isSelf && !isCreator) throw new Error('Upravit jiného uživatele může pouze zakladatel');
|
if (!isSelf && !isCreator) throw new Error('Upravit jiného uživatele může pouze zakladatel');
|
||||||
@@ -148,7 +168,8 @@ export async function updateGroupMember(login: string, groupId: string, targetLo
|
|||||||
const VALID_TRANSITIONS: Record<GroupState, GroupState[]> = {
|
const VALID_TRANSITIONS: Record<GroupState, GroupState[]> = {
|
||||||
[GroupState.OPEN]: [GroupState.LOCKED],
|
[GroupState.OPEN]: [GroupState.LOCKED],
|
||||||
[GroupState.LOCKED]: [GroupState.OPEN, GroupState.ORDERED],
|
[GroupState.LOCKED]: [GroupState.OPEN, GroupState.ORDERED],
|
||||||
[GroupState.ORDERED]: [GroupState.LOCKED],
|
[GroupState.ORDERED]: [GroupState.LOCKED, GroupState.DELIVERED],
|
||||||
|
[GroupState.DELIVERED]: [GroupState.ORDERED],
|
||||||
};
|
};
|
||||||
|
|
||||||
function getCurrentHHMM(): string {
|
function getCurrentHHMM(): string {
|
||||||
@@ -162,7 +183,7 @@ export async function setGroupState(login: string, groupId: string, newState: Gr
|
|||||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||||
if (group.creatorLogin !== login) throw new Error('Stav může měnit pouze zakladatel');
|
if (group.creatorLogin !== login) throw new Error('Stav může měnit pouze zakladatel');
|
||||||
if (!VALID_TRANSITIONS[group.state].includes(newState)) {
|
if (!VALID_TRANSITIONS[group.state].includes(newState)) {
|
||||||
throw new Error(`Nelze přejít ze stavu "${group.state}" do stavu "${newState}"`);
|
throw new Error(`Nelze přejít ze stavu "${STATE_LABEL[group.state]}" do stavu "${STATE_LABEL[newState]}"`);
|
||||||
}
|
}
|
||||||
if (newState === GroupState.ORDERED) {
|
if (newState === GroupState.ORDERED) {
|
||||||
group.orderedAt = getCurrentHHMM();
|
group.orderedAt = getCurrentHHMM();
|
||||||
@@ -185,14 +206,19 @@ export async function setGroupState(login: string, groupId: string, newState: Gr
|
|||||||
return saveExtraData(data, date);
|
return saveExtraData(data, date);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function markGroupQrGenerated(login: string, groupId: string, date?: Date): Promise<void> {
|
/**
|
||||||
|
* Označí skupinu za vygenerovanou (blokuje opakované generování QR) a zároveň ji
|
||||||
|
* překlopí do stavu "doručeno" — generování QR je poslední krok objednávky.
|
||||||
|
*/
|
||||||
|
export async function markGroupQrGenerated(login: string, groupId: string, date?: Date): Promise<ClientData> {
|
||||||
const data = await getExtraData(date);
|
const data = await getExtraData(date);
|
||||||
const group = findGroup(data, groupId);
|
const group = findGroup(data, groupId);
|
||||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||||
if (group.creatorLogin !== login) throw new Error('QR kódy může generovat pouze zakladatel');
|
if (group.creatorLogin !== login) throw new Error('QR kódy může generovat pouze zakladatel');
|
||||||
if (group.state !== GroupState.ORDERED) throw new Error('QR kódy lze generovat pouze ve stavu "objednáno"');
|
if (!isSubmitted(group.state)) throw new Error('QR kódy lze generovat pouze ve stavu "objednáno" nebo "doručeno"');
|
||||||
group.qrGenerated = true;
|
group.qrGenerated = true;
|
||||||
await saveExtraData(data, date);
|
group.state = GroupState.DELIVERED;
|
||||||
|
return saveExtraData(data, date);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function markGroupMemberPaid(login: string, groupId: string, date?: Date): Promise<ClientData | null> {
|
export async function markGroupMemberPaid(login: string, groupId: string, date?: Date): Promise<ClientData | null> {
|
||||||
@@ -208,7 +234,7 @@ export async function updateGroupFees(login: string, groupId: string, fees?: num
|
|||||||
const group = findGroup(data, groupId);
|
const group = findGroup(data, groupId);
|
||||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||||
if (group.creatorLogin !== login) throw new Error('Poplatky může měnit pouze zakladatel');
|
if (group.creatorLogin !== login) throw new Error('Poplatky může měnit pouze zakladatel');
|
||||||
if (group.state === GroupState.ORDERED) throw new Error('Skupinu ve stavu "objednáno" nelze upravovat');
|
assertNotSubmitted(group);
|
||||||
if (fees !== undefined) group.fees = fees > 0 ? fees : undefined;
|
if (fees !== undefined) group.fees = fees > 0 ? fees : undefined;
|
||||||
if (shipping !== undefined) group.shipping = shipping > 0 ? shipping : undefined;
|
if (shipping !== undefined) group.shipping = shipping > 0 ? shipping : undefined;
|
||||||
if (tip !== undefined) group.tip = tip > 0 ? tip : undefined;
|
if (tip !== undefined) group.tip = tip > 0 ? tip : undefined;
|
||||||
@@ -238,7 +264,7 @@ export async function setGroupTracking(login: string, groupId: string, shareUrl?
|
|||||||
group.trackingOrderState = undefined;
|
group.trackingOrderState = undefined;
|
||||||
group.trackingCourierState = undefined;
|
group.trackingCourierState = undefined;
|
||||||
} else {
|
} else {
|
||||||
if (group.state !== GroupState.ORDERED) throw new Error('Sledování objednávky lze nastavit pouze ve stavu "objednáno"');
|
if (!isSubmitted(group.state)) throw new Error('Sledování objednávky lze nastavit pouze ve stavu "objednáno" nebo "doručeno"');
|
||||||
const tracking = extractTracking(shareUrl);
|
const tracking = extractTracking(shareUrl);
|
||||||
if (!tracking) throw new Error('Neplatný odkaz na sledování objednávky');
|
if (!tracking) throw new Error('Neplatný odkaz na sledování objednávky');
|
||||||
if (tracking.code !== group.trackingCode || tracking.provider !== group.trackingProvider) {
|
if (tracking.code !== group.trackingCode || tracking.provider !== group.trackingProvider) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { parseToken, formatDate } from "../utils";
|
|||||||
import { generateQr } from "../qr";
|
import { generateQr } from "../qr";
|
||||||
import { addPendingQr } from "../pizza";
|
import { addPendingQr } from "../pizza";
|
||||||
import { markGroupQrGenerated } from "../groups";
|
import { markGroupQrGenerated } from "../groups";
|
||||||
import { emitToUser } from "../websocket";
|
import { emitToUser, getWebsocket } from "../websocket";
|
||||||
import { GenerateQrData } from "../../../types";
|
import { GenerateQrData } from "../../../types";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
@@ -59,7 +59,9 @@ router.post("/generate", async (req: Request<{}, any, GenerateQrData["body"]>, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
await markGroupQrGenerated(login, groupId);
|
// Skupina se zároveň překlopí do stavu "doručeno" — rozešleme nová data všem
|
||||||
|
const data = await markGroupQrGenerated(login, groupId);
|
||||||
|
getWebsocket().emit("message", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ success: true, count: recipients.length });
|
res.status(200).json({ success: true, count: recipients.length });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { resetMemoryStorage } from '../storage/memory';
|
import { resetMemoryStorage } from '../storage/memory';
|
||||||
import { getStores, addStore } from '../stores';
|
import { getStores, addStore } from '../stores';
|
||||||
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, setGroupTracking, markGroupMemberPaid } from '../groups';
|
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, setGroupTracking, markGroupMemberPaid, markGroupQrGenerated } from '../groups';
|
||||||
import { GroupState } from '../../../types/gen/types.gen';
|
import { GroupState } from '../../../types/gen/types.gen';
|
||||||
|
|
||||||
const CREATOR = 'tomas';
|
const CREATOR = 'tomas';
|
||||||
@@ -219,12 +219,48 @@ describe('setGroupState', () => {
|
|||||||
await expect(setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY)).rejects.toThrow('Nelze přejít');
|
await expect(setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY)).rejects.toThrow('Nelze přejít');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ordered je terminální stav', async () => {
|
test('ordered → open není povoleno', async () => {
|
||||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||||
await expect(setGroupState(CREATOR, groupId, GroupState.OPEN, TODAY)).rejects.toThrow('Nelze přejít');
|
await expect(setGroupState(CREATOR, groupId, GroupState.OPEN, TODAY)).rejects.toThrow('Nelze přejít');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ordered → delivered', async () => {
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||||
|
const d = await setGroupState(CREATOR, groupId, GroupState.DELIVERED, TODAY);
|
||||||
|
expect(d.groups![0].state).toBe(GroupState.DELIVERED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delivered → ordered (vrácení)', async () => {
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.DELIVERED, TODAY);
|
||||||
|
const d = await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||||
|
expect(d.groups![0].state).toBe(GroupState.ORDERED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('locked → delivered není povoleno', async () => {
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||||
|
await expect(setGroupState(CREATOR, groupId, GroupState.DELIVERED, TODAY)).rejects.toThrow('Nelze přejít');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delivered → locked není povoleno', async () => {
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.DELIVERED, TODAY);
|
||||||
|
await expect(setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY)).rejects.toThrow('Nelze přejít');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skupinu ve stavu delivered nelze upravovat', async () => {
|
||||||
|
await addGroupMember(CREATOR, groupId, USER, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.DELIVERED, TODAY);
|
||||||
|
await expect(updateGroupMember(CREATOR, groupId, USER, { amount: 100 }, TODAY)).rejects.toThrow('doručeno');
|
||||||
|
await expect(addGroupMember(CREATOR, groupId, 'dalsi', TODAY)).rejects.toThrow('doručeno');
|
||||||
|
});
|
||||||
|
|
||||||
test('nečlen nemůže měnit stav', async () => {
|
test('nečlen nemůže měnit stav', async () => {
|
||||||
await expect(setGroupState(USER, groupId, GroupState.LOCKED, TODAY)).rejects.toThrow('zakladatel');
|
await expect(setGroupState(USER, groupId, GroupState.LOCKED, TODAY)).rejects.toThrow('zakladatel');
|
||||||
});
|
});
|
||||||
@@ -240,6 +276,42 @@ describe('setGroupState', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('markGroupQrGenerated', () => {
|
||||||
|
let groupId: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||||
|
groupId = d.groups![0].id;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ze stavu ordered přepne skupinu na delivered', async () => {
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||||
|
const d = await markGroupQrGenerated(CREATOR, groupId, TODAY);
|
||||||
|
expect(d.groups![0].state).toBe(GroupState.DELIVERED);
|
||||||
|
expect(d.groups![0].qrGenerated).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('funguje i ve stavu delivered', async () => {
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.DELIVERED, TODAY);
|
||||||
|
const d = await markGroupQrGenerated(CREATOR, groupId, TODAY);
|
||||||
|
expect(d.groups![0].state).toBe(GroupState.DELIVERED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ve stavu locked selže', async () => {
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||||
|
await expect(markGroupQrGenerated(CREATOR, groupId, TODAY)).rejects.toThrow('objednáno');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('QR může generovat pouze zakladatel', async () => {
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||||
|
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||||
|
await expect(markGroupQrGenerated(USER, groupId, TODAY)).rejects.toThrow('zakladatel');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('markGroupMemberPaid', () => {
|
describe('markGroupMemberPaid', () => {
|
||||||
test('označí člena jako zaplaceného pro daný den', async () => {
|
test('označí člena jako zaplaceného pro daný den', async () => {
|
||||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||||
|
|||||||
@@ -950,10 +950,12 @@ GroupState:
|
|||||||
- open
|
- open
|
||||||
- locked
|
- locked
|
||||||
- ordered
|
- ordered
|
||||||
|
- delivered
|
||||||
x-enum-varnames:
|
x-enum-varnames:
|
||||||
- OPEN
|
- OPEN
|
||||||
- LOCKED
|
- LOCKED
|
||||||
- ORDERED
|
- ORDERED
|
||||||
|
- DELIVERED
|
||||||
|
|
||||||
OrderGroupMember:
|
OrderGroupMember:
|
||||||
description: Data člena skupiny objednávky
|
description: Data člena skupiny objednávky
|
||||||
|
|||||||
Reference in New Issue
Block a user