@olinfo/quizms-firebase 4.1.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/cli.js +14 -14
  2. package/dist/cli.js.map +1 -1
  3. package/dist/package.json.js +1 -1
  4. package/dist/src/cli/definalize.js +17 -17
  5. package/dist/src/cli/definalize.js.map +1 -1
  6. package/dist/src/cli/export.js +42 -34
  7. package/dist/src/cli/export.js.map +1 -1
  8. package/dist/src/cli/files/firestore-indexes.json.js +1 -1
  9. package/dist/src/cli/files/firestore.rules.js +27 -27
  10. package/dist/src/cli/files/firestore.rules.js.map +1 -1
  11. package/dist/src/cli/files/functions-index.js +3 -3
  12. package/dist/src/cli/files/functions-index.js.map +1 -1
  13. package/dist/src/cli/files/storage.rules.js +3 -3
  14. package/dist/src/cli/files/storage.rules.js.map +1 -1
  15. package/dist/src/cli/import.js +65 -71
  16. package/dist/src/cli/import.js.map +1 -1
  17. package/dist/src/cli/update-scores.js +7 -7
  18. package/dist/src/cli/update-scores.js.map +1 -1
  19. package/dist/src/cli/utils/converters-admin.js +23 -22
  20. package/dist/src/cli/utils/converters-admin.js.map +1 -1
  21. package/dist/src/functions/api/index.js +18 -18
  22. package/dist/src/functions/api/index.js.map +1 -1
  23. package/dist/src/functions/api/queries.js +21 -21
  24. package/dist/src/functions/api/queries.js.map +1 -1
  25. package/dist/src/functions/api/student.js +46 -47
  26. package/dist/src/functions/api/student.js.map +1 -1
  27. package/dist/src/functions/api/teacher.js +92 -95
  28. package/dist/src/functions/api/teacher.js.map +1 -1
  29. package/dist/src/functions/submissions.js +11 -7
  30. package/dist/src/functions/submissions.js.map +1 -1
  31. package/dist/src/functions/update-scores.js +14 -14
  32. package/dist/src/functions/update-scores.js.map +1 -1
  33. package/dist/src/models/submission.js +13 -0
  34. package/dist/src/models/submission.js.map +1 -0
  35. package/dist/src/web/admin/announcements.js +45 -42
  36. package/dist/src/web/admin/announcements.js.map +1 -1
  37. package/dist/src/web/admin/contest-settings.js +19 -11
  38. package/dist/src/web/admin/contest-settings.js.map +1 -1
  39. package/dist/src/web/admin/context.js.map +1 -1
  40. package/dist/src/web/admin/dashboard.js +56 -35
  41. package/dist/src/web/admin/dashboard.js.map +1 -1
  42. package/dist/src/web/admin/export.js +100 -43
  43. package/dist/src/web/admin/export.js.map +1 -1
  44. package/dist/src/web/admin/layout.js +25 -21
  45. package/dist/src/web/admin/layout.js.map +1 -1
  46. package/dist/src/web/admin/provider.js +41 -21
  47. package/dist/src/web/admin/provider.js.map +1 -1
  48. package/dist/src/web/admin/school-table.js +20 -24
  49. package/dist/src/web/admin/school-table.js.map +1 -1
  50. package/dist/src/web/common/api.js +24 -20
  51. package/dist/src/web/common/api.js.map +1 -1
  52. package/dist/src/web/common/converters.js +38 -39
  53. package/dist/src/web/common/converters.js.map +1 -1
  54. package/dist/src/web/common/token-login.js +18 -18
  55. package/dist/src/web/common/token-login.js.map +1 -1
  56. package/dist/src/web/entry/admin.js +22 -13
  57. package/dist/src/web/entry/admin.js.map +1 -1
  58. package/dist/src/web/entry/student-statement.js +1 -1
  59. package/dist/src/web/entry/student-statement.js.map +1 -1
  60. package/dist/src/web/entry/student.js +80 -75
  61. package/dist/src/web/entry/student.js.map +1 -1
  62. package/dist/src/web/entry/teacher.js +63 -62
  63. package/dist/src/web/entry/teacher.js.map +1 -1
  64. package/package.json +13 -13
@@ -1,63 +1,66 @@
1
- import { jsxs as o, Fragment as f, jsx as e } from "react/jsx-runtime";
2
- import { useRef as p, Fragment as u, forwardRef as g, useReducer as h } from "react";
3
- import { randomId as A } from "@olinfo/quizms/utils";
4
- import { DateTime as x, CardActions as w, Button as y, Modal as N, Form as b, SelectField as z, TextField as F, TextAreaField as I, SubmitButton as v } from "@olinfo/react-components";
5
- import { Info as C, TriangleAlert as M, CircleX as T } from "lucide-react";
6
- import S from "react-markdown";
7
- import { announcementConverter as B } from "../common/converters.js";
1
+ import { jsxs as t, Fragment as d, jsx as e } from "react/jsx-runtime";
2
+ import { useRef as f, Fragment as p, useReducer as u } from "react";
3
+ import { randomId as g } from "@olinfo/quizms/utils";
4
+ import { DateTime as h, CardActions as A, Button as x, Modal as w, Form as y, SelectField as N, TextField as b, TextAreaField as z, SubmitButton as F } from "@olinfo/react-components";
5
+ import { Info as I, TriangleAlert as v, CircleX as C } from "lucide-react";
6
+ import M from "react-markdown";
7
+ import { announcementConverter as T } from "../common/converters.js";
8
8
  import "firebase/auth";
9
9
  import "lodash-es";
10
10
  import "react-error-boundary";
11
11
  import "swr";
12
12
  import "../common/base-login.js";
13
- import { useCollection as R } from "../hooks/collection.js";
13
+ import { useCollection as S } from "../hooks/collection.js";
14
14
  import "firebase/firestore";
15
15
  import { useAdmin as s } from "./context.js";
