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

This commit is contained in:
2026-06-05 14:50:33 +02:00
parent fb84bff687
commit f28f127a92
13 changed files with 284 additions and 12 deletions
+91 -3
View File
@@ -284,9 +284,97 @@ body {
margin-bottom: 16px;
gap: 16px;
input[type="date"] {
text-align: center;
font-weight: 600;
// react-datepicker obaluje input do wrapperu necháme ho zabrat jen potřebnou šířku
.react-datepicker-wrapper {
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;
}
}
+42 -8
View File
@@ -3,9 +3,12 @@ import { Alert, Badge, Button, Card, Form, Modal, OverlayTrigger, Table, Tooltip
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
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 DatePicker, { registerLocale } from 'react-datepicker';
import { cs } from 'date-fns/locale';
import 'react-datepicker/dist/react-datepicker.css';
import {
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';
import { computeFeeShare, computeMemberTotal, countActiveMembers } from '../utils/groupFees';
import { EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from '../context/socket';
@@ -24,6 +27,9 @@ import PendingPayments from '../components/PendingPayments';
const SLOT = MealSlot.EXTRA;
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í. */
function shiftIsoDate(iso: string, days: number): string {
const date = new Date(`${iso}T00:00:00`);
@@ -31,6 +37,11 @@ function shiftIsoDate(iso: string, days: number): string {
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) {
const map: Record<GroupState, { bg: string; label: string }> = {
[GroupState.OPEN]: { bg: 'success', label: 'Otevřeno' },
@@ -53,6 +64,8 @@ export default function OrderGroupsPage() {
const [todayIso, setTodayIso] = useState<string | undefined>();
// Ref pro socket handler aby věděl, zda se zobrazuje historie (na ní se živé aktualizace neaplikují)
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 [creating, setCreating] = 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(() => {
selectedDateRef.current = selectedDate;
}, [selectedDate]);
@@ -87,6 +106,11 @@ export default function OrderGroupsPage() {
fetchData(selectedDate);
}, [auth?.login, selectedDate]);
useEffect(() => {
if (!auth?.login) return;
fetchOrderDates();
}, [auth?.login]);
useEffect(() => {
socket.on(EVENT_MESSAGE, (newData: ClientData) => {
// Ž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);
}
await fetchData();
// Sada dnů s objednávkou se mohla změnit (vytvoření/smazání skupiny)
fetchOrderDates();
return true;
};
@@ -265,6 +291,11 @@ export default function OrderGroupsPage() {
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 (
<div className="app-container">
<Header choices={data.choices} />
@@ -282,13 +313,16 @@ export default function OrderGroupsPage() {
<span title="Předchozí den">
<FontAwesomeIcon icon={faChevronLeft} onClick={() => goToDay(-1)} />
</span>
<Form.Control
type="date"
value={displayedIso ?? ''}
max={todayIso}
onChange={e => handleDatePick(e.target.value)}
className={isReadOnly ? 'text-muted' : ''}
style={{ maxWidth: 200 }}
<DatePicker
selected={isoToDate(displayedIso)}
onChange={(d: Date | null) => handleDatePick(d ? formatDate(d) : '')}
maxDate={isoToDate(todayIso) ?? undefined}
highlightDates={[{ 'luncher-order-day': highlightedOrderDates }]}
locale="cs"
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">
<FontAwesomeIcon