Compare commits

...

5 Commits

Author SHA1 Message Date
Simon Gruber e489b045fc Remove admin view pagination
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 2m12s
2026-05-07 07:45:55 +02:00
Simon Gruber b00cde2a6e UI adjustments 2026-05-06 08:07:08 +02:00
Simon Gruber bfa04936b2 Retain mailpit data 2026-05-06 08:06:47 +02:00
Simon Gruber 24fd13340e Add owner chip for unpaid submissions 2026-05-06 07:34:41 +02:00
Simon Gruber d037f1ded0 Fix description export 2026-05-06 07:32:49 +02:00
9 changed files with 73 additions and 76 deletions
+6 -1
View File
@@ -130,7 +130,7 @@ def row_to_my_order(row: sqlite3.Row) -> dict:
except json.JSONDecodeError: except json.JSONDecodeError:
submission_choices = {} submission_choices = {}
return { result = {
"id": row["id"], "id": row["id"],
"title": row["title"], "title": row["title"],
"description": row["description"], "description": row["description"],
@@ -145,3 +145,8 @@ def row_to_my_order(row: sqlite3.Row) -> dict:
"paid": bool(row["paid"]) if row["paid"] is not None else False, "paid": bool(row["paid"]) if row["paid"] is not None else False,
}, },
} }
if "has_unpaid_submissions" in row.keys() and row["has_unpaid_submissions"] is not None:
result["has_unpaid_submissions"] = bool(row["has_unpaid_submissions"])
return result
+6 -1
View File
@@ -674,7 +674,12 @@ def get_my_orders(
uot.submission_token, uot.submission_token,
s.choices_json, s.choices_json,
s.accepted, s.accepted,
s.paid s.paid,
CASE
WHEN uot.admin_token IS NOT NULL THEN
(SELECT COUNT(*) FROM submissions s2 WHERE s2.group_order_id = go.id AND s2.accepted = 1 AND (s2.paid IS NULL OR s2.paid = 0))
ELSE 0
END AS has_unpaid_submissions
FROM user_order_tokens uot FROM user_order_tokens uot
JOIN group_orders go ON go.id = uot.group_order_id JOIN group_orders go ON go.id = uot.group_order_id
LEFT JOIN submissions s ON s.submission_token = uot.submission_token LEFT JOIN submissions s ON s.submission_token = uot.submission_token
+7 -2
View File
@@ -36,15 +36,20 @@ services:
container_name: lunchtime-mailpit container_name: lunchtime-mailpit
ports: ports:
- "8025:8025" - "8025:8025"
volumes:
- mailpit-data:/data
restart: unless-stopped restart: unless-stopped
nginx: nginx:
image: nginx:alpine image: nginx:alpine
ports: ports:
- "8080:8080" - "8020:8080"
volumes: volumes:
- ../nginx.conf:/etc/nginx/conf.d/default.conf:ro - ../nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on: depends_on:
- backend - backend
- frontend - frontend
restart: unless-stopped restart: unless-stopped
volumes:
mailpit-data:
@@ -6,6 +6,7 @@ import ExportSelectionModal, { type ExportSelectionState } from "../modals/Expor
type MenuConfigImportExportProps = { type MenuConfigImportExportProps = {
config?: OrderFormConfig | null; config?: OrderFormConfig | null;
description?: string;
onImportConfig?: (config: OrderFormConfig) => void | Promise<void>; onImportConfig?: (config: OrderFormConfig) => void | Promise<void>;
showImport?: boolean; showImport?: boolean;
showExport?: boolean; showExport?: boolean;
@@ -23,6 +24,7 @@ function normalizeConfig(value: unknown): OrderFormConfig {
export default function MenuConfigImportExport({ export default function MenuConfigImportExport({
config, config,
description,
onImportConfig, onImportConfig,
showImport = true, showImport = true,
showExport = true, showExport = true,
@@ -98,8 +100,8 @@ export default function MenuConfigImportExport({
payload.title = fileNameBase || ""; payload.title = fileNameBase || "";
} }
if (exportSelection.description) { if (exportSelection.description || description) {
payload.description = ""; payload.description = description || "";
} }
if (exportSelection.menu) { if (exportSelection.menu) {
@@ -76,6 +76,7 @@ export default function ReadOnlyOrderOverviewCard({
extra={ extra={
<MenuConfigImportExport <MenuConfigImportExport
config={currentConfig} config={currentConfig}
description={currentDescription}
showImport={false} showImport={false}
onImportConfig={onImportConfig} onImportConfig={onImportConfig}
fileNameBase={order.title || "order"} fileNameBase={order.title || "order"}
@@ -51,17 +51,13 @@ export default function AdminSubmissionsCard({
onOpenMailto, onOpenMailto,
selectedRowKeys, selectedRowKeys,
onSelectedRowKeysChange, onSelectedRowKeysChange,
pagedSubmissions, submissions,
deletingId, deletingId,
savingStatusKey, savingStatusKey,
onDeleteSubmission, onDeleteSubmission,
onUpdateSubmissionStatus, onUpdateSubmissionStatus,
totalEstimatedText, totalEstimatedText,
selectedCount, selectedCount,
submissionsPage,
pageSize,
totalCount,
onSubmissionsPageChange,
}: { }: {
searchQuery: string; searchQuery: string;
selectedSubmissionState: "all" | "pending" | "unpaid" | "paid"; selectedSubmissionState: "all" | "pending" | "unpaid" | "paid";
@@ -74,17 +70,13 @@ export default function AdminSubmissionsCard({
onOpenMailto: () => void; onOpenMailto: () => void;
selectedRowKeys: Array<string | number>; selectedRowKeys: Array<string | number>;
onSelectedRowKeysChange: (keys: Array<string | number>) => void; onSelectedRowKeysChange: (keys: Array<string | number>) => void;
pagedSubmissions: any[]; submissions: any[];
deletingId: string | number | null; deletingId: string | number | null;
savingStatusKey: string | null; savingStatusKey: string | null;
onDeleteSubmission: (id: string | number) => void; onDeleteSubmission: (id: string | number) => void;
onUpdateSubmissionStatus: (submission: any, changes: { accepted?: boolean; paid?: boolean }) => void; onUpdateSubmissionStatus: (submission: any, changes: { accepted?: boolean; paid?: boolean }) => void;
totalEstimatedText: string | null; totalEstimatedText: string | null;
selectedCount: number; selectedCount: number;
submissionsPage: number;
pageSize: number;
totalCount: number;
onSubmissionsPageChange: (page: number) => void;
}) { }) {
const columns = [ const columns = [
{ {
@@ -224,14 +216,14 @@ export default function AdminSubmissionsCard({
<Table <Table
rowKey="id" rowKey="id"
columns={columns} columns={columns}
dataSource={pagedSubmissions} dataSource={submissions}
rowSelection={{ rowSelection={{
selectedRowKeys, selectedRowKeys,
onChange: (nextSelectedRowKeys) => onChange: (nextSelectedRowKeys) =>
onSelectedRowKeysChange(nextSelectedRowKeys as Array<string | number>), onSelectedRowKeysChange(nextSelectedRowKeys as Array<string | number>),
}} }}
pagination={false} pagination={false}
scroll={{ x: true }} scroll={{ y: 400, x: true }}
/> />
<Flex <Flex
justify="space-between" justify="space-between"
@@ -249,14 +241,6 @@ export default function AdminSubmissionsCard({
) : ( ) : (
<span /> <span />
)} )}
<Pagination
current={submissionsPage}
pageSize={pageSize}
total={totalCount}
showSizeChanger={false}
hideOnSinglePage
onChange={onSubmissionsPageChange}
/>
</Flex> </Flex>
</Card> </Card>
); );
@@ -92,33 +92,52 @@ export default function HomeOrdersTable({
const isClosed = !!item.closed; const isClosed = !!item.closed;
const closedStr = isClosed ? " Closed" : "Open"; const closedStr = isClosed ? " Closed" : "Open";
const dateStr = const dateStr = item.created_at
item.created_at ? new Date(item.created_at).toLocaleString()
? new Date(item.created_at).toLocaleString() : "Unknown creation time";
: "Unknown creation time";
return ( return (
<Space size={16}> <Space size={16}>
<Icon path={mdiFood} size={1} style={getOrderIconStyle(isClosed)} /> <Icon
path={mdiFood}
size={1}
style={getOrderIconStyle(isClosed)}
/>
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<Space size={8} align="center"> <Space size={8} align="center">
<Text strong>{item.title || item.id}</Text> <Text strong>{item.title || item.id}</Text>
{item.is_owner ? <Tag color="geekblue">Owner</Tag> : null} <Space size={0} align="center">
{state === "paid" ? <Tag color="green">Paid</Tag> : null} {item.is_owner ? <Tag color="geekblue">Owner</Tag> : null}
{state === "unpaid" ? ( {item.is_owner && item.has_unpaid_submissions ? (
<Tag color="volcano">Unpaid</Tag> <Tag color="volcano">Open payments</Tag>
) : null} ) : null}
{state === "pending" ? <Tag color="blue">Pending</Tag> : null} {state === "paid" ? <Tag color="green">Paid</Tag> : null}
{state === "unpaid" ? (
<Tag color="volcano">Unpaid</Tag>
) : null}
{state === "pending" ? (
<Tag color="blue">Pending</Tag>
) : null}
</Space>
</Space> </Space>
<Text type="secondary"> <Text type="secondary">
{<Tooltip title={`Order is ${closedStr.toLocaleLowerCase()}`}>{closedStr}</Tooltip>}{" • "} {
{<Tooltip title={`Created at: ${dateStr}`}>{dateStr}</Tooltip>} <Tooltip
title={`Order is ${closedStr.toLocaleLowerCase()}`}
>
{closedStr}
</Tooltip>
}
{" • "}
{
<Tooltip title={`Created at: ${dateStr}`}>
{dateStr}
</Tooltip>
}
{item.is_participant && formatted && ( {item.is_participant && formatted && (
<> <>
{" • "} {" • "}
<Tooltip title="Your order"> <Tooltip title="Your order">{formatted}</Tooltip>
{formatted}
</Tooltip>
</> </>
)} )}
</Text> </Text>
+1
View File
@@ -73,6 +73,7 @@ export type Order = OrderBase & {
is_owner: boolean; is_owner: boolean;
is_participant: boolean; is_participant: boolean;
}; };
has_unpaid_submissions?: boolean;
}; };
export type OrderAdminView = OrderBase & { export type OrderAdminView = OrderBase & {
+9 -34
View File
@@ -59,10 +59,8 @@ export default function AdminView({ orderId }: { orderId: string }) {
const [savingStatusKey, setSavingStatusKey] = useState<string | null>(null); const [savingStatusKey, setSavingStatusKey] = useState<string | null>(null);
const [updatingOrderStatus, setUpdatingOrderStatus] = useState(false); const [updatingOrderStatus, setUpdatingOrderStatus] = useState(false);
const [deletingOrder, setDeletingOrder] = useState(false); const [deletingOrder, setDeletingOrder] = useState(false);
const [submissionsPage, setSubmissionsPage] = useState(1);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [selectedSubmissionState, setSelectedSubmissionState] = useState<"all" | "pending" | "unpaid" | "paid">("all"); const [selectedSubmissionState, setSelectedSubmissionState] = useState<"all" | "pending" | "unpaid" | "paid">("all");
const SUBMISSIONS_PAGE_SIZE = 8;
const getSubmissionState = (submission: Submission): SubmissionStatus => { const getSubmissionState = (submission: Submission): SubmissionStatus => {
if (submission?.paid) { if (submission?.paid) {
@@ -263,17 +261,6 @@ export default function AdminView({ orderId }: { orderId: string }) {
}; };
}, [refreshSubmissions]); }, [refreshSubmissions]);
useEffect(() => {
if (!data) {
return;
}
const maxPage = Math.max(1, Math.ceil(data.submissions.length / SUBMISSIONS_PAGE_SIZE));
if (submissionsPage > maxPage) {
setSubmissionsPage(maxPage);
}
}, [data, submissionsPage]);
const filteredSubmissions = useMemo(() => { const filteredSubmissions = useMemo(() => {
if (!data?.submissions) { if (!data?.submissions) {
return []; return [];
@@ -312,10 +299,6 @@ export default function AdminView({ orderId }: { orderId: string }) {
setSelectedRowKeys((previous) => previous.filter((id) => visibleIds.has(id))); setSelectedRowKeys((previous) => previous.filter((id) => visibleIds.has(id)));
}, [filteredSubmissions]); }, [filteredSubmissions]);
useEffect(() => {
setSubmissionsPage(1);
}, [searchQuery, selectedSubmissionState]);
const selected = filteredSubmissions.filter((submission: any) => const selected = filteredSubmissions.filter((submission: any) =>
selectedRowKeys.includes(submission.id), selectedRowKeys.includes(submission.id),
); );
@@ -357,10 +340,6 @@ export default function AdminView({ orderId }: { orderId: string }) {
const totalEstimatedText = hasAnyEstimatedValue const totalEstimatedText = hasAnyEstimatedValue
? formatEstimatedTotal(totalEstimatedValue) ? formatEstimatedTotal(totalEstimatedValue)
: null; : null;
const pagedSubmissions = filteredSubmissions.slice(
(submissionsPage - 1) * SUBMISSIONS_PAGE_SIZE,
submissionsPage * SUBMISSIONS_PAGE_SIZE,
);
if (pageError || loading) { if (pageError || loading) {
return ( return (
@@ -417,6 +396,14 @@ export default function AdminView({ orderId }: { orderId: string }) {
onImportConfig={handleImportConfig} onImportConfig={handleImportConfig}
/> />
<AdminControlCenterCard
isClosed={!!data?.closed}
updatingOrderStatus={updatingOrderStatus}
deletingOrder={deletingOrder}
onToggleClosed={updateOrderClosedStatus}
onDeleteOrder={deleteOrder}
/>
<AdminSubmissionsCard <AdminSubmissionsCard
searchQuery={searchQuery} searchQuery={searchQuery}
selectedSubmissionState={selectedSubmissionState} selectedSubmissionState={selectedSubmissionState}
@@ -429,25 +416,13 @@ export default function AdminView({ orderId }: { orderId: string }) {
onOpenMailto={openSubmissionEmailDraft} onOpenMailto={openSubmissionEmailDraft}
selectedRowKeys={selectedRowKeys} selectedRowKeys={selectedRowKeys}
onSelectedRowKeysChange={setSelectedRowKeys} onSelectedRowKeysChange={setSelectedRowKeys}
pagedSubmissions={pagedSubmissions} submissions={filteredSubmissions}
deletingId={deletingId} deletingId={deletingId}
savingStatusKey={savingStatusKey} savingStatusKey={savingStatusKey}
onDeleteSubmission={deleteAsAdmin} onDeleteSubmission={deleteAsAdmin}
onUpdateSubmissionStatus={updateSubmissionStatus} onUpdateSubmissionStatus={updateSubmissionStatus}
totalEstimatedText={totalEstimatedText} totalEstimatedText={totalEstimatedText}
selectedCount={selected.length} selectedCount={selected.length}
submissionsPage={submissionsPage}
pageSize={SUBMISSIONS_PAGE_SIZE}
totalCount={filteredSubmissions.length}
onSubmissionsPageChange={setSubmissionsPage}
/>
<AdminControlCenterCard
isClosed={!!data?.closed}
updatingOrderStatus={updatingOrderStatus}
deletingOrder={deletingOrder}
onToggleClosed={updateOrderClosedStatus}
onDeleteOrder={deleteOrder}
/> />
</Space> </Space>
); );