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

This commit is contained in:
2026-09-07 11:30:40 +02:00
parent ecc28ee82d
commit b6834cb7aa
6 changed files with 162 additions and 25 deletions
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
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 { computeFeeShare, computeMemberTotal, countActiveMembers, isActiveMember } from "../../utils/groupFees";
@@ -102,6 +102,8 @@ export default function PayForGroupModal({ isOpen, onClose, onSuccess, group, pa
};
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 (
<Modal show={isOpen} onHide={onClose} size="lg">
@@ -115,6 +117,12 @@ export default function PayForGroupModal({ isOpen, onClose, onSuccess, group, pa
</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>
{error && (
+37 -10
View File
@@ -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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
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 { cs } from 'date-fns/locale';
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;
}
/** 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) {
const map: Record<GroupState, { bg: string; label: string }> = {
[GroupState.OPEN]: { bg: 'success', label: 'Otevřeno' },
[GroupState.LOCKED]: { bg: 'warning', label: 'Uzamčeno' },
[GroupState.ORDERED]: { bg: 'secondary', label: 'Objednáno' },
[GroupState.DELIVERED]: { bg: 'info', label: 'Doručeno' },
};
const { bg, label } = map[state] ?? { bg: 'light', label: state };
return <Badge bg={bg}>{label}</Badge>;
@@ -281,6 +287,12 @@ export default function OrderGroupsPage() {
const handleRevertOrdered = (group: OrderGroup) =>
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) =>
refresh(() => deleteGroup({ body: { id: groupId } }));
@@ -346,7 +358,7 @@ export default function OrderGroupsPage() {
// Historie (jiný než aktuální den) je vždy read-only.
const canEditMember = (group: OrderGroup, targetLogin: string) => {
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 === targetLogin && group.state === GroupState.OPEN) return true;
return false;
@@ -354,7 +366,7 @@ export default function OrderGroupsPage() {
const canManageMembers = (group: OrderGroup) => {
if (selectedDate) return false;
if (group.state === GroupState.ORDERED) return false;
if (isSubmittedState(group.state)) return false;
if (auth?.login === group.creatorLogin) return true;
return group.state === GroupState.OPEN;
};
@@ -520,6 +532,9 @@ export default function OrderGroupsPage() {
const isCreator = login === group.creatorLogin;
const isMember = login in group.members;
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 memberEntries = Object.entries(group.members) as [string, OrderGroupMember][];
const editingTimes = group.id in editTimes;
@@ -556,7 +571,7 @@ export default function OrderGroupsPage() {
<small className="text-muted">zakladatel: {group.creatorLogin}</small>
</div>
<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">
Poplatky
@@ -574,20 +589,32 @@ export default function OrderGroupsPage() {
</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 && (
<Button variant="primary" size="sm" onClick={() => setPayModal(group)}>
<FontAwesomeIcon icon={faBasketShopping} className="me-1" />
Generovat QR
</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>
{isDelivered ? (
<Button variant="outline-warning" size="sm" onClick={() => handleRevertDelivered(group)} title="Vrátit na Objednáno">
<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)}>
<FontAwesomeIcon icon={faUserPlus} className="me-1" />
Přidat se
@@ -773,7 +800,7 @@ export default function OrderGroupsPage() {
)}
{/* Časy objednání a doručení */}
{isOrdered && (
{isSubmitted && (
<div className="px-3 py-2 border-top">
{!isReadOnly && isCreator && editingTimes ? (
<div className="d-flex align-items-center gap-3 flex-wrap">