Opgave 11c: Ordreoversigt + markér betalt (harmonika-UI)
OrdersPage (/runder/:roundId/ordrer, linket fra RoundsPage og
RoundDetailPage) — en to-niveaus harmonika designet af brugeren:
øverste fold viser rundens flasketotal, total og udestående beløb
(sum af ordrer der endnu ikke er betalt), foldet ud til rundens
samlede, kategori-grupperede vinliste. Derunder én fold pr. ordre
(sorteret efter order_number, rød/grøn kant for betalt/ubetalt-status),
foldet ud til "Markér betalt" (POST /orders/{id}/mark-paid) + ordrens
egen kategori-grupperede vinliste. Ingen backend-ændringer nødvendige
— alt data kommer fra opgave 9's eksisterende GET /orders, al
aggregering sker client-side.
Verificeret i en fuldstændig isoleret sandbox (midlertidig separat
rute) for slet ikke at røre de 308 rigtige deltageres data.
To fund fra brugertest, begge rettet:
1. Beløb blev vist i EUR — vises nu i DKK via rundens eur_dkk_rate
(falder tilbage til EUR hvis runden ikke har en kurs), samme
omregningsprincip som den offentlige side/kvitteringsmailen.
2. Vinlisterne manglede kategori-gruppering (kun den samlede liste
havde det først) og viste antal efter vinnavn i stedet for før —
begge lister er nu kategori-grupperede med antal først.
Dette afslutter opgave 11 (hele admin-UI'et).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
cae8657ec8
commit
16d89db527
6 changed files with 336 additions and 4 deletions
36
CLAUDE.md
36
CLAUDE.md
|
|
@ -206,8 +206,8 @@ at være for stort til én omgang og er selv delt i tre dele, alle
|
||||||
stadig under "11b" (ingen ny bogstav-opdeling), **alle tre nu ✅**:
|
stadig under "11b" (ingen ny bogstav-opdeling), **alle tre nu ✅**:
|
||||||
**del 1** (delt frontend-infrastruktur + Deltagere-CRUD), **del 2**
|
**del 1** (delt frontend-infrastruktur + Deltagere-CRUD), **del 2**
|
||||||
(runder + vinliste), **del 3** (mail-skabelon-editor +
|
(runder + vinliste), **del 3** (mail-skabelon-editor +
|
||||||
annoncér-knap). **11c** (ordreoversigt/markér betalt) er ikke bygget
|
annoncér-knap). **11c** (ordreoversigt/markér betalt) er nu også ✅ —
|
||||||
endnu.
|
hele opgave 11 er dermed færdig.
|
||||||
|
|
||||||
**Hosting-beslutning:** Admin-UI'et (`admin-ui/`, React + TypeScript,
|
**Hosting-beslutning:** Admin-UI'et (`admin-ui/`, React + TypeScript,
|
||||||
almindelig CSS — ingen framework) serveres af **samme FastAPI-app**
|
almindelig CSS — ingen framework) serveres af **samme FastAPI-app**
|
||||||
|
|
@ -341,6 +341,33 @@ backend-ændringer nødvendige — opgave 7a's `MailTemplate`-CRUD og
|
||||||
utilsigtet rigtig annoncering til de 308 rigtige deltagere under
|
utilsigtet rigtig annoncering til de 308 rigtige deltagere under
|
||||||
udvikling.
|
udvikling.
|
||||||
|
|
||||||
|
**11c** (ordreoversigt/markér betalt) afslutter opgave 11. Ingen
|
||||||
|
backend-ændringer nødvendige — alt data kommer fra opgave 9's
|
||||||
|
allerede eksisterende `GET /orders?purchase_round_id=<id>`
|
||||||
|
(`OrderAdminView`), al aggregering (flaske-/beløbstotal, udestående,
|
||||||
|
den runde-brede vinliste) beregnes client-side.
|
||||||
|
- **`pages/OrdersPage.tsx`** (`/runder/:roundId/ordrer`, linket fra
|
||||||
|
`RoundsPage` og `RoundDetailPage`) — en to-niveaus harmonika
|
||||||
|
(bestemt af brugeren): øverste fold viser rundens samlede
|
||||||
|
flasketal, total og **udestående** (defineret som summen af
|
||||||
|
ordrer der endnu ikke er markeret betalt — ikke bogstaveligt "total
|
||||||
|
minus ubetalte", som ville givet summen af de betalte ordrer i
|
||||||
|
stedet), foldet ud til den samlede, **kategori-grupperede**
|
||||||
|
vinliste for hele runden. Derunder én fold pr. ordre (sorteret
|
||||||
|
efter `order_number`, med en rød/grøn kant der viser
|
||||||
|
betalt/ubetalt-status uden at skulle folde ud), foldet ud til en
|
||||||
|
"Markér betalt"-knap (`POST /orders/{id}/mark-paid`, skjult og
|
||||||
|
erstattet af "Betalt [dato]" hvis allerede betalt) + ordrens egen
|
||||||
|
**kategori-grupperede** vinliste. Alle folder kan være åbne
|
||||||
|
samtidig.
|
||||||
|
- **Beløb vises i DKK** (rundens `eur_dkk_rate`, samme
|
||||||
|
omregningsprincip som den offentlige bestillingsside og
|
||||||
|
kvitteringsmailen) — falder tilbage til EUR hvis runden ikke har en
|
||||||
|
kurs sat. Fundet under brugertest: beløb blev oprindeligt vist i
|
||||||
|
EUR, og både den samlede og de individuelle vinlister manglede
|
||||||
|
kategori-gruppering samt havde antal efter vinnavn i stedet for
|
||||||
|
før — alle tre rettet.
|
||||||
|
|
||||||
## Mail-events
|
## Mail-events
|
||||||
1. Runde åbnes → mail til alle aktive deltagere på ruten. **Implementeret**
|
1. Runde åbnes → mail til alle aktive deltagere på ruten. **Implementeret**
|
||||||
(opgave 7a-c): skabelon pr. rute (`MailTemplate`,
|
(opgave 7a-c): skabelon pr. rute (`MailTemplate`,
|
||||||
|
|
@ -373,7 +400,7 @@ logges/advares om i stedet.
|
||||||
|
|
||||||
## Fase 1 — nuværende scope
|
## Fase 1 — nuværende scope
|
||||||
|
|
||||||
**Færdige opgaver (1-10, 11a, 11b):**
|
**Færdige opgaver (1-11):**
|
||||||
1. ✅ FastAPI + SQLModel + PostgreSQL + Alembic scaffolding
|
1. ✅ FastAPI + SQLModel + PostgreSQL + Alembic scaffolding
|
||||||
2. ✅ Datamodellerne (Organization/Route-hierarki)
|
2. ✅ Datamodellerne (Organization/Route-hierarki)
|
||||||
3. ✅ Deltager-migrering fra MongoDB (308 deltagere importeret,
|
3. ✅ Deltager-migrering fra MongoDB (308 deltagere importeret,
|
||||||
|
|
@ -428,7 +455,8 @@ logges/advares om i stedet.
|
||||||
(se "Admin-UI-arkitektur" ovenfor), alle færdige: **del 1**
|
(se "Admin-UI-arkitektur" ovenfor), alle færdige: **del 1**
|
||||||
(frontend-infrastruktur + Deltagere-CRUD), **del 2** (runder +
|
(frontend-infrastruktur + Deltagere-CRUD), **del 2** (runder +
|
||||||
vinliste), **del 3** (mail-skabelon-editor + annoncér-knap).
|
vinliste), **del 3** (mail-skabelon-editor + annoncér-knap).
|
||||||
- **11c**: Ordreoversigt/markér betalt — ikke bygget.
|
- **11c**: ✅ Ordreoversigt/markér betalt — se
|
||||||
|
"Admin-UI-arkitektur" ovenfor. Hele opgave 11 er dermed færdig.
|
||||||
|
|
||||||
**Resterende opgaver:**
|
**Resterende opgaver:**
|
||||||
12. Testkør en rigtig runde gennem hele flowet
|
12. Testkør en rigtig runde gennem hele flowet
|
||||||
|
|
|
||||||
|
|
@ -164,3 +164,41 @@ td {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accordion {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-item {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f7f7f7;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header:hover {
|
||||||
|
background: #efefef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header.unpaid {
|
||||||
|
border-left: 4px solid #b3261e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header.paid {
|
||||||
|
border-left: 4px solid #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-body {
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Navigate, Route, BrowserRouter, Routes } from 'react-router-dom'
|
||||||
import { AuthProvider, useAuth } from './AuthContext'
|
import { AuthProvider, useAuth } from './AuthContext'
|
||||||
import { Layout } from './Layout'
|
import { Layout } from './Layout'
|
||||||
import { LoginPage } from './pages/LoginPage'
|
import { LoginPage } from './pages/LoginPage'
|
||||||
|
import { OrdersPage } from './pages/OrdersPage'
|
||||||
import { ParticipantsPage } from './pages/ParticipantsPage'
|
import { ParticipantsPage } from './pages/ParticipantsPage'
|
||||||
import { RoundDetailPage } from './pages/RoundDetailPage'
|
import { RoundDetailPage } from './pages/RoundDetailPage'
|
||||||
import { RoundsPage } from './pages/RoundsPage'
|
import { RoundsPage } from './pages/RoundsPage'
|
||||||
|
|
@ -20,6 +21,7 @@ function AppRoutes() {
|
||||||
<Route path="/deltagere" element={<ParticipantsPage />} />
|
<Route path="/deltagere" element={<ParticipantsPage />} />
|
||||||
<Route path="/runder" element={<RoundsPage />} />
|
<Route path="/runder" element={<RoundsPage />} />
|
||||||
<Route path="/runder/:roundId" element={<RoundDetailPage />} />
|
<Route path="/runder/:roundId" element={<RoundDetailPage />} />
|
||||||
|
<Route path="/runder/:roundId/ordrer" element={<OrdersPage />} />
|
||||||
<Route path="/skabeloner" element={<TemplatesPage />} />
|
<Route path="/skabeloner" element={<TemplatesPage />} />
|
||||||
<Route path="*" element={<Navigate to="/deltagere" replace />} />
|
<Route path="*" element={<Navigate to="/deltagere" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
||||||
260
admin-ui/src/pages/OrdersPage.tsx
Normal file
260
admin-ui/src/pages/OrdersPage.tsx
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { apiFetch, ApiError } from '../api'
|
||||||
|
|
||||||
|
interface PurchaseRound {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
eur_dkk_rate: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WineCategory {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
sort_order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WineOffering {
|
||||||
|
id: number
|
||||||
|
category_id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderLineAdminView {
|
||||||
|
id: number
|
||||||
|
order_id: number
|
||||||
|
wine_offering_id: number
|
||||||
|
quantity: number
|
||||||
|
wine_name: string
|
||||||
|
unit_price: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderAdminView {
|
||||||
|
id: number
|
||||||
|
participant_id: number
|
||||||
|
purchase_round_id: number
|
||||||
|
order_number: number
|
||||||
|
payment_status: 'unpaid' | 'paid'
|
||||||
|
paid_at: string | null
|
||||||
|
created_at: string
|
||||||
|
participant_name: string
|
||||||
|
participant_email: string
|
||||||
|
participant_phone: string | null
|
||||||
|
order_lines: OrderLineAdminView[]
|
||||||
|
total_price_eur: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OrdersPage() {
|
||||||
|
const { roundId } = useParams<{ roundId: string }>()
|
||||||
|
const [round, setRound] = useState<PurchaseRound | null>(null)
|
||||||
|
const [orders, setOrders] = useState<OrderAdminView[]>([])
|
||||||
|
const [wineOfferings, setWineOfferings] = useState<WineOffering[]>([])
|
||||||
|
const [categories, setCategories] = useState<WineCategory[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [summaryOpen, setSummaryOpen] = useState(false)
|
||||||
|
const [openOrderIds, setOpenOrderIds] = useState<Set<number>>(new Set())
|
||||||
|
|
||||||
|
async function loadOrders(forRoundId: string) {
|
||||||
|
const data = await apiFetch<OrderAdminView[]>(`/orders?purchase_round_id=${forRoundId}&limit=500`)
|
||||||
|
setOrders(data.slice().sort((a, b) => a.order_number - b.order_number))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAll() {
|
||||||
|
if (!roundId) return
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const [roundData, wineOfferingData, categoryData] = await Promise.all([
|
||||||
|
apiFetch<PurchaseRound>(`/purchase-rounds/${roundId}`),
|
||||||
|
apiFetch<WineOffering[]>(`/wine-offerings?purchase_round_id=${roundId}&limit=500`),
|
||||||
|
apiFetch<WineCategory[]>('/wine-categories'),
|
||||||
|
loadOrders(roundId),
|
||||||
|
])
|
||||||
|
setRound(roundData)
|
||||||
|
setWineOfferings(wineOfferingData)
|
||||||
|
setCategories(categoryData)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof ApiError ? err.message : 'Kunne ikke hente ordrer')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAll()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [roundId])
|
||||||
|
|
||||||
|
function toggleOrder(id: number) {
|
||||||
|
setOpenOrderIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (next.has(id)) next.delete(id)
|
||||||
|
else next.add(id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMarkPaid(order: OrderAdminView) {
|
||||||
|
if (!roundId) return
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
await apiFetch(`/orders/${order.id}/mark-paid`, { method: 'POST' })
|
||||||
|
await loadOrders(roundId)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof ApiError ? err.message : 'Kunne ikke markere ordren som betalt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return <p>Indlæser...</p>
|
||||||
|
if (!round) return <p className="error">{error ?? 'Runden blev ikke fundet'}</p>
|
||||||
|
|
||||||
|
const rate = round.eur_dkk_rate ? Number(round.eur_dkk_rate) : null
|
||||||
|
|
||||||
|
function formatEur(amountEur: number): string {
|
||||||
|
if (rate) return `${(amountEur * rate).toFixed(2)} kr`
|
||||||
|
return `${amountEur.toFixed(2)} EUR`
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalBottles = orders.reduce(
|
||||||
|
(sum, o) => sum + o.order_lines.reduce((lineSum, l) => lineSum + l.quantity, 0),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
const totalAmount = orders.reduce((sum, o) => sum + Number(o.total_price_eur), 0)
|
||||||
|
const outstanding = orders
|
||||||
|
.filter((o) => o.payment_status === 'unpaid')
|
||||||
|
.reduce((sum, o) => sum + Number(o.total_price_eur), 0)
|
||||||
|
|
||||||
|
const categoryByWineOfferingId = new Map<number, WineCategory>()
|
||||||
|
for (const offering of wineOfferings) {
|
||||||
|
const category = categories.find((c) => c.id === offering.category_id)
|
||||||
|
if (category) categoryByWineOfferingId.set(offering.id, category)
|
||||||
|
}
|
||||||
|
|
||||||
|
const wineAggregation = new Map<number, { name: string; quantity: number; category: WineCategory | undefined }>()
|
||||||
|
for (const order of orders) {
|
||||||
|
for (const line of order.order_lines) {
|
||||||
|
const existing = wineAggregation.get(line.wine_offering_id)
|
||||||
|
if (existing) existing.quantity += line.quantity
|
||||||
|
else
|
||||||
|
wineAggregation.set(line.wine_offering_id, {
|
||||||
|
name: line.wine_name,
|
||||||
|
quantity: line.quantity,
|
||||||
|
category: categoryByWineOfferingId.get(line.wine_offering_id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const wineRows = Array.from(wineAggregation.values()).sort((a, b) => {
|
||||||
|
const orderA = a.category?.sort_order ?? 0
|
||||||
|
const orderB = b.category?.sort_order ?? 0
|
||||||
|
if (orderA !== orderB) return orderA - orderB
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
const wineRowsByCategory = new Map<string, typeof wineRows>()
|
||||||
|
for (const row of wineRows) {
|
||||||
|
const categoryName = row.category?.name ?? 'Ukategoriseret'
|
||||||
|
const list = wineRowsByCategory.get(categoryName) ?? []
|
||||||
|
list.push(row)
|
||||||
|
wineRowsByCategory.set(categoryName, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupOrderLinesByCategory(lines: OrderLineAdminView[]): [string, OrderLineAdminView[]][] {
|
||||||
|
const sorted = lines.slice().sort((a, b) => {
|
||||||
|
const orderA = categoryByWineOfferingId.get(a.wine_offering_id)?.sort_order ?? 0
|
||||||
|
const orderB = categoryByWineOfferingId.get(b.wine_offering_id)?.sort_order ?? 0
|
||||||
|
if (orderA !== orderB) return orderA - orderB
|
||||||
|
return a.wine_name.localeCompare(b.wine_name)
|
||||||
|
})
|
||||||
|
const groups = new Map<string, OrderLineAdminView[]>()
|
||||||
|
for (const line of sorted) {
|
||||||
|
const categoryName = categoryByWineOfferingId.get(line.wine_offering_id)?.name ?? 'Ukategoriseret'
|
||||||
|
const list = groups.get(categoryName) ?? []
|
||||||
|
list.push(line)
|
||||||
|
groups.set(categoryName, list)
|
||||||
|
}
|
||||||
|
return Array.from(groups.entries())
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Ordreoversigt</h1>
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
|
||||||
|
{orders.length === 0 ? (
|
||||||
|
<p>Ingen bestillinger endnu for denne runde.</p>
|
||||||
|
) : (
|
||||||
|
<div className="accordion">
|
||||||
|
<div className="accordion-item">
|
||||||
|
<button className="accordion-header" onClick={() => setSummaryOpen((v) => !v)}>
|
||||||
|
{round.name} — {totalBottles} flasker — Total: {formatEur(totalAmount)} — Udestående:{' '}
|
||||||
|
{formatEur(outstanding)}
|
||||||
|
</button>
|
||||||
|
{summaryOpen && (
|
||||||
|
<div className="accordion-body">
|
||||||
|
{Array.from(wineRowsByCategory.entries()).map(([categoryName, rows]) => (
|
||||||
|
<table key={categoryName}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colSpan={2}>{categoryName}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((row) => (
|
||||||
|
<tr key={row.name}>
|
||||||
|
<td>{row.quantity}</td>
|
||||||
|
<td>{row.name}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{orders.map((order) => (
|
||||||
|
<div className="accordion-item" key={order.id}>
|
||||||
|
<button
|
||||||
|
className={`accordion-header ${order.payment_status === 'paid' ? 'paid' : 'unpaid'}`}
|
||||||
|
onClick={() => toggleOrder(order.id)}
|
||||||
|
>
|
||||||
|
#{order.order_number} — {order.participant_name} — {order.participant_phone ?? ''} —{' '}
|
||||||
|
{order.participant_email} — {formatEur(Number(order.total_price_eur))} —{' '}
|
||||||
|
{order.payment_status === 'paid' ? 'Betalt' : 'Ubetalt'}
|
||||||
|
</button>
|
||||||
|
{openOrderIds.has(order.id) && (
|
||||||
|
<div className="accordion-body">
|
||||||
|
{order.payment_status === 'paid' ? (
|
||||||
|
<p className="hint">
|
||||||
|
Betalt {order.paid_at ? new Date(order.paid_at).toLocaleString('da-DK') : ''}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<button onClick={() => handleMarkPaid(order)}>Markér betalt</button>
|
||||||
|
)}
|
||||||
|
{groupOrderLinesByCategory(order.order_lines).map(([categoryName, lines]) => (
|
||||||
|
<table key={categoryName}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colSpan={3}>{categoryName}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{lines.map((line) => (
|
||||||
|
<tr key={line.id}>
|
||||||
|
<td>{line.quantity}</td>
|
||||||
|
<td>{line.wine_name}</td>
|
||||||
|
<td>{formatEur(Number(line.unit_price))}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -318,6 +318,9 @@ export function RoundDetailPage() {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">Gem</button>
|
<button type="submit">Gem</button>
|
||||||
|
<button type="button" onClick={() => navigate(`/runder/${roundId}/ordrer`)}>
|
||||||
|
Se ordrer
|
||||||
|
</button>
|
||||||
<button type="button" onClick={handleCopy}>
|
<button type="button" onClick={handleCopy}>
|
||||||
Kopiér denne runde
|
Kopiér denne runde
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,7 @@ export function RoundsPage() {
|
||||||
<td>{formatDateTime(round.pickup_at)}</td>
|
<td>{formatDateTime(round.pickup_at)}</td>
|
||||||
<td>
|
<td>
|
||||||
<button onClick={() => navigate(`/runder/${round.id}`)}>Redigér</button>
|
<button onClick={() => navigate(`/runder/${round.id}`)}>Redigér</button>
|
||||||
|
<button onClick={() => navigate(`/runder/${round.id}/ordrer`)}>Se ordrer</button>
|
||||||
<button onClick={() => handleCopy(round)}>Kopiér</button>
|
<button onClick={() => handleCopy(round)}>Kopiér</button>
|
||||||
{(round.status === 'draft' ||
|
{(round.status === 'draft' ||
|
||||||
(round.status === 'closed' && currentUser?.is_superadmin && isElevated)) && (
|
(round.status === 'closed' && currentUser?.is_superadmin && isElevated)) && (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue