Skip to content

Commit 59e8b5a

Browse files
authored
Merge pull request #13 from icdocsoc/collection-security
Add syncing of data directly from eActivities
2 parents 2ee725d + a93e991 commit 59e8b5a

File tree

26 files changed

+1352
-38
lines changed

26 files changed

+1352
-38
lines changed

collection/app/(app)/CSVImport.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,16 @@ const CSVImportForm: React.FC<CSVImportFormProp> = ({
5353
const [formState, setFormState] = useState<StatusReturn>({
5454
status: "pending",
5555
});
56+
console.log(productsByAcademicYear);
5657
const form = useForm<CSVFormValues>({
5758
mode: "controlled",
5859
initialValues: {
60+
academicYear: academicYear,
5961
productId:
60-
productsByAcademicYear[academicYear][0].id.toString(10),
62+
productsByAcademicYear[academicYear]?.length > 0
63+
? productsByAcademicYear[academicYear][0].id.toString(10)
64+
: "", // sometimes no products are defined
6165
csv: [],
62-
academicYear: academicYear,
6366
},
6467
validate: {
6568
productId: (value: string) => {
@@ -141,7 +144,7 @@ const CSVImportForm: React.FC<CSVImportFormProp> = ({
141144
label="Product"
142145
name="productId"
143146
key={form.key("productId")}
144-
description="Product to import the CSV into (variants will be detected from the data automatically)"
147+
description="Product to import the CSV into (variants will be detected from the data automatically). Product not showing? Add it in the Products page."
145148
data={
146149
productsByAcademicYear[form.getValues().academicYear]?.map((item) => ({
147150
value: item.id.toString(10),
@@ -267,7 +270,7 @@ export const CSVImport = ({
267270
</Stack>
268271
</Modal>
269272

270-
<Button leftSection={<FaTable />} onClick={open}>
273+
<Button leftSection={<FaTable />} onClick={open} color="teal">
271274
Import data from CSV
272275
</Button>
273276
</>

collection/app/(app)/PageActions.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ import { Paper, MultiSelect, Group } from "@mantine/core";
33
import { UseFormReturnType } from "@mantine/form";
44

55
import { CSVImport } from "./CSVImport";
6+
import { SyncEActivities } from "./SyncEActivities";
67

78
export const PageActions = ({
89
formHook,
910
academicYears,
1011
currentAcademicYear,
12+
setActionsError,
1113
}: {
1214
formHook: UseFormReturnType<{ academicYears: AcademicYear[]; shortcode: string }>;
1315
academicYears: string[];
1416
currentAcademicYear: string;
17+
setActionsError: (error: string | null) => void;
1518
}) => {
1619
return (
1720
<Paper p="lg" withBorder>
@@ -23,10 +26,17 @@ export const PageActions = ({
2326
data={academicYears}
2427
{...formHook.getInputProps("academicYears")}
2528
/>
26-
<CSVImport
27-
academicYears={academicYears}
28-
currentAcademicYear={currentAcademicYear}
29-
/>
29+
<Group>
30+
<SyncEActivities
31+
setActionsError={setActionsError}
32+
academicYears={academicYears}
33+
currentlySelectedAcademicYears={formHook.getValues().academicYears}
34+
/>
35+
<CSVImport
36+
academicYears={academicYears}
37+
currentAcademicYear={currentAcademicYear}
38+
/>
39+
</Group>
3040
</Group>
3141
</Paper>
3242
);
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"use client";
2+
3+
import { loadSalesFromEActivites } from "@/lib/crud/loadSalesFromEActivites";
4+
import { fetcher } from "@/lib/fetcher";
5+
import { StatusReturn } from "@/lib/types";
6+
import { Product } from "@docsoc/eactivities";
7+
import {
8+
Alert,
9+
Box,
10+
Button,
11+
Checkbox,
12+
Group,
13+
Loader,
14+
Modal,
15+
MultiSelect,
16+
Stack,
17+
Text,
18+
} from "@mantine/core";
19+
import { useDisclosure } from "@mantine/hooks";
20+
import { RootItem } from "@prisma/client";
21+
import React, { useCallback, useState, useTransition } from "react";
22+
import { FaSync } from "react-icons/fa";
23+
import { FaUpRightFromSquare } from "react-icons/fa6";
24+
import useSWR from "swr";
25+
26+
function CheckboxesForProducts({
27+
close,
28+
academicYears,
29+
currentlySelectedAcademicYears,
30+
}: {
31+
close: () => void;
32+
academicYears: string[];
33+
currentlySelectedAcademicYears: string[];
34+
}) {
35+
const [value, setValue] = useState<string[]>([]);
36+
const [status, setStatus] = useState<StatusReturn>({
37+
status: "pending",
38+
});
39+
40+
const [academicYearsToSync, setAcademicYearsToSync] = useState(currentlySelectedAcademicYears);
41+
42+
const { data: products, isLoading } = useSWR<RootItem[]>("/api/products/syncable", fetcher);
43+
44+
const [isPending, startTransition] = useTransition();
45+
46+
const syncEActivities = useCallback(() => {
47+
setStatus({
48+
status: "pending",
49+
});
50+
startTransition(async () => {
51+
const res = await loadSalesFromEActivites(
52+
value.map((id) => parseInt(id, 10)).filter(isFinite),
53+
academicYearsToSync,
54+
);
55+
if (res.status === "error") {
56+
setStatus(res);
57+
} else {
58+
setStatus({
59+
status: "success",
60+
});
61+
close();
62+
}
63+
});
64+
}, [close, value, academicYearsToSync]);
65+
66+
const cards = products?.map((product, i) => (
67+
<Checkbox.Card radius="md" value={product.id.toString(10)} key={i} flex="1 0 0">
68+
<Group wrap="nowrap" align="center" p="md">
69+
<Checkbox.Indicator />
70+
<Group flex="1 0 0">
71+
<Box flex="1 0 0">
72+
<Text>
73+
<b>
74+
<u>{product.name}</u>
75+
</b>
76+
</Text>
77+
<Text>
78+
<b>eActivites ID:</b> {product.eActivitiesId}
79+
</Text>
80+
<Text>
81+
<b>eActivites Name:</b> {product.eActivitiesName}
82+
</Text>
83+
</Box>
84+
{isLoading ? (
85+
<Loader size="md" />
86+
) : (
87+
product.eActivitiesURL && (
88+
<Button
89+
component="a"
90+
href={product.eActivitiesURL}
91+
target="_blank"
92+
rightSection={<FaUpRightFromSquare />}
93+
>
94+
View on Union Shop
95+
</Button>
96+
)
97+
)}
98+
</Group>
99+
</Group>
100+
</Checkbox.Card>
101+
));
102+
103+
return (
104+
<Stack>
105+
{
106+
// Display error if there is one
107+
status.status === "error" && (
108+
<Alert color="red" title="Error">
109+
{status.error}
110+
</Alert>
111+
)
112+
}
113+
<MultiSelect
114+
label="Sync purchases these academic years"
115+
description=" "
116+
value={academicYearsToSync}
117+
onChange={(e) => setAcademicYearsToSync(e)}
118+
data={academicYears}
119+
/>
120+
<Checkbox.Group
121+
value={value}
122+
onChange={setValue}
123+
label="Select products to sync"
124+
description="If you are being IP banned, you might want to wait a few minutes and try fewer products"
125+
>
126+
<Stack pt="md" gap="xs">
127+
{products && products.length > 0 ? cards : <Text>No products found</Text>}
128+
</Stack>
129+
</Checkbox.Group>
130+
131+
<Group justify="space-between" mt="sm">
132+
<Button
133+
onClick={() =>
134+
setValue(products?.map((product) => product.id.toString(10)) ?? [])
135+
}
136+
color="violet"
137+
loading={false}
138+
>
139+
Select all
140+
</Button>
141+
<Button onClick={syncEActivities} color="green" loading={isPending}>
142+
Sync {value.length} products
143+
</Button>
144+
</Group>
145+
</Stack>
146+
);
147+
}
148+
149+
export const SyncEActivities = ({
150+
setActionsError,
151+
academicYears,
152+
currentlySelectedAcademicYears,
153+
}: {
154+
setActionsError: (error: string | null) => void;
155+
academicYears: string[];
156+
currentlySelectedAcademicYears: string[];
157+
}) => {
158+
const [opened, { open, close }] = useDisclosure(false);
159+
160+
return (
161+
<>
162+
<Modal opened={opened} onClose={close} title={`Select products to sync`} size="xl">
163+
<CheckboxesForProducts
164+
close={close}
165+
academicYears={academicYears}
166+
currentlySelectedAcademicYears={currentlySelectedAcademicYears}
167+
/>
168+
</Modal>
169+
<Button leftSection={<FaSync />} color="violet" onClick={open}>
170+
Sync from eActivities
171+
</Button>
172+
</>
173+
);
174+
};

collection/app/(app)/UserSearch.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const UserSearch: React.FC<UserSearchProps> = ({
2222
validAcaemicYears,
2323
}) => {
2424
const [error, setError] = useState<string | null>(null);
25+
const [actionsError, setActionsError] = useState<string | null>(null);
2526
const [purchases, setPurchases] = useState<OrderResponse[]>([]);
2627
const [isPending, startTransition] = useTransition();
2728

@@ -117,10 +118,16 @@ export const UserSearch: React.FC<UserSearchProps> = ({
117118
return (
118119
<Container w="70%">
119120
<Stack gap="lg">
121+
{actionsError && (
122+
<Alert title="Error" color="red" mt="md" icon={<FaTimesCircle />}>
123+
{actionsError}
124+
</Alert>
125+
)}
120126
<PageActions
121127
currentAcademicYear={currentAcademicYear}
122128
academicYears={validAcaemicYears}
123129
formHook={form}
130+
setActionsError={setActionsError}
124131
/>
125132
<Center>
126133
<Stack w="90%" justify="centre" align="centre">

0 commit comments

Comments
 (0)