diff --git a/admin-frontend/package-lock.json b/admin-frontend/package-lock.json index bf8559a3251f0b879b8fa8efcc833d620b785d70..2888474ce00bdb8cea8024f530afe7f879be925f 100644 --- a/admin-frontend/package-lock.json +++ b/admin-frontend/package-lock.json @@ -1178,6 +1178,11 @@ "to-fast-properties": "^2.0.0" } }, + "@bb-tech/ra-treemenu": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@bb-tech/ra-treemenu/-/ra-treemenu-1.0.5.tgz", + "integrity": "sha512-uYRWfGBmgSd5/qcBC560NreX6OVCNw8x+lKyvmqFtVqpCX2M0OH5mQx7IV6jZMRK3L5G2lZVLgwNxHNoGFB/Sw==" + }, "@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", diff --git a/admin-frontend/package.json b/admin-frontend/package.json index 3b62760b51286c641618e52585888f21175b2f7d..abfce6c55c1226114eadecf8c77216b30237b0db 100644 --- a/admin-frontend/package.json +++ b/admin-frontend/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@bb-tech/ra-treemenu": "^1.0.5", "@testing-library/jest-dom": "^5.12.0", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", @@ -17,7 +18,7 @@ "web-vitals": "^1.1.2" }, "scripts": { - "start": "react-scripts start", + "start": "PORT=4000 react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" @@ -40,4 +41,4 @@ "last 1 safari version" ] } -} +} \ No newline at end of file diff --git a/admin-frontend/src/App.js b/admin-frontend/src/App.js index da714a5f1bd7e9c9a83118acaf575a954f08a598..09f16c1daf91766bd8fe3dcd3b7a196b11d1d6a0 100644 --- a/admin-frontend/src/App.js +++ b/admin-frontend/src/App.js @@ -2,13 +2,18 @@ import * as React from 'react'; import { Admin, Resource } from 'react-admin'; import dataProviderMapper from './dataProviderMapper'; +import TreeMenu from '@bb-tech/ra-treemenu'; import { SportList, SportEdit, SportCreate } from './sportList'; import { IncompleteList, IncompleteEdit } from './incompleteList'; import { QuestionList, QuestionEdit, QuestionCreate } from './questionList'; import { ScraperList } from './scraperList'; import { orderList } from './questionOrderList'; +import { archiveList } from './archiveList'; +import { criteriaList } from './criteriaList'; +import RestoreFromTrashIcon from '@material-ui/icons/RestoreFromTrash'; +import SportsKabaddiIcon from '@material-ui/icons/SportsKabaddi'; import QuestionAnswerIcon from '@material-ui/icons/QuestionAnswer'; import SportsFootballIcon from '@material-ui/icons/SportsFootball'; import ListAltIcon from '@material-ui/icons/ListAlt'; @@ -20,47 +25,69 @@ const App = () => ( <Admin dataProvider={dataProviderMapper} // A custom mapper is used because different resources need different dataProviders disableTelemetry + menu={TreeMenu} > + <Resource + name='sportarten' + options={{ "label": "Sportarten", "isMenuParent": true }} + /> + <Resource name='sport' // name of the API endpoint icon={SportsFootballIcon} - options={{ label: 'Sport Management' }} // if we do not rename the resource, the API name will be used + options={{ label: 'Management', "menuParent": "sportarten" }} // if we do not rename the resource, the API name will be used list={SportList} edit={SportEdit} create={SportCreate} /> + <Resource + name='sport-scraper' + icon={YoutubeSearchedForIcon} + options={{ label: 'Buchsys Auslesen', "menuParent": "sportarten" }} + list={ScraperList} + /> + <Resource name='sport-incomplete' icon={ListAltIcon} - options={{ label: 'Incomplete Sports' }} + options={{ label: 'Fehlende Kriterien', "menuParent": "sportarten" }} list={IncompleteList} edit={IncompleteEdit} /> + <Resource + name='sport-archive' + icon={RestoreFromTrashIcon} + options={{ label: 'Archiv', "menuParent": "sportarten" }} + list={archiveList} + /> + + <Resource name='fragen' options={{ "label": "Fragen", "isMenuParent": true }} /> + <Resource name='question' icon={QuestionAnswerIcon} - options={{ label: 'Question Management' }} + options={{ label: 'Management', "menuParent": "fragen" }} list={QuestionList} edit={QuestionEdit} create={QuestionCreate} /> - <Resource - name='sport-scraper' - icon={YoutubeSearchedForIcon} - options={{ label: 'Sportarten auslesen' }} - list={ScraperList} - /> - <Resource name='question-order' icon={FormatListNumberedIcon} - options={{ label: 'Fragenreihenfolge' }} + options={{ label: 'Reihenfolge', "menuParent": "fragen" }} list={orderList} /> + + <Resource + name='criteria' + icon={SportsKabaddiIcon} + options={{ label: 'Kriterienübersicht', "menuParent": "fragen" }} + list={criteriaList} + /> </Admin> ); diff --git a/admin-frontend/src/archiveList.js b/admin-frontend/src/archiveList.js new file mode 100644 index 0000000000000000000000000000000000000000..2dd8921d8778ca83e41213d88774ce217c149b5e --- /dev/null +++ b/admin-frontend/src/archiveList.js @@ -0,0 +1,81 @@ +import { + List, + Datagrid, + UrlField, + TextField, + DateField, + Button, + useDataProvider, + useNotify, + useRefresh, +} from 'react-admin'; + + +import Typography from '@material-ui/core/Typography' +import Card from '@material-ui/core/Card' +import CardContent from '@material-ui/core/CardContent' + +const ArchiveCriterionList = () => ( + <div style={{ + width: 400, + margin: '1em' + }}> + <Card> + <CardContent> + <Typography + variant="h4" + align="center" + > + Tipps&Tricks + </Typography> + <br /> + <ul> + <li>Dies ist das Archiv für inaktive Sportarten.</li> + <br /> + <li>Dazu zählen Sportarten, die beim letzten Auslesen von <a href="https://www.buchsys.de/fu-berlin/angebote/aktueller_zeitraum/index.html">buchsys.de</a> nicht mehr erkannt worden sind und deswegen aus der Liste von aktiven Sportarten genommen wurden.</li> + <br /> + <li>Die Sportarten können trotzdem manuell per Knopfdruck wieder der aktiven Sportartenliste zugeordnet werden.</li> + <br /> + </ul> + + </CardContent> + </Card> + </div> +); + + +const ActivateButton = props => { + const notify = useNotify(); + const refresh = useRefresh(); + const dataProvider = useDataProvider(); + + let id = props.record.id; + + const activate_sport = props => { + dataProvider.update('sport', { id: id, data: { currently_active: true } }).then( + response => { + notify("Erfolgreich Aktiviert!"); + refresh(); + }).catch(error => { + notify("Etwas ist schief gelaufen!", "warning"); + }); + + } + + return <Button label="Aktivieren!" onClick={activate_sport} {...props} /> +} + +export const archiveList = props => ( + <List {...props} + aside={<ArchiveCriterionList />} + bulkActionButtons={false}> + <Datagrid> + <TextField source="id" sortable={false} /> + <TextField source="name" sortable={false} /> + <UrlField source="url" sortable={false} /> + <DateField source="last_used" sortable={false} /> + + <ActivateButton /> + </Datagrid> + </List> +); diff --git a/admin-frontend/src/criteriaList.js b/admin-frontend/src/criteriaList.js new file mode 100644 index 0000000000000000000000000000000000000000..694aa2f2c90b3d5d03614125ef67dcfca12b1f76 --- /dev/null +++ b/admin-frontend/src/criteriaList.js @@ -0,0 +1,73 @@ +import { + List, + Datagrid, + TextField, + ReferenceField, + ChipField +} from 'react-admin'; + + +import Typography from '@material-ui/core/Typography' +import Card from '@material-ui/core/Card' +import CardContent from '@material-ui/core/CardContent' + + +const AsideCriterionList = () => ( + <div style={{ + width: 400, + margin: '1em' + }}> + <Card> + <CardContent> + <Typography + variant="h4" + align="center" + > + Tipps&Tricks + </Typography> + <br /> + <ul> + <li>Hier wird jedes Kriterium aufgelistet</li> + <br /> + <ul> + <li><i>Anzahl von aktiven Sportarten</i>: <br />Gibt an, wie viele Sportarten ein Rating größer als 1 für dieses Kriterium haben.</li> + <br /> + <li><i>Summe der Gewichte</i>: <br />Gibt die Summe der Gewichte von aktiven Sportarten für dieses Kriterium an.</li> + <br /> + + </ul> + + </ul> + + </CardContent> + </Card> + </div> +); + +export const criteriaList = props => ( + <List {...props} + aside={<AsideCriterionList />} + bulkActionButtons={false}> + <Datagrid> + + <TextField source="id" sortable={false} /> + <TextField source="name" sortable={false} /> + + <TextField + source="number_of_sports_active" + label="Anzahl von aktiven Sportarten" + sortable={false} /> + + <TextField + source="sum_of_weights" + label="Summe der Gewichte" + sortable={false} /> + + + <ReferenceField source="question_id" reference="question" label="Frage" sortable={false}> + <ChipField source="text_de" sortable={false} /> + </ReferenceField> + + </Datagrid> + </List> +); diff --git a/admin-frontend/src/dataProviderMapper.js b/admin-frontend/src/dataProviderMapper.js index 46559858b56e8ba94d8837da9ea5e00d0297ac08..7f549895df755439380e0fc387a18cda965f95e7 100644 --- a/admin-frontend/src/dataProviderMapper.js +++ b/admin-frontend/src/dataProviderMapper.js @@ -20,7 +20,7 @@ import { const dataProviders = [ { dataProvider: drfProvider('http://localhost:8000/api/admin'), - resources: ['sport', 'question'], + resources: ['sport', 'question', 'sport-archive', 'criteria'], }, { dataProvider: sportIncompleteProvider('http://localhost:8000/api/admin'), diff --git a/admin-frontend/src/incompleteList.js b/admin-frontend/src/incompleteList.js index 6f89829b635a458782b944a6c6eb63bf8b0537e2..7abf290bc690f97ce77a51979eae641967c8d6e1 100644 --- a/admin-frontend/src/incompleteList.js +++ b/admin-frontend/src/incompleteList.js @@ -117,6 +117,7 @@ export const IncompleteEdit = props => { {...props} mutationMode="pessimistic" onSuccess={onSuccess} + title={" "} > <SimpleForm toolbar={<IncompleteEditToolbar />}> diff --git a/admin-frontend/src/questionList.js b/admin-frontend/src/questionList.js index b18097efac3d7d2f9709d92609f4a1e2fe09f2ec..a11029f8a985f82a2aba6da4a5ed1fb172ae9810 100644 --- a/admin-frontend/src/questionList.js +++ b/admin-frontend/src/questionList.js @@ -6,7 +6,8 @@ import { Edit, SimpleForm, TextInput, - Create + Create, + NumberField } from 'react-admin'; import Typography from '@material-ui/core/Typography' @@ -31,6 +32,8 @@ const AsideQuestionList = () => ( <li>Fragen müssen eine deutsche Übersetzung und können eine englische Übersetzung haben.</li> <br /> <li>Kriterien sind einzigartig. Das heißt, dass ein Kriterium nicht doppelt vergeben werden kann!</li> + <br /> + <li>Wenn eine Frage mit ihrem Kriterium gelöscht wird, gehen auch alle Gewichtungen zu diesem Kriterium verloren!</li> </ul> </CardContent> @@ -110,7 +113,14 @@ export const QuestionList = props => ( > <Datagrid rowClick="edit" - style={{ tableLayout: "fixed", }}> + > + + <NumberField + source="id" + label="ID" + sortable={false} + /> + <TextField source="text_de" label="Deutscher Fragetext" @@ -135,7 +145,7 @@ export const QuestionList = props => ( export const QuestionEdit = props => ( <Edit {...props} - mutationMode="pessimistic" + mutationMode="undoable" aside={<AsideQuestionEdit />} > <SimpleForm> diff --git a/admin-frontend/src/questionOrderList.js b/admin-frontend/src/questionOrderList.js index ab9f9dc7a427f04a5d8e46b7fa69126789e9af59..954ba334e00f3880e00e3224ebe3cd49cb105daf 100644 --- a/admin-frontend/src/questionOrderList.js +++ b/admin-frontend/src/questionOrderList.js @@ -16,9 +16,13 @@ import { RadioButtonGroupInput, useRefresh, useListContext, - Pagination + Pagination, } from 'react-admin'; +import Chip from '@material-ui/core/Chip'; +import { makeStyles } from '@material-ui/core/styles'; + + import Typography from '@material-ui/core/Typography'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; @@ -162,6 +166,37 @@ const DirEditButton = props => { return <Button label={props.custom_label} onClick={action} /> } +const useStyles = makeStyles({ + question: { color: 'white', background: 'blue' }, + snack: { color: 'black', background: 'lightGreen' }, + activity: { color: 'white', background: 'darkRed' }, +}); + +const QuestionTypeChip = props => { + const classes = useStyles(); + + if (props.record === undefined) { + return null; + }; + + + let verbose_type = ""; + if (props.record.type === "question") { + verbose_type = "Frage"; + } else if (props.record.type === "snack") { + verbose_type = "Wissenssnack"; + } else { + verbose_type = "Aktivität"; + }; + + return ( + <Chip label={verbose_type} className={classes[props.record.type]} /> + ); +}; + + + + const QuestionOrderPagination = props => <Pagination rowsPerPageOptions={[]} {...props} />; export const orderList = props => ( @@ -174,9 +209,9 @@ export const orderList = props => ( pagination={<QuestionOrderPagination />} perPage={1000} bulkActionButtons={false} + empty={false} > <Datagrid - rowClick="expand" expand={orderEdit} > <TextField @@ -184,11 +219,12 @@ export const orderList = props => ( source="id" sortable={false} /> - <TextField + <QuestionTypeChip label="Eintragstyp" source="type" sortable={false} /> + <TextField label="Fragen-ID" source="question_id" @@ -261,7 +297,7 @@ const CategorySensitiveQuestionField = (props) => { export const orderEdit = props => ( - <Edit {...props} mutationMode={"optimistic"}> + <Edit {...props} mutationMode={"optimistic"} title={" "}> <SimpleForm toolbar={<OrderEditToolbar />}> <RadioButtonGroupInput diff --git a/admin-frontend/src/questionOrderProvider.js b/admin-frontend/src/questionOrderProvider.js index d6e931f915d7139083830f5c8e6216509925100b..d67caee64fc89af1ee570b390a16d4f4250af377 100644 --- a/admin-frontend/src/questionOrderProvider.js +++ b/admin-frontend/src/questionOrderProvider.js @@ -61,8 +61,6 @@ export default ( ...getOrderingQuery(params.sort), }; - console.log("cached_order in getList is: ", cached_order) - if (cached_order == undefined) { const url = `${apiUrl}/${resource}/?${stringify(query)}`; @@ -162,8 +160,6 @@ export default ( cached_order.data[index].id = index + 1; } - console.log(cached_order); - return { data: {} }; }, diff --git a/admin-frontend/src/scraperList.js b/admin-frontend/src/scraperList.js index 93dd2eea0fa2391d796c88c251ecef29d9585b68..cb0e7097e5593440b6a2a9122826f362a71ff50d 100644 --- a/admin-frontend/src/scraperList.js +++ b/admin-frontend/src/scraperList.js @@ -2,11 +2,16 @@ import { List, Datagrid, TextField, - NumberField, DateField, Pagination, + ReferenceField, + ChipField } from 'react-admin'; + +import Chip from '@material-ui/core/Chip'; +import { makeStyles } from '@material-ui/core/styles'; + import Typography from '@material-ui/core/Typography'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; @@ -31,10 +36,10 @@ const AsideSportScrape = () => ( </p> <p>Diese werden verglichen mit den Sportarten, die momentan in der Sportliste stehen. Hierbei kann es zu vier verschiedenen Fällen kommen:</p> <ul> - <li><i>Same:</i> Der gefundene Sport steht bereits in der Datenbank und wird bis auf die URL nicht aktualisiert.</li> - <li><i>New:</i> Der gefundene Sport existiert in der Datenbank noch nicht und wird vollkommen neu angelegt. Die Kriterien müssen hier noch ausgefüllt werden.</li> - <li><i>From Archive:</i> Der gefundene Sport existiert zwar, liegt momentan aber in dem Archiv. Der Sport wird wieder aktiviert werden, und die alten Kriterienbewertungen werden von diesem Sport übernommen werden. </li> - <li><i>To be archived:</i> Es wurde kein neuer Sport gefunden, der dem Sport in der Datenbank zu gleichen scheint, also wird der Sport in das Archiv geschoben, um nicht mehr als Sportempfehlung angezeigt zu werden.</li> + <li><i style={{ color: 'black', background: 'lightGrey' }}>Gleicher Sport:</i> <br /> Der gefundene Sport steht bereits in der Datenbank und wird bis auf die URL nicht aktualisiert.</li> + <li><i style={{ color: 'black', background: '#A5FF7E' }}>Neuer Sport:</i> <br /> Der gefundene Sport existiert in der Datenbank noch nicht und wird vollkommen neu angelegt. Die Kriterien müssen hier noch ausgefüllt werden.</li> + <li><i style={{ color: 'black', background: '#FFFB8C' }}>Sport aus Archiv:</i> <br /> Der gefundene Sport existiert zwar, liegt momentan aber in dem Archiv. Der Sport wird wieder aktiviert werden, und die alten Kriterienbewertungen werden von diesem Sport übernommen werden. </li> + <li><i style={{ color: 'black', background: '#FF8EAA' }}>Sport ins Archiv:</i> <br /> Es wurde kein neuer Sport gefunden, der dem Sport in der Datenbank zu gleichen scheint, also wird der Sport in das Archiv geschoben, um nicht mehr als Sportempfehlung angezeigt zu werden.</li> </ul> </li> <li> @@ -46,6 +51,40 @@ const AsideSportScrape = () => ( </div> ); + + + +const useStyles = makeStyles({ + same: { color: 'black', background: 'lightGrey' }, + new: { color: 'black', background: '#A5FF7E' }, + from_archive: { color: 'black', background: '#FFFB8C' }, + to_be_archived: { color: 'black', background: '#FF8EAA' }, +}); + +const DiffTypeChip = props => { + const classes = useStyles(); + + let verbose_type = ""; + if (props.record.kind_of_diff === "new") { + verbose_type = "Neuer Sport"; + } else if (props.record.kind_of_diff === "same") { + verbose_type = "Gleicher Sport"; + } else if (props.record.kind_of_diff === "from_archive") { + verbose_type = "Sport aus Archiv"; + } else { + verbose_type = "Sport ins Archiv"; + }; + + return ( + <Chip label={verbose_type} className={classes[props.record.kind_of_diff]} /> + ); +}; + + + + + + const ScraperPagination = props => <Pagination rowsPerPageOptions={[]} {...props} />; /* bulkActionButtons should theoretically be able to take a whole Fragment worth of buttons, but React hates us and we return the feeling*/ @@ -66,19 +105,24 @@ export const ScraperList = props => ( source="id" sortable={false} /> - <TextField + <DiffTypeChip source="kind_of_diff" label="Art der Differenz" sortable={false} /> - <NumberField - source="old_sport.id" - label="Alte ID" - sortable={false} - /> + + <ReferenceField reference="sport" source="old_sport.id" label="Sport ID"> + <ChipField + source="id" + label="Alte ID" + sortable={false} + /> + </ReferenceField> + <DateField source="old_sport.last_used" label="Zuletzt aktiviert" + locales="fr-FR" sortable={false} /> <TextField diff --git a/admin-frontend/src/sportList.js b/admin-frontend/src/sportList.js index 62db9e342781648b59151cba84e219ad6b403f85..f9be8e02cf63446bbed9cb1c6499599d9075a2f5 100644 --- a/admin-frontend/src/sportList.js +++ b/admin-frontend/src/sportList.js @@ -43,7 +43,7 @@ const AsideSportList = () => ( <br /> <li>Neue Sportarten können durch "Create" hinzugefügt werden.</li> <br /> - <li>Die Sportarten können inkl. ihrer Kriterienwertungen über "Export" als .csv-Datei heruntergeladen werden.</li> {/* Das Implementieren */} + <li>Die Sportarten können (momentan noch ohne Kriterien) über "Export" als .csv-Datei heruntergeladen werden.</li> {/* Das Implementieren */} </ul> </CardContent> @@ -134,7 +134,11 @@ export const SportList = props => ( export const SportEdit = props => ( - <Edit {...props} aside={<AsideSportEdit />}> + <Edit + {...props} + aside={<AsideSportEdit />} + mutationMode='pessimistic' + > <SimpleForm> <Typography