16
- function W() {
17
- const { contest: a } = s(), t = p(null), [r, n] = R("announcements", B, {
16
+ function U() {
17
+ const { contest: o } = s(), r = f(null), [l, n] = S("announcements", T, {
18
18
  arrayConstraints: {
19
- contestIds: a.id
19
+ contestIds: o.id
20
20
  },
21
21
  orderBy: ["createdAt", "desc"]
22
22
  });
23
- return /* @__PURE__ */ o(f, { children: [
24
- /* @__PURE__ */ e("div", { className: "prose mt-4 max-w-none", children: r.map((i) => /* @__PURE__ */ o(u, { children: [
25
- /* @__PURE__ */ o("h4", { className: "flex items-center gap-3", children: [
26
- i.level === "info" && /* @__PURE__ */ e(C, { size: 20, className: "text-info" }),
27
- i.level === "warning" && /* @__PURE__ */ e(M, { size: 20, className: "text-warning" }),
28
- i.level === "error" && /* @__PURE__ */ e(T, { size: 20, className: "text-error" }),
23
+ return /* @__PURE__ */ t(d, { children: [
24
+ /* @__PURE__ */ e("div", { className: "prose mt-4 max-w-none", children: l.map((i) => /* @__PURE__ */ t(p, { children: [
25
+ /* @__PURE__ */ t("h4", { className: "flex items-center gap-3", children: [
26
+ i.level === "info" && /* @__PURE__ */ e(I, { size: 20, className: "text-info" }),
27
+ i.level === "warning" && /* @__PURE__ */ e(v, { size: 20, className: "text-warning" }),
28
+ i.level === "error" && /* @__PURE__ */ e(C, { size: 20, className: "text-error" }),
29
29
  i.title
30
30
  ] }),
31
- /* @__PURE__ */ e("small", { children: /* @__PURE__ */ e(x, { date: i.createdAt }) }),
32
- /* @__PURE__ */ e(S, { children: i.body }),
31
+ /* @__PURE__ */ e("small", { children: /* @__PURE__ */ e(h, { date: i.createdAt }) }),
32
+ /* @__PURE__ */ e(M, { children: i.body }),
33
33
  /* @__PURE__ */ e("hr", { className: "my-5 last:hidden" })
34
34
  ] }, i.id)) }),
35
- /* @__PURE__ */ e(w, { children: /* @__PURE__ */ e(y, { className: "btn-primary", onClick: () => t.current?.showModal(), children: "Aggiungi comunicazione" }) }),
36
- /* @__PURE__ */ e(j, { ref: t, addAnnouncement: n })
35
+ /* @__PURE__ */ e(A, { children: /* @__PURE__ */ e(x, { className: "btn-primary", onClick: () => r.current?.showModal(), children: "Aggiungi comunicazione" }) }),
36
+ /* @__PURE__ */ e(B, { ref: r, addAnnouncement: n })
37
37
  ] });
38
38
  }
39
- const j = g(function({ addAnnouncement: t }, r) {
40
- const { contest: n } = s(), [i, c] = h((l) => l + 1, 0);
41
- return /* @__PURE__ */ e(N, { ref: r, title: "Aggiungi comunicazione", children: /* @__PURE__ */ o(b, { onSubmit: async ({
42
- level: l,
43
- title: m,
44
- body: d
39
+ function B({
40
+ addAnnouncement: o,
41
+ ref: r
42
+ }) {
43
+ const { contest: l } = s(), [n, i] = u((a) => a + 1, 0);
44
+ return /* @__PURE__ */ e(w, { ref: r, title: "Aggiungi comunicazione", children: /* @__PURE__ */ t(y, { onSubmit: async ({
45
+ level: a,
46
+ title: c,
47
+ body: m
45
48
  }) => {
46
49
  try {
47
- await t({
48
- id: A(),
50
+ await o({
51
+ id: g(),
49
52
  createdAt: /* @__PURE__ */ new Date(),
50
- contestIds: [n.id],
51
- level: l,
52
- title: m,
53
- body: d
54
- }), c();
53
+ contestIds: [l.id],
54
+ level: a,
55
+ title: c,
56
+ body: m
57
+ }), i();
55
58
  } finally {
56
- r && "current" in r && r.current?.close();
59
+ r.current?.close();
57
60
  }
58
61
  }, className: "!max-w-full", children: [
59
62
  /* @__PURE__ */ e(
60
- z,
63
+ N,
61
64
  {
62
65
  field: "level",
63
66
  label: "Priorità",
@@ -65,9 +68,9 @@ const j = g(function({ addAnnouncement: t }, r) {
65
68
  placeholder: "Seleziona la priorità"
66
69
  }
67
70
  ),
68
- /* @__PURE__ */ e(F, { field: "title", label: "Titolo", placeholder: "Inserisci il titolo" }),
71
+ /* @__PURE__ */ e(b, { field: "title", label: "Titolo", placeholder: "Inserisci il titolo" }),
69
72
  /* @__PURE__ */ e(
70
- I,
73
+ z,
71
74
  {
72
75
  field: "body",
73
76
  label: "Messaggio",
@@ -75,10 +78,10 @@ const j = g(function({ addAnnouncement: t }, r) {
75
78
  rows: 5
76
79
  }
77
80
  ),
78
- /* @__PURE__ */ e(v, { children: "Aggiungi" })
79
- ] }, i) });
80
- });
81
+ /* @__PURE__ */ e(F, { children: "Aggiungi" })
82
+ ] }, n) });
83
+ }
81
84
  export {
82
- W as default
85
+ U as default
83
86
  };
84
87
  //# sourceMappingURL=announcements.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"announcements.js","sources":["../../../../src/web/admin/announcements.tsx"],"sourcesContent":["import { Fragment, forwardRef, type Ref, useReducer, useRef } from \"react\";\n\nimport type { Announcement } from \"@olinfo/quizms/models\";\nimport { randomId } from \"@olinfo/quizms/utils\";\nimport {\n Button,\n CardActions,\n DateTime,\n Form,\n Modal,\n SelectField,\n SubmitButton,\n TextAreaField,\n TextField,\n} from \"@olinfo/react-components\";\nimport { CircleX, Info, TriangleAlert } from \"lucide-react\";\nimport Markdown from \"react-markdown\";\n\nimport { announcementConverter } from \"~/web/common/converters\";\nimport { useCollection } from \"~/web/hooks\";\n\nimport { useAdmin } from \"./context\";\n\nexport default function Announcements() {\n const { contest } = useAdmin();\n const ref = useRef<HTMLDialogElement>(null);\n\n const [announcements, addAnnouncement] = useCollection(\"announcements\", announcementConverter, {\n arrayConstraints: {\n contestIds: contest.id,\n },\n orderBy: [\"createdAt\", \"desc\"],\n });\n\n return (\n <>\n <div className=\"prose mt-4 max-w-none\">\n {announcements.map((announcement) => (\n <Fragment key={announcement.id}>\n <h4 className=\"flex items-center gap-3\">\n {announcement.level === \"info\" && <Info size={20} className=\"text-info\" />}\n {announcement.level === \"warning\" && (\n <TriangleAlert size={20} className=\"text-warning\" />\n )}\n {announcement.level === \"error\" && <CircleX size={20} className=\"text-error\" />}\n {announcement.title}\n </h4>\n <small>\n <DateTime date={announcement.createdAt} />\n </small>\n <Markdown>{announcement.body}</Markdown>\n <hr className=\"my-5 last:hidden\" />\n </Fragment>\n ))}\n </div>\n <CardActions>\n <Button className=\"btn-primary\" onClick={() => ref.current?.showModal()}>\n Aggiungi comunicazione\n </Button>\n </CardActions>\n <AnnouncementModal ref={ref} addAnnouncement={addAnnouncement} />\n </>\n );\n}\n\nconst AnnouncementModal = forwardRef(function AnnouncementModal(\n { addAnnouncement }: { addAnnouncement: (announcement: Announcement) => Promise<void> },\n ref: Ref<HTMLDialogElement>,\n) {\n const { contest } = useAdmin();\n const [id, nextId] = useReducer((prev) => prev + 1, 0);\n\n const finalize = async ({\n level,\n title,\n body,\n }: Pick<Announcement, \"level\" | \"title\" | \"body\">) => {\n try {\n await addAnnouncement({\n id: randomId(),\n createdAt: new Date(),\n contestIds: [contest.id],\n level,\n title,\n body,\n });\n nextId();\n } finally {\n if (ref && \"current\" in ref) {\n ref.current?.close();\n }\n }\n };\n\n return (\n <Modal ref={ref} title=\"Aggiungi comunicazione\">\n <Form key={id} onSubmit={finalize} className=\"!max-w-full\">\n <SelectField\n field=\"level\"\n label=\"Priorità\"\n options={{ info: \"Normale\", warning: \"Alta\" }}\n placeholder=\"Seleziona la priorità\"\n />\n <TextField field=\"title\" label=\"Titolo\" placeholder=\"Inserisci il titolo\" />\n <TextAreaField\n field=\"body\"\n label=\"Messaggio\"\n placeholder=\"Inserisci il messaggio\"\n rows={5}\n />\n <SubmitButton>Aggiungi</SubmitButton>\n </Form>\n </Modal>\n );\n});\n"],"names":["Announcements","contest","useAdmin","ref","useRef","announcements","addAnnouncement","useCollection","announcementConverter","jsxs","Fragment","jsx","announcement","Info","TriangleAlert","CircleX","DateTime","Markdown","CardActions","Button","AnnouncementModal","forwardRef","id","nextId","useReducer","prev","Modal","Form","level","title","body","randomId","SelectField","TextField","TextAreaField","SubmitButton"],"mappings":"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"}
1
+ {"version":3,"file":"announcements.js","sources":["../../../../src/web/admin/announcements.tsx"],"sourcesContent":["import { Fragment, type RefObject, useReducer, useRef } from \"react\";\n\nimport type { Announcement } from \"@olinfo/quizms/models\";\nimport { randomId } from \"@olinfo/quizms/utils\";\nimport {\n Button,\n CardActions,\n DateTime,\n Form,\n Modal,\n SelectField,\n SubmitButton,\n TextAreaField,\n TextField,\n} from \"@olinfo/react-components\";\nimport { CircleX, Info, TriangleAlert } from \"lucide-react\";\nimport Markdown from \"react-markdown\";\n\nimport { announcementConverter } from \"~/web/common/converters\";\nimport { useCollection } from \"~/web/hooks\";\n\nimport { useAdmin } from \"./context\";\n\nexport default function Announcements() {\n const { contest } = useAdmin();\n const ref = useRef<HTMLDialogElement>(null);\n\n const [announcements, addAnnouncement] = useCollection(\"announcements\", announcementConverter, {\n arrayConstraints: {\n contestIds: contest.id,\n },\n orderBy: [\"createdAt\", \"desc\"],\n });\n\n return (\n <>\n <div className=\"prose mt-4 max-w-none\">\n {announcements.map((announcement) => (\n <Fragment key={announcement.id}>\n <h4 className=\"flex items-center gap-3\">\n {announcement.level === \"info\" && <Info size={20} className=\"text-info\" />}\n {announcement.level === \"warning\" && (\n <TriangleAlert size={20} className=\"text-warning\" />\n )}\n {announcement.level === \"error\" && <CircleX size={20} className=\"text-error\" />}\n {announcement.title}\n </h4>\n <small>\n <DateTime date={announcement.createdAt} />\n </small>\n <Markdown>{announcement.body}</Markdown>\n <hr className=\"my-5 last:hidden\" />\n </Fragment>\n ))}\n </div>\n <CardActions>\n <Button className=\"btn-primary\" onClick={() => ref.current?.showModal()}>\n Aggiungi comunicazione\n </Button>\n </CardActions>\n <AnnouncementModal ref={ref} addAnnouncement={addAnnouncement} />\n </>\n );\n}\n\nfunction AnnouncementModal({\n addAnnouncement,\n ref,\n}: {\n addAnnouncement: (announcement: Announcement) => Promise<void>;\n ref: RefObject<HTMLDialogElement | null>;\n}) {\n const { contest } = useAdmin();\n const [id, nextId] = useReducer((prev) => prev + 1, 0);\n\n const finalize = async ({\n level,\n title,\n body,\n }: Pick<Announcement, \"level\" | \"title\" | \"body\">) => {\n try {\n await addAnnouncement({\n id: randomId(),\n createdAt: new Date(),\n contestIds: [contest.id],\n level,\n title,\n body,\n });\n nextId();\n } finally {\n ref.current?.close();\n }\n };\n\n return (\n <Modal ref={ref} title=\"Aggiungi comunicazione\">\n <Form key={id} onSubmit={finalize} className=\"!max-w-full\">\n <SelectField\n field=\"level\"\n label=\"Priorità\"\n options={{ info: \"Normale\", warning: \"Alta\" }}\n placeholder=\"Seleziona la priorità\"\n />\n <TextField field=\"title\" label=\"Titolo\" placeholder=\"Inserisci il titolo\" />\n <TextAreaField\n field=\"body\"\n label=\"Messaggio\"\n placeholder=\"Inserisci il messaggio\"\n rows={5}\n />\n <SubmitButton>Aggiungi</SubmitButton>\n </Form>\n </Modal>\n );\n}\n"],"names":["Announcements","contest","useAdmin","ref","useRef","announcements","addAnnouncement","useCollection","announcementConverter","jsxs","Fragment","jsx","announcement","Info","TriangleAlert","CircleX","DateTime","Markdown","CardActions","Button","AnnouncementModal","id","nextId","useReducer","prev","Modal","Form","level","title","body","randomId","SelectField","TextField","TextAreaField","SubmitButton"],"mappings":"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"}
@@ -1,16 +1,24 @@
1
- import { jsxs as a, jsx as e } from "react/jsx-runtime";
2
- import { Form as i, TextAreaField as n, SubmitButton as r } from "@olinfo/react-components";
3
- import { useAdmin as l } from "./context.js";
4
- function f() {
5
- const { contest: t, setContest: s } = l();
6
- return /* @__PURE__ */ a(i, { defaultValue: t, onSubmit: async (o) => {
7
- await s({ ...t, ...o });
1
+ import { jsxs as o, jsx as t } from "react/jsx-runtime";
2
+ import { Form as l, SelectField as s, TextAreaField as n, SubmitButton as r } from "@olinfo/react-components";
3
+ import { useAdmin as u } from "./context.js";
4
+ function p() {
5
+ const { contest: e, setContest: i } = u();
6
+ return /* @__PURE__ */ o(l, { defaultValue: e, onSubmit: async (a) => {
7
+ await i({ ...e, ...a });
8
8
  }, className: "!max-w-full", children: [
9
- /* @__PURE__ */ e(n, { field: "instructions", label: "Istruzioni gara", rows: 8 }),
10
- /* @__PURE__ */ e(r, { children: "Salva" })
11
- ] }, t.id);
9
+ /* @__PURE__ */ t(
10
+ s,
11
+ {
12
+ field: "scoreVisibility",
13
+ label: "Mostra punteggi",
14
+ options: { never: "Mai", always: "Sempre", finalized: "Solo alle scuole finalizzate" }
15
+ }
16
+ ),
17
+ /* @__PURE__ */ t(n, { field: "instructions", label: "Istruzioni gara", rows: 8, optional: !0 }),
18
+ /* @__PURE__ */ t(r, { children: "Salva" })
19
+ ] }, e.id);
12
20
  }
13
21
  export {
14
- f as default
22
+ p as default
15
23
  };
16
24
  //# sourceMappingURL=contest-settings.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contest-settings.js","sources":["../../../../src/web/admin/contest-settings.tsx"],"sourcesContent":["import type { Contest } from \"@olinfo/quizms/models\";\nimport { Form, SubmitButton, TextAreaField } from \"@olinfo/react-components\";\n\nimport { useAdmin } from \"./context\";\n\nexport default function ContestSettings() {\n const { contest, setContest } = useAdmin();\n\n const save = async (newContest: Pick<Contest, \"instructions\">) => {\n await setContest({ ...contest, ...newContest });\n };\n\n return (\n <Form key={contest.id} defaultValue={contest} onSubmit={save} className=\"!max-w-full\">\n <TextAreaField field=\"instructions\" label=\"Istruzioni gara\" rows={8} />\n <SubmitButton>Salva</SubmitButton>\n </Form>\n );\n}\n"],"names":["ContestSettings","contest","setContest","useAdmin","Form","newContest","jsx","TextAreaField","SubmitButton"],"mappings":";;;AAKA,SAAwBA,IAAkB;AACxC,QAAM,EAAE,SAAAC,GAAS,YAAAC,EAAA,IAAeC,EAAA;AAMhC,2BACGC,GAAA,EAAsB,cAAcH,GAAS,UALnC,OAAOI,MAA8C;AAChE,UAAMH,EAAW,EAAE,GAAGD,GAAS,GAAGI,GAAY;AAAA,EAChD,GAGgE,WAAU,eACtE,UAAA;AAAA,IAAA,gBAAAC,EAACC,KAAc,OAAM,gBAAe,OAAM,mBAAkB,MAAM,GAAG;AAAA,IACrE,gBAAAD,EAACE,KAAa,UAAA,QAAA,CAAK;AAAA,EAAA,EAAA,GAFVP,EAAQ,EAGnB;AAEJ;"}
1
+ {"version":3,"file":"contest-settings.js","sources":["../../../../src/web/admin/contest-settings.tsx"],"sourcesContent":["import type { Contest } from \"@olinfo/quizms/models\";\nimport { Form, SelectField, SubmitButton, TextAreaField } from \"@olinfo/react-components\";\n\nimport { useAdmin } from \"./context\";\n\nexport default function ContestSettings() {\n const { contest, setContest } = useAdmin();\n\n const save = async (newContest: Pick<Contest, \"instructions\" | \"scoreVisibility\">) => {\n await setContest({ ...contest, ...newContest });\n };\n\n return (\n <Form key={contest.id} defaultValue={contest} onSubmit={save} className=\"!max-w-full\">\n <SelectField\n field=\"scoreVisibility\"\n label=\"Mostra punteggi\"\n options={{ never: \"Mai\", always: \"Sempre\", finalized: \"Solo alle scuole finalizzate\" }}\n />\n <TextAreaField field=\"instructions\" label=\"Istruzioni gara\" rows={8} optional={true} />\n <SubmitButton>Salva</SubmitButton>\n </Form>\n );\n}\n"],"names":["ContestSettings","contest","setContest","useAdmin","Form","newContest","jsx","SelectField","TextAreaField","SubmitButton"],"mappings":";;;AAKA,SAAwBA,IAAkB;AACxC,QAAM,EAAE,SAAAC,GAAS,YAAAC,EAAA,IAAeC,EAAA;AAMhC,2BACGC,GAAA,EAAsB,cAAcH,GAAS,UALnC,OAAOI,MAAkE;AACpF,UAAMH,EAAW,EAAE,GAAGD,GAAS,GAAGI,GAAY;AAAA,EAChD,GAGgE,WAAU,eACtE,UAAA;AAAA,IAAA,gBAAAC;AAAA,MAACC;AAAA,MAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAM;AAAA,QACN,SAAS,EAAE,OAAO,OAAO,QAAQ,UAAU,WAAW,+BAAA;AAAA,MAA+B;AAAA,IAAA;AAAA,IAEvF,gBAAAD,EAACE,KAAc,OAAM,gBAAe,OAAM,mBAAkB,MAAM,GAAG,UAAU,GAAA,CAAM;AAAA,IACrF,gBAAAF,EAACG,KAAa,UAAA,QAAA,CAAK;AAAA,EAAA,EAAA,GAPVR,EAAQ,EAQnB;AAEJ;"}
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sources":["../../../../src/web/admin/context.tsx"],"sourcesContent":["import { createContext, use } from \"react\";\n\nimport type { Contest } from \"@olinfo/quizms/models\";\n\nexport type AdminContextProps = {\n name: string;\n contest: Contest;\n contests: Contest[];\n setContest: (contest: Contest) => Promise<void>;\n};\n\nexport const AdminContext = createContext<AdminContextProps>({} as AdminContextProps);\nAdminContext.displayName = \"AdminContext\";\n\nexport function useAdmin() {\n return use(AdminContext);\n}\n"],"names":["AdminContext","createContext","useAdmin","use"],"mappings":";AAWO,MAAMA,IAAeC,EAAiC,CAAA,CAAuB;AACpFD,EAAa,cAAc;AAEpB,SAASE,IAAW;AACzB,SAAOC,EAAIH,CAAY;AACzB;"}
1
+ {"version":3,"file":"context.js","sources":["../../../../src/web/admin/context.tsx"],"sourcesContent":["import { createContext, use } from \"react\";\n\nimport type { Contest, Variant } from \"@olinfo/quizms/models\";\n\nexport type AdminContextProps = {\n name: string;\n contest: Contest;\n contests: Contest[];\n variants: Record<string, Variant>;\n setContest: (contest: Contest) => Promise<void>;\n};\n\nexport const AdminContext = createContext<AdminContextProps>({} as AdminContextProps);\nAdminContext.displayName = \"AdminContext\";\n\nexport function useAdmin() {\n return use(AdminContext);\n}\n"],"names":["AdminContext","createContext","useAdmin","use"],"mappings":";AAYO,MAAMA,IAAeC,EAAiC,CAAA,CAAuB;AACpFD,EAAa,cAAc;AAEpB,SAASE,IAAW;AACzB,SAAOC,EAAIH,CAAY;AACzB;"}
@@ -1,65 +1,86 @@
1
1
  import { jsxs as i, jsx as t } from "react/jsx-runtime";
2
- import { Suspense as u } from "react";
3
- import { Loading as m } from "@olinfo/quizms/components";
4
- import { Card as e, CardBody as n, CardActions as s } from "@olinfo/react-components";
5
- import { Link as f } from "wouter";
6
- import { participationConverter as h, studentConverter as b } from "../common/converters.js";
7
- import { useCount as r } from "../hooks/count.js";
8
- import v from "./announcements.js";
9
- import C from "./contest-settings.js";
10
- import { useAdmin as c } from "./context.js";
11
- import l from "./export.js";
2
+ import { useCallback as f, Suspense as p } from "react";
3
+ import { Loading as h } from "@olinfo/quizms/components";
4
+ import { Card as r, CardBody as s, CardActions as a } from "@olinfo/react-components";
5
+ import { Link as b } from "wouter";
6
+ import { venueConverter as v, studentConverter as u } from "../common/converters.js";
7
+ import { useCount as c } from "../hooks/count.js";
8
+ import g from "./announcements.js";
9
+ import S from "./contest-settings.js";
10
+ import { useAdmin as m } from "./context.js";
11
+ import l, { scoreboardFormatter as C, scoreboradHeader as I } from "./export.js";
12
12
  function B() {
13
- const { contest: o } = c();
13
+ const { contest: e, variants: n } = m(), d = f(
14
+ (o) => C(e, n, o),
15
+ [e, n]
16
+ );
14
17
  return /* @__PURE__ */ i("div", { className: "flex flex-col gap-4", children: [
15
- /* @__PURE__ */ t(e, { children: /* @__PURE__ */ t(n, { title: "Impostazioni gara", children: /* @__PURE__ */ t(C, {}) }) }),
16
- /* @__PURE__ */ t(e, { children: /* @__PURE__ */ t(n, { title: "Comunicazioni", children: /* @__PURE__ */ t(v, {}) }) }),
17
- /* @__PURE__ */ t(e, { children: /* @__PURE__ */ i(n, { title: "Scuole", children: [
18
- /* @__PURE__ */ t(u, { fallback: /* @__PURE__ */ t(m, {}), children: /* @__PURE__ */ t(S, {}) }),
19
- /* @__PURE__ */ t(s, { children: /* @__PURE__ */ t(f, { href: "/schools", className: "btn btn-primary", children: "Gestione scuole" }) })
18
+ /* @__PURE__ */ t(r, { children: /* @__PURE__ */ t(s, { title: "Impostazioni gara", children: /* @__PURE__ */ t(S, {}) }) }),
19
+ /* @__PURE__ */ t(r, { children: /* @__PURE__ */ t(s, { title: "Comunicazioni", children: /* @__PURE__ */ t(g, {}) }) }),
20
+ /* @__PURE__ */ t(r, { children: /* @__PURE__ */ i(s, { title: "Scuole", children: [
21
+ /* @__PURE__ */ t(p, { fallback: /* @__PURE__ */ t(h, {}), children: /* @__PURE__ */ t(z, {}) }),
22
+ /* @__PURE__ */ t(a, { children: /* @__PURE__ */ t(b, { href: "/schools", className: "btn btn-primary", children: "Gestione scuole" }) })
20
23
  ] }) }),
21
- /* @__PURE__ */ t(e, { children: /* @__PURE__ */ t(n, { title: "Esportazione", children: /* @__PURE__ */ i(s, { children: [
24
+ /* @__PURE__ */ t(r, { children: /* @__PURE__ */ t(s, { title: "Esportazione", children: /* @__PURE__ */ i(a, { children: [
22
25
  /* @__PURE__ */ t(
23
26
  l,
24
27
  {
25
28
  label: "scuole",
26
- description: "tutte le scuole",
27
- collection: "participations",
28
- converter: h,
29
- options: { constraints: { contestId: o.id, disabled: !1 } }
29
+ description: "i dati di tutte le scuole",
30
+ collection: "venues",
31
+ converter: v,
32
+ options: { constraints: { contestId: e.id, disabled: !1 } },
33
+ formatter: (o) => JSON.stringify(o)
30
34
  }
31
35
  ),
32
36
  /* @__PURE__ */ t(
33
37
  l,
34
38
  {
35
39
  label: "studenti",
36
- description: "tutti gli studenti",
40
+ description: "i dati di tutti gli studenti",
37
41
  collection: "students",
38
- converter: b,
42
+ converter: u,
39
43
  options: {
40
- constraints: { contestId: o.id, disabled: !1 },
44
+ constraints: { contestId: e.id, disabled: !1 },
41
45
  group: !0
42
- }
46
+ },
47
+ formatter: (o) => JSON.stringify(o)
48
+ }
49
+ ),
50
+ /* @__PURE__ */ t(
51
+ l,
52
+ {
53
+ label: "classifica",
54
+ description: "la classifica",
55
+ collection: "students",
56
+ suggestedName: `scoreboard-${e.id}.csv`,
57
+ converter: u,
58
+ options: {
59
+ constraints: { contestId: e.id, disabled: !1 },
60
+ group: !0
61
+ },
62
+ formatter: d,
63
+ header: I(e)
43
64
  }
44
65
  )
45
66
  ] }) }) })
46
67
  ] });
47
68
  }
48
- function S() {
49
- const { contest: o } = c(), d = r("participations", {
69
+ function z() {
70
+ const { contest: e } = m(), n = c("venues", {
50
71
  constraints: {
51
- contestId: o.id,
72
+ contestId: e.id,
52
73
  disabled: !1
53
74
  }
54
- }), a = r("participations", {
75
+ }), d = c("venues", {
55
76
  constraints: {
56
- contestId: o.id,
77
+ contestId: e.id,
57
78
  finalized: !0,
58
79
  disabled: !1
59
80
  }
60
- }), p = r("students", {
81
+ }), o = c("students", {
61
82
  constraints: {
62
- contestId: o.id,
83
+ contestId: e.id,
63
84
  disabled: !1
64
85
  },
65
86
  group: !0
@@ -67,15 +88,15 @@ function S() {
67
88
  return /* @__PURE__ */ i("div", { children: [
68
89
  /* @__PURE__ */ i("div", { children: [
69
90
  "Scuole totali: ",
70
- d
91
+ n
71
92
  ] }),
72
93
  /* @__PURE__ */ i("div", { children: [
73
94
  "Scuole finalizzate: ",
74
- a
95
+ d
75
96
  ] }),
76
97
  /* @__PURE__ */ i("div", { children: [
77
98
  "Studenti totali: ",
78
- p
99
+ o
79
100
  ] })
80
101
  ] });
81
102
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.js","sources":["../../../../src/web/admin/dashboard.tsx"],"sourcesContent":["import { Suspense } from \"react\";\n\nimport { Loading } from \"@olinfo/quizms/components\";\nimport type { Participation, Student } from \"@olinfo/quizms/models\";\nimport { Card, CardActions, CardBody } from \"@olinfo/react-components\";\nimport { Link } from \"wouter\";\n\nimport { participationConverter, studentConverter } from \"~/web/common/converters\";\nimport { useCount } from \"~/web/hooks/count\";\n\nimport Announcements from \"./announcements\";\nimport ContestSettings from \"./contest-settings\";\nimport { useAdmin } from \"./context\";\nimport Export from \"./export\";\n\nexport default function Dashboard() {\n const { contest } = useAdmin();\n\n return (\n <div className=\"flex flex-col gap-4\">\n <Card>\n <CardBody title=\"Impostazioni gara\">\n <ContestSettings />\n </CardBody>\n </Card>\n <Card>\n <CardBody title=\"Comunicazioni\">\n <Announcements />\n </CardBody>\n </Card>\n <Card>\n <CardBody title=\"Scuole\">\n <Suspense fallback={<Loading />}>\n <ContestInformation />\n </Suspense>\n <CardActions>\n <Link href=\"/schools\" className=\"btn btn-primary\">\n Gestione scuole\n </Link>\n </CardActions>\n </CardBody>\n </Card>\n <Card>\n <CardBody title=\"Esportazione\">\n <CardActions>\n <Export\n label=\"scuole\"\n description=\"tutte le scuole\"\n collection=\"participations\"\n converter={participationConverter}\n options={{ constraints: { contestId: contest.id, disabled: false } }}\n />\n <Export\n label=\"studenti\"\n description=\"tutti gli studenti\"\n collection=\"students\"\n converter={studentConverter}\n options={{\n constraints: { contestId: contest.id, disabled: false },\n group: true,\n }}\n />\n </CardActions>\n </CardBody>\n </Card>\n </div>\n );\n}\n\nfunction ContestInformation() {\n const { contest } = useAdmin();\n\n const totalSchools = useCount<Participation>(\"participations\", {\n constraints: {\n contestId: contest.id,\n disabled: false,\n },\n });\n\n const finalizedSchools = useCount<Participation>(\"participations\", {\n constraints: {\n contestId: contest.id,\n finalized: true,\n disabled: false,\n },\n });\n\n const totalStudents = useCount<Student>(\"students\", {\n constraints: {\n contestId: contest.id,\n disabled: false,\n },\n group: true,\n });\n\n return (\n <div>\n <div>Scuole totali: {totalSchools}</div>\n <div>Scuole finalizzate: {finalizedSchools}</div>\n <div>Studenti totali: {totalStudents}</div>\n </div>\n );\n}\n"],"names":["Dashboard","contest","useAdmin","jsxs","jsx","Card","CardBody","ContestSettings","Announcements","Suspense","Loading","ContestInformation","CardActions","Link","Export","participationConverter","studentConverter","totalSchools","useCount","finalizedSchools","totalStudents"],"mappings":";;;;;;;;;;;AAeA,SAAwBA,IAAY;AAClC,QAAM,EAAE,SAAAC,EAAA,IAAYC,EAAA;AAEpB,SACE,gBAAAC,EAAC,OAAA,EAAI,WAAU,uBACb,UAAA;AAAA,IAAA,gBAAAC,EAACC,GAAA,EACC,4BAACC,GAAA,EAAS,OAAM,qBACd,UAAA,gBAAAF,EAACG,GAAA,CAAA,CAAgB,GACnB,EAAA,CACF;AAAA,IACA,gBAAAH,EAACC,KACC,UAAA,gBAAAD,EAACE,GAAA,EAAS,OAAM,iBACd,UAAA,gBAAAF,EAACI,GAAA,CAAA,CAAc,EAAA,CACjB,EAAA,CACF;AAAA,IACA,gBAAAJ,EAACC,GAAA,EACC,UAAA,gBAAAF,EAACG,GAAA,EAAS,OAAM,UACd,UAAA;AAAA,MAAA,gBAAAF,EAACK,KAAS,UAAU,gBAAAL,EAACM,KAAQ,GAC3B,UAAA,gBAAAN,EAACO,KAAmB,EAAA,CACtB;AAAA,MACA,gBAAAP,EAACQ,KACC,UAAA,gBAAAR,EAACS,GAAA,EAAK,MAAK,YAAW,WAAU,mBAAkB,UAAA,kBAAA,CAElD,EAAA,CACF;AAAA,IAAA,EAAA,CACF,EAAA,CACF;AAAA,sBACCR,GAAA,EACC,UAAA,gBAAAD,EAACE,KAAS,OAAM,gBACd,4BAACM,GAAA,EACC,UAAA;AAAA,MAAA,gBAAAR;AAAA,QAACU;AAAA,QAAA;AAAA,UACC,OAAM;AAAA,UACN,aAAY;AAAA,UACZ,YAAW;AAAA,UACX,WAAWC;AAAA,UACX,SAAS,EAAE,aAAa,EAAE,WAAWd,EAAQ,IAAI,UAAU,GAAA,EAAM;AAAA,QAAE;AAAA,MAAA;AAAA,MAErE,gBAAAG;AAAA,QAACU;AAAA,QAAA;AAAA,UACC,OAAM;AAAA,UACN,aAAY;AAAA,UACZ,YAAW;AAAA,UACX,WAAWE;AAAA,UACX,SAAS;AAAA,YACP,aAAa,EAAE,WAAWf,EAAQ,IAAI,UAAU,GAAA;AAAA,YAChD,OAAO;AAAA,UAAA;AAAA,QACT;AAAA,MAAA;AAAA,IACF,EAAA,CACF,GACF,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;AAEA,SAASU,IAAqB;AAC5B,QAAM,EAAE,SAAAV,EAAA,IAAYC,EAAA,GAEde,IAAeC,EAAwB,kBAAkB;AAAA,IAC7D,aAAa;AAAA,MACX,WAAWjB,EAAQ;AAAA,MACnB,UAAU;AAAA,IAAA;AAAA,EACZ,CACD,GAEKkB,IAAmBD,EAAwB,kBAAkB;AAAA,IACjE,aAAa;AAAA,MACX,WAAWjB,EAAQ;AAAA,MACnB,WAAW;AAAA,MACX,UAAU;AAAA,IAAA;AAAA,EACZ,CACD,GAEKmB,IAAgBF,EAAkB,YAAY;AAAA,IAClD,aAAa;AAAA,MACX,WAAWjB,EAAQ;AAAA,MACnB,UAAU;AAAA,IAAA;AAAA,IAEZ,OAAO;AAAA,EAAA,CACR;AAED,2BACG,OAAA,EACC,UAAA;AAAA,IAAA,gBAAAE,EAAC,OAAA,EAAI,UAAA;AAAA,MAAA;AAAA,MAAgBc;AAAA,IAAA,GAAa;AAAA,sBACjC,OAAA,EAAI,UAAA;AAAA,MAAA;AAAA,MAAqBE;AAAA,IAAA,GAAiB;AAAA,sBAC1C,OAAA,EAAI,UAAA;AAAA,MAAA;AAAA,MAAkBC;AAAA,IAAA,EAAA,CAAc;AAAA,EAAA,GACvC;AAEJ;"}
1
+ {"version":3,"file":"dashboard.js","sources":["../../../../src/web/admin/dashboard.tsx"],"sourcesContent":["import { Suspense, useCallback } from \"react\";\n\nimport { Loading } from \"@olinfo/quizms/components\";\nimport type { Student, Venue } from \"@olinfo/quizms/models\";\nimport { Card, CardActions, CardBody } from \"@olinfo/react-components\";\nimport { Link } from \"wouter\";\n\nimport { studentConverter, venueConverter } from \"~/web/common/converters\";\nimport { useCount } from \"~/web/hooks/count\";\n\nimport Announcements from \"./announcements\";\nimport ContestSettings from \"./contest-settings\";\nimport { useAdmin } from \"./context\";\nimport Export, { scoreboardFormatter, scoreboradHeader } from \"./export\";\n\nexport default function Dashboard() {\n const { contest, variants } = useAdmin();\n const contestScoreboardFormatter = useCallback(\n (student: Student) => scoreboardFormatter(contest, variants, student),\n [contest, variants],\n );\n\n return (\n <div className=\"flex flex-col gap-4\">\n <Card>\n <CardBody title=\"Impostazioni gara\">\n <ContestSettings />\n </CardBody>\n </Card>\n <Card>\n <CardBody title=\"Comunicazioni\">\n <Announcements />\n </CardBody>\n </Card>\n <Card>\n <CardBody title=\"Scuole\">\n <Suspense fallback={<Loading />}>\n <ContestInformation />\n </Suspense>\n <CardActions>\n <Link href=\"/schools\" className=\"btn btn-primary\">\n Gestione scuole\n </Link>\n </CardActions>\n </CardBody>\n </Card>\n <Card>\n <CardBody title=\"Esportazione\">\n <CardActions>\n <Export\n label=\"scuole\"\n description=\"i dati di tutte le scuole\"\n collection=\"venues\"\n converter={venueConverter}\n options={{ constraints: { contestId: contest.id, disabled: false } }}\n formatter={(data: Venue) => JSON.stringify(data)}\n />\n <Export\n label=\"studenti\"\n description=\"i dati di tutti gli studenti\"\n collection=\"students\"\n converter={studentConverter}\n options={{\n constraints: { contestId: contest.id, disabled: false },\n group: true,\n }}\n formatter={(data: Student) => JSON.stringify(data)}\n />\n <Export\n label=\"classifica\"\n description=\"la classifica\"\n collection=\"students\"\n suggestedName={`scoreboard-${contest.id}.csv`}\n converter={studentConverter}\n options={{\n constraints: { contestId: contest.id, disabled: false },\n group: true,\n }}\n formatter={contestScoreboardFormatter}\n header={scoreboradHeader(contest)}\n />\n </CardActions>\n </CardBody>\n </Card>\n </div>\n );\n}\n\nfunction ContestInformation() {\n const { contest } = useAdmin();\n\n const totalSchools = useCount<Venue>(\"venues\", {\n constraints: {\n contestId: contest.id,\n disabled: false,\n },\n });\n\n const finalizedSchools = useCount<Venue>(\"venues\", {\n constraints: {\n contestId: contest.id,\n finalized: true,\n disabled: false,\n },\n });\n\n const totalStudents = useCount<Student>(\"students\", {\n constraints: {\n contestId: contest.id,\n disabled: false,\n },\n group: true,\n });\n\n return (\n <div>\n <div>Scuole totali: {totalSchools}</div>\n <div>Scuole finalizzate: {finalizedSchools}</div>\n <div>Studenti totali: {totalStudents}</div>\n </div>\n );\n}\n"],"names":["Dashboard","contest","variants","useAdmin","contestScoreboardFormatter","useCallback","student","scoreboardFormatter","jsxs","jsx","Card","CardBody","ContestSettings","Announcements","Suspense","Loading","ContestInformation","CardActions","Link","Export","venueConverter","data","studentConverter","scoreboradHeader","totalSchools","useCount","finalizedSchools","totalStudents"],"mappings":"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"}
@@ -1,59 +1,116 @@
1
- import { jsxs as i, Fragment as p, jsx as t } from "react/jsx-runtime";
2
- import { useRef as h, forwardRef as w } from "react";
3
- import { Button as u, Modal as g } from "@olinfo/react-components";
4
- import { getDocs as x } from "firebase/firestore";
5
- import { TriangleAlert as b } from "lucide-react";
6
- import { useDb as y } from "../common/base-login.js";
7
- import E from "../common/query.js";
8
- import { useAdmin as v } from "./context.js";
9
- function $(a) {
10
- const e = h(null);
11
- return /* @__PURE__ */ i(p, { children: [
12
- /* @__PURE__ */ i(u, { className: "btn-primary", onClick: () => e.current?.showModal(), children: [
1
+ import { jsxs as d, Fragment as h, jsx as p } from "react/jsx-runtime";
2
+ import { useRef as v } from "react";
3
+ import { formatUserData as g, displayAnswer as I, unshuffleAnswer as x, calcProblemPoints as y } from "@olinfo/quizms/models";
4
+ import { Button as w, Modal as $ } from "@olinfo/react-components";
5
+ import { formatISO as E } from "date-fns";
6
+ import { getDocs as A } from "firebase/firestore";
7
+ import { TriangleAlert as P } from "lucide-react";
8
+ import { useDb as S } from "../common/base-login.js";
9
+ import j from "../common/query.js";
10
+ import { useAdmin as k } from "./context.js";
11
+ function R(s) {
12
+ const o = v(null);
13
+ return /* @__PURE__ */ d(h, { children: [
14
+ /* @__PURE__ */ d(w, { className: "btn-primary", onClick: () => o.current?.showModal(), children: [
13
15
  "Esporta ",
14
- a.label
16
+ s.label
15
17
  ] }),
16
- /* @__PURE__ */ t(N, { ref: e, ...a })
18
+ /* @__PURE__ */ p(D, { ref: o, ...s })
17
19
  ] });
18
20
  }
19
- const N = w(function({ label: e, description: l, collection: c, converter: m, options: n }, r) {
20
- const { contest: s } = v(), d = y(), o = async () => {
21
- await S(d, c, m, n), r && "current" in r && r.current?.close();
21
+ function D({
22
+ label: s,
23
+ description: o,
24
+ collection: e,
25
+ converter: r,
26
+ options: c,
27
+ formatter: a,
28
+ suggestedName: t,
29
+ header: n,
30
+ ref: i
31
+ }) {
32
+ const { contest: l } = k(), m = S(), u = async () => {
33
+ await N(m, e, r, c, a, t, n), i.current?.close();
22
34
  };
23
- return /* @__PURE__ */ t(g, { ref: r, title: `Esporta ${e}`, children: window.showSaveFilePicker === void 0 ? /* @__PURE__ */ i("div", { className: "flex items-center gap-3", children: [
24
- /* @__PURE__ */ t(b, { size: 20, className: "flex-none text-warning" }),
25
- /* @__PURE__ */ t("span", { children: "Il tuo browser non supporta l'esportazione dei dati. Usa un browser Chrome o Edge." })
26
- ] }) : /* @__PURE__ */ i(p, { children: [
27
- /* @__PURE__ */ i("div", { children: [
28
- "Stai per esportare i dati di ",
29
- l,
35
+ return /* @__PURE__ */ p($, { ref: i, title: `Esporta ${s}`, children: window.showSaveFilePicker === void 0 ? /* @__PURE__ */ d("div", { className: "flex items-center gap-3", children: [
36
+ /* @__PURE__ */ p(P, { size: 20, className: "flex-none text-warning" }),
37
+ /* @__PURE__ */ p("span", { children: "Il tuo browser non supporta l'esportazione dei dati. Usa un browser Chrome o Edge." })
38
+ ] }) : /* @__PURE__ */ d(h, { children: [
39
+ /* @__PURE__ */ d("div", { children: [
40
+ "Stai per esportare ",
41
+ o,
30
42
  " della gara ",
31
- /* @__PURE__ */ t("b", { children: s.name }),
43
+ /* @__PURE__ */ p("b", { children: l.name }),
32
44
  ". Questa operazione esegue un grande numero di query al database che richiederà del tempo e potrà incidere sulla fatturazione del progetto."
33
45
  ] }),
34
- /* @__PURE__ */ t("div", { className: "mt-4 flex flex-wrap justify-center gap-2", children: /* @__PURE__ */ t(u, { className: "btn-primary", onClick: o, children: "Esporta" }) })
46
+ /* @__PURE__ */ p("div", { className: "mt-4 flex flex-wrap justify-center gap-2", children: /* @__PURE__ */ p(w, { className: "btn-primary", onClick: u, children: "Esporta" }) })
35
47
  ] }) });
36
- });
37
- async function S(a, e, l, c) {
38
- const n = await (await window.showSaveFilePicker({
39
- suggestedName: `${e}.jsonl`
40
- })).createWritable(), r = 1e4;
41
- let s;
48
+ }
49
+ function T(s) {
50
+ const o = [];
51
+ for (const e of s.userData)
52
+ o.push(e.label);
53
+ o.push("Scuola", "Variante");
54
+ for (const e of s.problemIds)
55
+ o.push(`${e}A`, `${e}P`);
56
+ return o.push("Punteggio"), o.push("Assente"), `${o.map((e) => `"${e}"`).join(",")}
57
+ `;
58
+ }
59
+ function V(s, o, e) {
60
+ const r = [];
61
+ for (const a of s.userData)
62
+ if (a.type === "date") {
63
+ const t = e.userData?.[a.name];
64
+ t ? r.push(E(t, { representation: "date" })) : r.push("");
65
+ } else
66
+ r.push(g(e, a));
67
+ r.push(e.venueId?.split("-")[0] ?? ""), r.push(e.variantId ?? "");
68
+ const c = {};
69
+ if (e.answers !== void 0 && e.variantId !== void 0 && o[e.variantId] !== void 0) {
70
+ const a = o[e.variantId].schema;
71
+ for (const t in a) {
72
+ const n = a[t], i = n.originalId, l = e.answers[t];
73
+ c[i] = [
74
+ l?.value ? I({
75
+ type: l.type,
76
+ value: x(n, l.value)
77
+ }) : "",
78
+ y(n, l)
79
+ ];
80
+ }
81
+ }
82
+ for (const a of s.problemIds)
83
+ if (c[a] !== void 0) {
84
+ const [t, n] = c[a];
85
+ r.push(`${t}`), r.push(`${n}`);
86
+ } else
87
+ r.push(""), r.push("");
88
+ return r.push(`${e.score ?? ""}`), r.push(`${e.absent ? 1 : 0}`), r.map((a) => `"${a}"`).join(",");
89
+ }
90
+ async function N(s, o, e, r, c, a, t) {
91
+ const i = await (await window.showSaveFilePicker({
92
+ suggestedName: a ?? `${o}.jsonl`
93
+ })).createWritable();
94
+ t !== void 0 && await i.write(t);
95
+ const l = 1e4;
96
+ let m;
42
97
  for (; ; ) {
43
- const d = E(a, e, l, {
44
- ...c,
45
- limit: r,
46
- startAfter: s
47
- }), o = await x(d);
48
- if (o.empty) break;
49
- for (const f of o.docs)
50
- await n.write(`${JSON.stringify(f.data())}
98
+ const u = j(s, o, e, {
99
+ ...r,
100
+ limit: l,
101
+ startAfter: m
102
+ }), f = await A(u);
103
+ if (f.empty) break;
104
+ for (const b of f.docs)
105
+ await i.write(`${c(b.data())}
51
106
  `);
52
- s = o.docs.at(-1);
107
+ m = f.docs.at(-1);
53
108
  }
54
- await n.close();
109
+ await i.close();
55
110
  }
56
111
  export {
57
- $ as default
112
+ R as default,
113
+ V as scoreboardFormatter,
114
+ T as scoreboradHeader
58
115
  };
59
116
  //# sourceMappingURL=export.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"export.js","sources":["../../../../src/web/admin/export.tsx"],"sourcesContent":["import { forwardRef, type Ref, useRef } from \"react\";\n\nimport { Button, Modal } from \"@olinfo/react-components\";\nimport {\n type DocumentSnapshot,\n type Firestore,\n type FirestoreDataConverter,\n getDocs,\n} from \"firebase/firestore\";\nimport { TriangleAlert } from \"lucide-react\";\n\nimport { useDb } from \"~/web/common/base-login\";\nimport query, { type QueryOption } from \"~/web/common/query\";\n\nimport { useAdmin } from \"./context\";\n\ntype Props<T> = {\n label: string;\n description: string;\n collection: string;\n converter: FirestoreDataConverter<T>;\n options: QueryOption<T>;\n};\n\nexport default function Export<T>(props: Props<T>) {\n const ref = useRef<HTMLDialogElement>(null);\n\n return (\n <>\n <Button className=\"btn-primary\" onClick={() => ref.current?.showModal()}>\n Esporta {props.label}\n </Button>\n <ExportModal ref={ref} {...props} />\n </>\n );\n}\n\nconst ExportModal = forwardRef(function StudentExportModal(\n { label, description, collection, converter, options }: Props<any>,\n ref: Ref<HTMLDialogElement>,\n) {\n const { contest } = useAdmin();\n const db = useDb();\n\n const onExport = async () => {\n await saveExport(db, collection, converter, options);\n if (ref && \"current\" in ref) {\n ref.current?.close();\n }\n };\n\n return (\n <Modal ref={ref} title={`Esporta ${label}`}>\n {window.showSaveFilePicker === undefined ? (\n <div className=\"flex items-center gap-3\">\n <TriangleAlert size={20} className=\"flex-none text-warning\" />\n <span>\n Il tuo browser non supporta l&apos;esportazione dei dati. Usa un browser Chrome o Edge.\n </span>\n </div>\n ) : (\n <>\n <div>\n Stai per esportare i dati di {description} della gara <b>{contest.name}</b>. Questa\n operazione esegue un grande numero di query al database che richiederà del tempo e potrà\n incidere sulla fatturazione del progetto.\n </div>\n <div className=\"mt-4 flex flex-wrap justify-center gap-2\">\n <Button className=\"btn-primary\" onClick={onExport}>\n Esporta\n </Button>\n </div>\n </>\n )}\n </Modal>\n );\n});\n\nasync function saveExport<T>(\n db: Firestore,\n collection: string,\n converter: FirestoreDataConverter<T>,\n options: QueryOption<T>,\n) {\n const handle = await window.showSaveFilePicker({\n suggestedName: `${collection}.jsonl`,\n });\n\n const writableStream = await handle.createWritable();\n\n const chunkSize = 10_000;\n\n let last: DocumentSnapshot<T> | undefined;\n for (;;) {\n const q = query(db, collection, converter, {\n ...options,\n limit: chunkSize,\n startAfter: last,\n });\n\n const snapshot = await getDocs(q);\n if (snapshot.empty) break;\n\n for (const doc of snapshot.docs) {\n await writableStream.write(`${JSON.stringify(doc.data())}\\n`);\n }\n\n last = snapshot.docs.at(-1);\n }\n\n await writableStream.close();\n}\n"],"names":["Export","props","ref","useRef","jsxs","Fragment","Button","jsx","ExportModal","forwardRef","label","description","collection","converter","options","contest","useAdmin","db","useDb","onExport","saveExport","Modal","TriangleAlert","writableStream","chunkSize","last","q","query","snapshot","getDocs","doc"],"mappings":";;;;;;;;AAwBA,SAAwBA,EAAUC,GAAiB;AACjD,QAAMC,IAAMC,EAA0B,IAAI;AAE1C,SACE,gBAAAC,EAAAC,GAAA,EACE,UAAA;AAAA,IAAA,gBAAAD,EAACE,GAAA,EAAO,WAAU,eAAc,SAAS,MAAMJ,EAAI,SAAS,aAAa,UAAA;AAAA,MAAA;AAAA,MAC9DD,EAAM;AAAA,IAAA,GACjB;AAAA,IACA,gBAAAM,EAACC,GAAA,EAAY,KAAAN,GAAW,GAAGD,EAAA,CAAO;AAAA,EAAA,GACpC;AAEJ;AAEA,MAAMO,IAAcC,EAAW,SAC7B,EAAE,OAAAC,GAAO,aAAAC,GAAa,YAAAC,GAAY,WAAAC,GAAW,SAAAC,EAAA,GAC7CZ,GACA;AACA,QAAM,EAAE,SAAAa,EAAA,IAAYC,EAAA,GACdC,IAAKC,EAAA,GAELC,IAAW,YAAY;AAC3B,UAAMC,EAAWH,GAAIL,GAAYC,GAAWC,CAAO,GAC/CZ,KAAO,aAAaA,KACtBA,EAAI,SAAS,MAAA;AAAA,EAEjB;AAEA,SACE,gBAAAK,EAACc,GAAA,EAAM,KAAAnB,GAAU,OAAO,WAAWQ,CAAK,IACrC,UAAA,OAAO,uBAAuB,SAC7B,gBAAAN,EAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,IAAA,gBAAAG,EAACe,GAAA,EAAc,MAAM,IAAI,WAAU,0BAAyB;AAAA,IAC5D,gBAAAf,EAAC,UAAK,UAAA,qFAAA,CAEN;AAAA,EAAA,EAAA,CACF,IAEA,gBAAAH,EAAAC,GAAA,EACE,UAAA;AAAA,IAAA,gBAAAD,EAAC,OAAA,EAAI,UAAA;AAAA,MAAA;AAAA,MAC2BO;AAAA,MAAY;AAAA,MAAY,gBAAAJ,EAAC,KAAA,EAAG,UAAAQ,EAAQ,KAAA,CAAK;AAAA,MAAI;AAAA,IAAA,GAG7E;AAAA,IACA,gBAAAR,EAAC,OAAA,EAAI,WAAU,4CACb,UAAA,gBAAAA,EAACD,GAAA,EAAO,WAAU,eAAc,SAASa,GAAU,UAAA,UAAA,CAEnD,EAAA,CACF;AAAA,EAAA,EAAA,CACF,EAAA,CAEJ;AAEJ,CAAC;AAED,eAAeC,EACbH,GACAL,GACAC,GACAC,GACA;AAKA,QAAMS,IAAiB,OAJR,MAAM,OAAO,mBAAmB;AAAA,IAC7C,eAAe,GAAGX,CAAU;AAAA,EAAA,CAC7B,GAEmC,eAAA,GAE9BY,IAAY;AAElB,MAAIC;AACJ,aAAS;AACP,UAAMC,IAAIC,EAAMV,GAAIL,GAAYC,GAAW;AAAA,MACzC,GAAGC;AAAA,MACH,OAAOU;AAAA,MACP,YAAYC;AAAA,IAAA,CACb,GAEKG,IAAW,MAAMC,EAAQH,CAAC;AAChC,QAAIE,EAAS,MAAO;AAEpB,eAAWE,KAAOF,EAAS;AACzB,YAAML,EAAe,MAAM,GAAG,KAAK,UAAUO,EAAI,MAAM,CAAC;AAAA,CAAI;AAG9D,IAAAL,IAAOG,EAAS,KAAK,GAAG,EAAE;AAAA,EAC5B;AAEA,QAAML,EAAe,MAAA;AACvB;"}
1
+ {"version":3,"file":"export.js","sources":["../../../../src/web/admin/export.tsx"],"sourcesContent":["import { type RefObject, useRef } from \"react\";\n\nimport {\n type Contest,\n calcProblemPoints,\n displayAnswer,\n formatUserData,\n type Student,\n unshuffleAnswer,\n type Variant,\n} from \"@olinfo/quizms/models\";\nimport { Button, Modal } from \"@olinfo/react-components\";\nimport { formatISO } from \"date-fns\";\nimport {\n type DocumentSnapshot,\n type Firestore,\n type FirestoreDataConverter,\n getDocs,\n} from \"firebase/firestore\";\nimport { TriangleAlert } from \"lucide-react\";\n\nimport { useDb } from \"~/web/common/base-login\";\nimport query, { type QueryOption } from \"~/web/common/query\";\n\nimport { useAdmin } from \"./context\";\n\ntype Props<T> = {\n label: string;\n description: string;\n collection: string;\n converter: FirestoreDataConverter<T>;\n options: QueryOption<T>;\n suggestedName?: string;\n formatter: (data: T) => string;\n header?: string;\n};\n\nexport default function Export<T>(props: Props<T>) {\n const ref = useRef<HTMLDialogElement>(null);\n\n return (\n <>\n <Button className=\"btn-primary\" onClick={() => ref.current?.showModal()}>\n Esporta {props.label}\n </Button>\n <ExportModal ref={ref} {...props} />\n </>\n );\n}\n\nfunction ExportModal({\n label,\n description,\n collection,\n converter,\n options,\n formatter,\n suggestedName,\n header,\n ref,\n}: Props<any> & { ref: RefObject<HTMLDialogElement | null> }) {\n const { contest } = useAdmin();\n const db = useDb();\n\n const onExport = async () => {\n await saveExport(db, collection, converter, options, formatter, suggestedName, header);\n ref.current?.close();\n };\n\n return (\n <Modal ref={ref} title={`Esporta ${label}`}>\n {window.showSaveFilePicker === undefined ? (\n <div className=\"flex items-center gap-3\">\n <TriangleAlert size={20} className=\"flex-none text-warning\" />\n <span>\n Il tuo browser non supporta l&apos;esportazione dei dati. Usa un browser Chrome o Edge.\n </span>\n </div>\n ) : (\n <>\n <div>\n Stai per esportare {description} della gara <b>{contest.name}</b>. Questa operazione\n esegue un grande numero di query al database che richiederà del tempo e potrà incidere\n sulla fatturazione del progetto.\n </div>\n <div className=\"mt-4 flex flex-wrap justify-center gap-2\">\n <Button className=\"btn-primary\" onClick={onExport}>\n Esporta\n </Button>\n </div>\n </>\n )}\n </Modal>\n );\n}\n\nexport function scoreboradHeader(contest: Contest) {\n const row: string[] = [];\n for (const field of contest.userData) {\n row.push(field.label);\n }\n row.push(\"Scuola\", \"Variante\");\n for (const problemId of contest.problemIds) {\n row.push(`${problemId}A`, `${problemId}P`);\n }\n row.push(\"Punteggio\");\n row.push(\"Assente\");\n return `${row.map((s) => `\"${s}\"`).join(\",\")}\\n`;\n}\n\n// TODO: move logic to @olinfo/quizms\nexport function scoreboardFormatter(\n contest: Contest,\n variants: Record<string, Variant>,\n student: Student,\n) {\n const row: string[] = [];\n for (const field of contest.userData) {\n if (field.type === \"date\") {\n const value = student.userData?.[field.name];\n if (value) {\n row.push(formatISO(value, { representation: \"date\" }));\n } else {\n row.push(\"\");\n }\n } else {\n row.push(formatUserData(student, field));\n }\n }\n row.push(student.venueId?.split(\"-\")[0] ?? \"\");\n row.push(student.variantId ?? \"\");\n const unshuflledProblems: Record<string, [string, number]> = {};\n if (\n student.answers !== undefined &&\n student.variantId !== undefined &&\n variants[student.variantId] !== undefined\n ) {\n const schema = variants[student.variantId].schema;\n for (const problemId in schema) {\n const problem = schema[problemId];\n const originalId = problem.originalId;\n const answer = student.answers[problemId];\n unshuflledProblems[originalId] = [\n answer?.value\n ? displayAnswer({\n type: answer.type,\n value: unshuffleAnswer(problem, answer.value) as any,\n })\n : \"\",\n calcProblemPoints(problem, answer),\n ];\n }\n }\n for (const problemId of contest.problemIds) {\n if (unshuflledProblems[problemId] !== undefined) {\n const [answer, points] = unshuflledProblems[problemId];\n row.push(`${answer}`);\n row.push(`${points}`);\n } else {\n row.push(\"\");\n row.push(\"\");\n }\n }\n row.push(`${student.score ?? \"\"}`);\n row.push(`${student.absent ? 1 : 0}`);\n return row.map((s) => `\"${s}\"`).join(\",\");\n}\n\nasync function saveExport<T>(\n db: Firestore,\n collection: string,\n converter: FirestoreDataConverter<T>,\n options: QueryOption<T>,\n formatter: (data: T) => string,\n suggestedName: string | undefined,\n header?: string,\n) {\n const handle = await window.showSaveFilePicker({\n suggestedName: suggestedName ?? `${collection}.jsonl`,\n });\n\n const writableStream = await handle.createWritable();\n\n if (header !== undefined) {\n await writableStream.write(header);\n }\n\n const chunkSize = 10_000;\n\n let last: DocumentSnapshot<T> | undefined;\n for (;;) {\n const q = query(db, collection, converter, {\n ...options,\n limit: chunkSize,\n startAfter: last,\n });\n\n const snapshot = await getDocs(q);\n if (snapshot.empty) break;\n\n for (const doc of snapshot.docs) {\n await writableStream.write(`${formatter(doc.data())}\\n`);\n }\n\n last = snapshot.docs.at(-1);\n }\n\n await writableStream.close();\n}\n"],"names":["Export","props","ref","useRef","jsxs","Fragment","Button","jsx","ExportModal","label","description","collection","converter","options","formatter","suggestedName","header","contest","useAdmin","db","useDb","onExport","saveExport","Modal","TriangleAlert","scoreboradHeader","row","field","problemId","s","scoreboardFormatter","variants","student","value","formatISO","formatUserData","unshuflledProblems","schema","problem","originalId","answer","displayAnswer","unshuffleAnswer","calcProblemPoints","points","writableStream","chunkSize","last","q","query","snapshot","getDocs","doc"],"mappings":"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"}