Compare commits
8 Commits
2b42033c70
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ff82ec2ba | |||
| cacdb5b2bd | |||
| 7cf05d5801 | |||
| e489b045fc | |||
| b00cde2a6e | |||
| bfa04936b2 | |||
| 24fd13340e | |||
| d037f1ded0 |
@@ -130,7 +130,7 @@ def row_to_my_order(row: sqlite3.Row) -> dict:
|
||||
except json.JSONDecodeError:
|
||||
submission_choices = {}
|
||||
|
||||
return {
|
||||
result = {
|
||||
"id": row["id"],
|
||||
"title": row["title"],
|
||||
"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,
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -674,7 +674,12 @@ def get_my_orders(
|
||||
uot.submission_token,
|
||||
s.choices_json,
|
||||
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
|
||||
JOIN group_orders go ON go.id = uot.group_order_id
|
||||
LEFT JOIN submissions s ON s.submission_token = uot.submission_token
|
||||
|
||||
+7
-2
@@ -36,15 +36,20 @@ services:
|
||||
container_name: lunchtime-mailpit
|
||||
ports:
|
||||
- "8025:8025"
|
||||
volumes:
|
||||
- mailpit-data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8020:8080"
|
||||
volumes:
|
||||
- ../nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- backend
|
||||
- frontend
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
mailpit-data:
|
||||
@@ -6,6 +6,7 @@ import ExportSelectionModal, { type ExportSelectionState } from "../modals/Expor
|
||||
|
||||
type MenuConfigImportExportProps = {
|
||||
config?: OrderFormConfig | null;
|
||||
description?: string;
|
||||
onImportConfig?: (config: OrderFormConfig) => void | Promise<void>;
|
||||
showImport?: boolean;
|
||||
showExport?: boolean;
|
||||
@@ -23,6 +24,7 @@ function normalizeConfig(value: unknown): OrderFormConfig {
|
||||
|
||||
export default function MenuConfigImportExport({
|
||||
config,
|
||||
description,
|
||||
onImportConfig,
|
||||
showImport = true,
|
||||
showExport = true,
|
||||
@@ -98,8 +100,8 @@ export default function MenuConfigImportExport({
|
||||
payload.title = fileNameBase || "";
|
||||
}
|
||||
|
||||
if (exportSelection.description) {
|
||||
payload.description = "";
|
||||
if (exportSelection.description || description) {
|
||||
payload.description = description || "";
|
||||
}
|
||||
|
||||
if (exportSelection.menu) {
|
||||
|
||||
@@ -76,6 +76,7 @@ export default function ReadOnlyOrderOverviewCard({
|
||||
extra={
|
||||
<MenuConfigImportExport
|
||||
config={currentConfig}
|
||||
description={currentDescription}
|
||||
showImport={false}
|
||||
onImportConfig={onImportConfig}
|
||||
fileNameBase={order.title || "order"}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import React from "react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import {
|
||||
CloseOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
MailOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
Flex,
|
||||
Grid,
|
||||
Input,
|
||||
Pagination,
|
||||
List,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
@@ -42,8 +47,10 @@ function getEstimatedTotalText(rawValue: unknown): string | null {
|
||||
export default function AdminSubmissionsCard({
|
||||
searchQuery,
|
||||
selectedSubmissionState,
|
||||
sortBy,
|
||||
onSearchQueryChange,
|
||||
onSelectedSubmissionStateChange,
|
||||
onSortByChange,
|
||||
onReload,
|
||||
copyText,
|
||||
onCopy,
|
||||
@@ -51,22 +58,20 @@ export default function AdminSubmissionsCard({
|
||||
onOpenMailto,
|
||||
selectedRowKeys,
|
||||
onSelectedRowKeysChange,
|
||||
pagedSubmissions,
|
||||
submissions,
|
||||
deletingId,
|
||||
savingStatusKey,
|
||||
onDeleteSubmission,
|
||||
onUpdateSubmissionStatus,
|
||||
totalEstimatedText,
|
||||
selectedCount,
|
||||
submissionsPage,
|
||||
pageSize,
|
||||
totalCount,
|
||||
onSubmissionsPageChange,
|
||||
}: {
|
||||
searchQuery: string;
|
||||
selectedSubmissionState: "all" | "pending" | "unpaid" | "paid";
|
||||
sortBy: "time" | "order" | "email" | "amount";
|
||||
onSearchQueryChange: (value: string) => void;
|
||||
onSelectedSubmissionStateChange: (value: "all" | "pending" | "unpaid" | "paid") => void;
|
||||
onSortByChange: (value: "time" | "order" | "email" | "amount") => void;
|
||||
onReload: () => void;
|
||||
copyText: string;
|
||||
onCopy: (text: string, didCopy: boolean) => void;
|
||||
@@ -74,18 +79,74 @@ export default function AdminSubmissionsCard({
|
||||
onOpenMailto: () => void;
|
||||
selectedRowKeys: Array<string | number>;
|
||||
onSelectedRowKeysChange: (keys: Array<string | number>) => void;
|
||||
pagedSubmissions: any[];
|
||||
submissions: any[];
|
||||
deletingId: string | number | null;
|
||||
savingStatusKey: string | null;
|
||||
onDeleteSubmission: (id: string | number) => void;
|
||||
onDeleteSubmission: (ids: Array<string | number>) => void;
|
||||
onUpdateSubmissionStatus: (submission: any, changes: { accepted?: boolean; paid?: boolean }) => void;
|
||||
totalEstimatedText: string | null;
|
||||
selectedCount: number;
|
||||
submissionsPage: number;
|
||||
pageSize: number;
|
||||
totalCount: number;
|
||||
onSubmissionsPageChange: (page: number) => void;
|
||||
}) {
|
||||
const hasSelection = selectedRowKeys.length > 0;
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isMobile = !screens.md;
|
||||
|
||||
const actionMenuItems = [
|
||||
{
|
||||
key: "copy",
|
||||
label: (
|
||||
<CopyToClipboard text={copyText} onCopy={onCopy}>
|
||||
<div style={{ width: "100%" }}>
|
||||
<CopyOutlined style={{ marginRight: 8 }} />
|
||||
{hasSelection ? "Copy (selected)" : "Copy"}
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "email",
|
||||
label: (
|
||||
<a href={mailtoHref || undefined} onClick={onOpenMailto}>
|
||||
<MailOutlined style={{ marginRight: 8 }} />
|
||||
{hasSelection ? "Email (selected)" : "Email"}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
...(hasSelection
|
||||
? [
|
||||
{
|
||||
key: "clear",
|
||||
label: (
|
||||
<div
|
||||
onClick={() => onSelectedRowKeysChange([])}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<CloseOutlined style={{ marginRight: 8 }} />
|
||||
Clear selection
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={`Delete ${selectedRowKeys.length} submission${selectedRowKeys.length > 1 ? "s" : ""}?`}
|
||||
description="This cannot be undone."
|
||||
okText="Delete"
|
||||
okButtonProps={{ danger: true, loading: deletingId === "multiple" }}
|
||||
onConfirm={() => onDeleteSubmission(selectedRowKeys)}
|
||||
>
|
||||
<div style={{ color: "#ff4d4f", width: "100%" }}>
|
||||
<DeleteOutlined style={{ marginRight: 8 }} />
|
||||
Delete Selected
|
||||
</div>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "Submission",
|
||||
@@ -137,29 +198,6 @@ export default function AdminSubmissionsCard({
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
key: "actions",
|
||||
width: 1,
|
||||
render: (_: any, record: any) => (
|
||||
<Popconfirm
|
||||
title="Delete this submission?"
|
||||
description="This cannot be undone."
|
||||
okText="Delete"
|
||||
okButtonProps={{ danger: true, loading: deletingId === record.id }}
|
||||
onConfirm={() => onDeleteSubmission(record.id)}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
loading={deletingId === record.id}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -167,72 +205,125 @@ export default function AdminSubmissionsCard({
|
||||
title="Submissions"
|
||||
extra={
|
||||
<Space>
|
||||
<Tooltip
|
||||
title={
|
||||
selectedRowKeys.length > 0
|
||||
? "Copy selected submissions as list to clipboard"
|
||||
: "Copy submissions as list to clipboard"
|
||||
}
|
||||
>
|
||||
<CopyToClipboard text={copyText} onCopy={onCopy}>
|
||||
<Button icon={<CopyOutlined />}>
|
||||
{selectedRowKeys.length > 0 ? "Copy (selected)" : "Copy"}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
selectedRowKeys.length > 0
|
||||
? "Create draft email with selected submissions"
|
||||
: "Create draft email with submissions"
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon={<MailOutlined />}
|
||||
href={mailtoHref || undefined}
|
||||
onClick={onOpenMailto}
|
||||
>
|
||||
{selectedRowKeys.length > 0 ? "Email (selected)" : "Email"}
|
||||
<Dropdown menu={{ items: actionMenuItems }}>
|
||||
<Button>
|
||||
Actions <DownOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
<Tooltip title="Reload latest order details">
|
||||
<Button icon={<ReloadOutlined />} onClick={onReload} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space style={{ marginBottom: 12 }} wrap>
|
||||
<Flex style={{ marginBottom: 12, width: "100%" }} gap={8} wrap>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="Filter by email or order text"
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="Search"
|
||||
value={searchQuery}
|
||||
onChange={(event) => onSearchQueryChange(event.target.value)}
|
||||
style={{ minWidth: 260 }}
|
||||
style={{ flex: isMobile ? "1 1 100%" : "3 1 0%" }}
|
||||
/>
|
||||
<Select
|
||||
value={selectedSubmissionState}
|
||||
style={{ minWidth: 180 }}
|
||||
style={{ flex: isMobile ? "1 1 calc(50% - 4px)" : "1 1 0%" }}
|
||||
options={[
|
||||
{ label: "All statuses", value: "all" },
|
||||
{ label: "Pending", value: "pending" },
|
||||
{ label: "Unpaid", value: "unpaid" },
|
||||
{ label: "Paid", value: "paid" },
|
||||
{ label: "Show all", value: "all" },
|
||||
{ label: "Show pending", value: "pending" },
|
||||
{ label: "Show unpaid", value: "unpaid" },
|
||||
{ label: "Show paid", value: "paid" },
|
||||
]}
|
||||
onChange={(value) => onSelectedSubmissionStateChange(value)}
|
||||
/>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={pagedSubmissions}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (nextSelectedRowKeys) =>
|
||||
onSelectedRowKeysChange(nextSelectedRowKeys as Array<string | number>),
|
||||
}}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
/>
|
||||
<Select
|
||||
value={sortBy}
|
||||
style={{ flex: isMobile ? "1 1 calc(50% - 4px)" : "1 1 0%" }}
|
||||
options={[
|
||||
{ label: "by time", value: "time" },
|
||||
{ label: "by order", value: "order" },
|
||||
{ label: "by email", value: "email" },
|
||||
{ label: "by amount", value: "amount" },
|
||||
]}
|
||||
onChange={(value) => onSortByChange(value)}
|
||||
/>
|
||||
</Flex>
|
||||
{isMobile ? (
|
||||
<List
|
||||
dataSource={submissions}
|
||||
renderItem={(record) => {
|
||||
const cleanValue = stripPriceDecorations(String(record.formatted_string || "")).trim();
|
||||
const estimateText = getEstimatedTotalText(record.estimated_total);
|
||||
const emailText = String(record.email || "").trim() || "-";
|
||||
const secondaryText = `${emailText} • ${estimateText || "-"}`;
|
||||
const isAcceptedSaving = savingStatusKey === `${record.id}:accepted`;
|
||||
const isPaidSaving = savingStatusKey === `${record.id}:paid`;
|
||||
const isSelected = selectedRowKeys.includes(record.id);
|
||||
|
||||
return (
|
||||
<List.Item>
|
||||
<Flex gap={12} align="flex-start" style={{ width: "100%" }}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
const nextKeys = e.target.checked
|
||||
? [...selectedRowKeys, record.id]
|
||||
: selectedRowKeys.filter((k) => k !== record.id);
|
||||
onSelectedRowKeysChange(nextKeys);
|
||||
}}
|
||||
/>
|
||||
<Space direction="vertical" size={8} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text code copyable={{ text: cleanValue }} style={{ whiteSpace: "normal", wordBreak: "break-word" }}>
|
||||
{cleanValue || "-"}
|
||||
</Text>
|
||||
<Tooltip title="Rough estimate based on menu prices. Final amount may differ.">
|
||||
<Text type="secondary">{secondaryText}</Text>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
<Space size={16} wrap>
|
||||
<Space size={8}>
|
||||
<Text>Accepted</Text>
|
||||
<Switch
|
||||
checked={!!record.accepted}
|
||||
loading={isAcceptedSaving}
|
||||
onChange={(checked) => onUpdateSubmissionStatus(record, { accepted: checked })}
|
||||
/>
|
||||
</Space>
|
||||
<Space size={8}>
|
||||
<Text>Paid</Text>
|
||||
<Switch
|
||||
checked={!!record.paid}
|
||||
loading={isPaidSaving}
|
||||
onChange={(checked) => onUpdateSubmissionStatus(record, { paid: checked })}
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
</Space>
|
||||
</Flex>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
style={{
|
||||
maxHeight: "66vh",
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden"
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={submissions}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (nextSelectedRowKeys) =>
|
||||
onSelectedRowKeysChange(nextSelectedRowKeys as Array<string | number>),
|
||||
}}
|
||||
pagination={false}
|
||||
scroll={{ y: 400, x: true }}
|
||||
/>
|
||||
)}
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
@@ -249,14 +340,6 @@ export default function AdminSubmissionsCard({
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<Pagination
|
||||
current={submissionsPage}
|
||||
pageSize={pageSize}
|
||||
total={totalCount}
|
||||
showSizeChanger={false}
|
||||
hideOnSinglePage
|
||||
onChange={onSubmissionsPageChange}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -92,33 +92,52 @@ export default function HomeOrdersTable({
|
||||
const isClosed = !!item.closed;
|
||||
const closedStr = isClosed ? " Closed" : "Open";
|
||||
|
||||
const dateStr =
|
||||
item.created_at
|
||||
? new Date(item.created_at).toLocaleString()
|
||||
: "Unknown creation time";
|
||||
const dateStr = item.created_at
|
||||
? new Date(item.created_at).toLocaleString()
|
||||
: "Unknown creation time";
|
||||
|
||||
return (
|
||||
<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 size={8} align="center">
|
||||
<Space size={8} align="center" wrap>
|
||||
<Text strong>{item.title || item.id}</Text>
|
||||
{item.is_owner ? <Tag color="geekblue">Owner</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 size={0} align="center" wrap>
|
||||
{item.is_owner ? <Tag color="geekblue">Owner</Tag> : null}
|
||||
{item.is_owner && item.has_unpaid_submissions ? (
|
||||
<Tag color="volcano">Open payments</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>
|
||||
<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 && (
|
||||
<>
|
||||
{" • "}
|
||||
<Tooltip title="Your order">
|
||||
{formatted}
|
||||
</Tooltip>
|
||||
<Tooltip title="Your order">{formatted}</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
@@ -73,6 +73,7 @@ export type Order = OrderBase & {
|
||||
is_owner: boolean;
|
||||
is_participant: boolean;
|
||||
};
|
||||
has_unpaid_submissions?: boolean;
|
||||
};
|
||||
|
||||
export type OrderAdminView = OrderBase & {
|
||||
|
||||
@@ -59,10 +59,9 @@ export default function AdminView({ orderId }: { orderId: string }) {
|
||||
const [savingStatusKey, setSavingStatusKey] = useState<string | null>(null);
|
||||
const [updatingOrderStatus, setUpdatingOrderStatus] = useState(false);
|
||||
const [deletingOrder, setDeletingOrder] = useState(false);
|
||||
const [submissionsPage, setSubmissionsPage] = useState(1);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedSubmissionState, setSelectedSubmissionState] = useState<"all" | "pending" | "unpaid" | "paid">("all");
|
||||
const SUBMISSIONS_PAGE_SIZE = 8;
|
||||
const [sortBy, setSortBy] = useState<"time" | "order" | "email" | "amount">("time");
|
||||
|
||||
const getSubmissionState = (submission: Submission): SubmissionStatus => {
|
||||
if (submission?.paid) {
|
||||
@@ -119,11 +118,14 @@ export default function AdminView({ orderId }: { orderId: string }) {
|
||||
},
|
||||
});
|
||||
|
||||
const deleteAsAdmin = async (submissionId: string | number) => {
|
||||
setDeletingId(submissionId);
|
||||
const deleteAsAdmin = async (submissionIds: (string | number)[]) => {
|
||||
setDeletingId(submissionIds.length === 1 ? submissionIds[0] : "multiple");
|
||||
try {
|
||||
await apiService.submissions.removeAsAdmin(orderId, submissionId);
|
||||
message.success("Submission deleted");
|
||||
for (const id of submissionIds) {
|
||||
await apiService.submissions.removeAsAdmin(orderId, id);
|
||||
}
|
||||
message.success(`Submission${submissionIds.length > 1 ? "s" : ""} deleted`);
|
||||
setSelectedRowKeys((prev) => prev.filter(k => !submissionIds.includes(k)));
|
||||
await loadAdmin();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
@@ -263,17 +265,6 @@ export default function AdminView({ orderId }: { orderId: string }) {
|
||||
};
|
||||
}, [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(() => {
|
||||
if (!data?.submissions) {
|
||||
return [];
|
||||
@@ -307,19 +298,45 @@ export default function AdminView({ orderId }: { orderId: string }) {
|
||||
});
|
||||
}, [data?.submissions, searchQuery, selectedSubmissionState]);
|
||||
|
||||
const sortedAndFilteredSubmissions = useMemo(() => {
|
||||
const list = [...filteredSubmissions];
|
||||
list.sort((a: any, b: any) => {
|
||||
switch (sortBy) {
|
||||
case "order": {
|
||||
const aText = stripPriceDecorations(String(a.formatted_string || "")).toLowerCase();
|
||||
const bText = stripPriceDecorations(String(b.formatted_string || "")).toLowerCase();
|
||||
return aText.localeCompare(bText);
|
||||
}
|
||||
case "email": {
|
||||
const aEmail = String(a.email || "").toLowerCase();
|
||||
const bEmail = String(b.email || "").toLowerCase();
|
||||
return aEmail.localeCompare(bEmail);
|
||||
}
|
||||
case "amount": {
|
||||
const aAmount = getEstimatedTotalNumber(a.estimated_total) || 0;
|
||||
const bAmount = getEstimatedTotalNumber(b.estimated_total) || 0;
|
||||
return bAmount - aAmount; // sort highest amount first
|
||||
}
|
||||
case "time":
|
||||
default: {
|
||||
const aTime = new Date(a.created_at || 0).getTime();
|
||||
const bTime = new Date(b.created_at || 0).getTime();
|
||||
return aTime - bTime; // oldest first
|
||||
}
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}, [filteredSubmissions, sortBy]);
|
||||
|
||||
useEffect(() => {
|
||||
const visibleIds = new Set(filteredSubmissions.map((submission: any) => submission.id));
|
||||
const visibleIds = new Set(sortedAndFilteredSubmissions.map((submission: any) => submission.id));
|
||||
setSelectedRowKeys((previous) => previous.filter((id) => visibleIds.has(id)));
|
||||
}, [filteredSubmissions]);
|
||||
}, [sortedAndFilteredSubmissions]);
|
||||
|
||||
useEffect(() => {
|
||||
setSubmissionsPage(1);
|
||||
}, [searchQuery, selectedSubmissionState]);
|
||||
|
||||
const selected = filteredSubmissions.filter((submission: any) =>
|
||||
const selected = sortedAndFilteredSubmissions.filter((submission: any) =>
|
||||
selectedRowKeys.includes(submission.id),
|
||||
);
|
||||
const rowsToCopy = selected.length > 0 ? selected : filteredSubmissions;
|
||||
const rowsToCopy = selected.length > 0 ? selected : sortedAndFilteredSubmissions;
|
||||
const copyText = rowsToCopy
|
||||
.map((submission: Submission) => getCleanSubmissionText(submission))
|
||||
.filter(Boolean)
|
||||
@@ -357,10 +374,6 @@ export default function AdminView({ orderId }: { orderId: string }) {
|
||||
const totalEstimatedText = hasAnyEstimatedValue
|
||||
? formatEstimatedTotal(totalEstimatedValue)
|
||||
: null;
|
||||
const pagedSubmissions = filteredSubmissions.slice(
|
||||
(submissionsPage - 1) * SUBMISSIONS_PAGE_SIZE,
|
||||
submissionsPage * SUBMISSIONS_PAGE_SIZE,
|
||||
);
|
||||
|
||||
if (pageError || loading) {
|
||||
return (
|
||||
@@ -417,31 +430,6 @@ export default function AdminView({ orderId }: { orderId: string }) {
|
||||
onImportConfig={handleImportConfig}
|
||||
/>
|
||||
|
||||
<AdminSubmissionsCard
|
||||
searchQuery={searchQuery}
|
||||
selectedSubmissionState={selectedSubmissionState}
|
||||
onSearchQueryChange={setSearchQuery}
|
||||
onSelectedSubmissionStateChange={setSelectedSubmissionState}
|
||||
onReload={refreshSubmissions}
|
||||
copyText={copyText}
|
||||
onCopy={copySubmissionList}
|
||||
mailtoHref={mailtoHref}
|
||||
onOpenMailto={openSubmissionEmailDraft}
|
||||
selectedRowKeys={selectedRowKeys}
|
||||
onSelectedRowKeysChange={setSelectedRowKeys}
|
||||
pagedSubmissions={pagedSubmissions}
|
||||
deletingId={deletingId}
|
||||
savingStatusKey={savingStatusKey}
|
||||
onDeleteSubmission={deleteAsAdmin}
|
||||
onUpdateSubmissionStatus={updateSubmissionStatus}
|
||||
totalEstimatedText={totalEstimatedText}
|
||||
selectedCount={selected.length}
|
||||
submissionsPage={submissionsPage}
|
||||
pageSize={SUBMISSIONS_PAGE_SIZE}
|
||||
totalCount={filteredSubmissions.length}
|
||||
onSubmissionsPageChange={setSubmissionsPage}
|
||||
/>
|
||||
|
||||
<AdminControlCenterCard
|
||||
isClosed={!!data?.closed}
|
||||
updatingOrderStatus={updatingOrderStatus}
|
||||
@@ -449,6 +437,29 @@ export default function AdminView({ orderId }: { orderId: string }) {
|
||||
onToggleClosed={updateOrderClosedStatus}
|
||||
onDeleteOrder={deleteOrder}
|
||||
/>
|
||||
|
||||
<AdminSubmissionsCard
|
||||
searchQuery={searchQuery}
|
||||
selectedSubmissionState={selectedSubmissionState}
|
||||
sortBy={sortBy}
|
||||
onSearchQueryChange={setSearchQuery}
|
||||
onSelectedSubmissionStateChange={setSelectedSubmissionState}
|
||||
onSortByChange={setSortBy}
|
||||
onReload={refreshSubmissions}
|
||||
copyText={copyText}
|
||||
onCopy={copySubmissionList}
|
||||
mailtoHref={mailtoHref}
|
||||
onOpenMailto={openSubmissionEmailDraft}
|
||||
selectedRowKeys={selectedRowKeys}
|
||||
onSelectedRowKeysChange={setSelectedRowKeys}
|
||||
submissions={sortedAndFilteredSubmissions}
|
||||
deletingId={deletingId}
|
||||
savingStatusKey={savingStatusKey}
|
||||
onDeleteSubmission={deleteAsAdmin}
|
||||
onUpdateSubmissionStatus={updateSubmissionStatus}
|
||||
totalEstimatedText={totalEstimatedText}
|
||||
selectedCount={selected.length}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user