From 7dec531beea13ae05fd657a45afa592d3a4a6694 Mon Sep 17 00:00:00 2001 From: Radoslav Popov Date: Sun, 4 Jul 2021 15:16:21 +0200 Subject: [PATCH 01/10] fixing most of the console errors and format the code to make it more readable --- .../modules/protocols/ProtocolDetails.js | 183 +- .../modules/protocols/ProtocolsHome.js | 197 +- .../modules/violations/CommentSection.js | 165 +- .../modules/violations/ViolationDetails.js | 668 ++++--- .../modules/violations/ViolationList.js | 136 +- .../process_protocols/ProcessProtocols.js | 273 +-- .../process_protocols/ProtocolPage.js | 169 +- .../process_protocols/VerifyProtocolInfo.js | 1734 +++++++++-------- 8 files changed, 1881 insertions(+), 1644 deletions(-) diff --git a/src/components/modules/protocols/ProtocolDetails.js b/src/components/modules/protocols/ProtocolDetails.js index 038dd0c..4ba94e1 100644 --- a/src/components/modules/protocols/ProtocolDetails.js +++ b/src/components/modules/protocols/ProtocolDetails.js @@ -9,99 +9,98 @@ import ImageGallery from '../../ImageGallery'; import Loading from '../../layout/Loading'; import { TableStyle } from '../Profile'; -export default props => { - const { authGet } = useContext(AuthContext); - const { protocol } = useParams(); - const history = useHistory(); - const [data, setData] = useState(null); +export default (props) => { + const { authGet } = useContext(AuthContext); + const { protocol } = useParams(); + const history = useHistory(); + const [data, setData] = useState(null); - useEffect(() => { - authGet(`/protocols/${protocol}`).then(res => { - setData(res.data); - }); - }, []); + useEffect(() => { + authGet(`/protocols/${protocol}`).then((res) => { + setData(res.data); + }); + }, []); - const goBack = () => { - history.goBack() - } + const goBack = () => { + history.goBack(); + }; - return( - - -

Протокол {protocol}

-
- { - !data? : -
-

Секция

- - - - Номер - {data.section.id} - - - Адрес - {data.section.place} - - - -
-

Протокол

- - - - Изпратен от (организация) - {data.author.organization.name} - - { - data.assignees.map(assignee => - - Проверява се от - {assignee.firstName} {assignee.lastName} - - ) - } - - Действителни гласове - {data.results.validVotesCount} - - - Недействителни гласове - {data.results.invalidVotesCount} - - - Машинни гласове - {data.results.machineVotesCount} - - - -
-

Резултати

- - - { - data.results.results.map(result => - - {result.party.name} - {result.validVotesCount} - {result.invalidVotesCount} - {result.machineVotesCount} - {result.nonMachineVotesCount} - - ) - } - - -
-

Снимки

- ({ - original: picture.url - }))} - /> -
- } -
- ); + return ( + + +

Протокол {protocol}

+
+ {!data ? ( + + ) : ( +
+

Секция

+ + + + Номер + {data.section.id} + + + Адрес + {data.section.place} + + + +
+

Протокол

+ + + + Изпратен от (организация) + {data.author.organization.name} + + {data.assignees.map((assignee, idx) => ( + + Проверява се от + + {assignee.firstName} {assignee.lastName} + + + ))} + + Действителни гласове + {data.results.validVotesCount} + + + Недействителни гласове + {data.results.invalidVotesCount} + + + Машинни гласове + {data.results.machineVotesCount} + + + +
+

Резултати

+ + + {data.results.results.map((result, idx) => ( + + {result.party.name} + {result.validVotesCount} + {result.invalidVotesCount} + {result.machineVotesCount} + {result.nonMachineVotesCount} + + ))} + + +
+

Снимки

+ ({ + original: picture.url, + }))} + /> +
+ )} +
+ ); }; diff --git a/src/components/modules/protocols/ProtocolsHome.js b/src/components/modules/protocols/ProtocolsHome.js index 274b97a..9444168 100644 --- a/src/components/modules/protocols/ProtocolsHome.js +++ b/src/components/modules/protocols/ProtocolsHome.js @@ -1,25 +1,25 @@ -import React, { useEffect, useContext, useState } from "react"; +import React, { useEffect, useContext, useState } from 'react'; -import { Link, useLocation, useHistory } from "react-router-dom"; +import { Link, useLocation, useHistory } from 'react-router-dom'; -import { AuthContext } from "../../App"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { AuthContext } from '../../App'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faChevronLeft, faChevronRight, faFastForward, faFastBackward, -} from "@fortawesome/free-solid-svg-icons"; +} from '@fortawesome/free-solid-svg-icons'; -import styled from "styled-components"; -import Loading from "../../layout/Loading"; +import styled from 'styled-components'; +import Loading from '../../layout/Loading'; -import ProtocolFilter from "./ProtocolFilter"; +import ProtocolFilter from './ProtocolFilter'; -import firebase from "firebase/app"; -import "firebase/auth"; +import firebase from 'firebase/app'; +import 'firebase/auth'; -import axios from "axios"; +import axios from 'axios'; const TableViewContainer = styled.div` padding: 40px; @@ -97,20 +97,20 @@ export default (props) => { const history = useHistory(); useEffect(() => { - let url = "/protocols"; - - const page = query.get("page"); - const limit = query.get("limit"); - const country = query.get("country"); - const electionRegion = query.get("electionRegion"); - const assignee = query.get("assignee"); - const section = query.get("section"); - const municipality = query.get("municipality"); - const town = query.get("town"); - const cityRegion = query.get("cityRegion"); - const status = query.get("status"); - const organization = query.get("organization"); - const origin = query.get("origin"); + let url = '/protocols'; + + const page = query.get('page'); + const limit = query.get('limit'); + const country = query.get('country'); + const electionRegion = query.get('electionRegion'); + const assignee = query.get('assignee'); + const section = query.get('section'); + const municipality = query.get('municipality'); + const town = query.get('town'); + const cityRegion = query.get('cityRegion'); + const status = query.get('status'); + const organization = query.get('organization'); + const origin = query.get('origin'); if ( page || @@ -125,8 +125,9 @@ export default (props) => { status || organization || origin - ) url += "?"; - + ) + url += '?'; + if (country) url += `country=${country}`; if (electionRegion) url += `&electionRegion=${electionRegion}`; if (municipality) url += `&municipality=${municipality}`; @@ -146,47 +147,47 @@ export default (props) => { setData(res.data); }); }, [ - query.get("page"), - query.get("country"), - query.get("electionRegion"), - query.get("assignee"), - query.get("section"), - query.get("municipality"), - query.get("town"), - query.get("cityRegion"), - query.get("status"), - query.get("organization"), - query.get("origin"), + query.get('page'), + query.get('country'), + query.get('electionRegion'), + query.get('assignee'), + query.get('section'), + query.get('municipality'), + query.get('town'), + query.get('cityRegion'), + query.get('status'), + query.get('organization'), + query.get('origin'), ]); const origin = (apiOrigin) => { switch (apiOrigin) { - case "ti-broish": - return "Ти Броиш"; + case 'ti-broish': + return 'Ти Броиш'; default: return apiOrigin; } }; - + const status = (apiStatus) => { switch (apiStatus) { - case "received": + case 'received': return ( - Получен + Получен ); - case "rejected": + case 'rejected': return ( - + Отхвърлен ); - case "approved": + case 'approved': return ( - Одобрен + Одобрен ); - case "replaced": + case 'replaced': return ( - + Редактиран ); @@ -203,26 +204,26 @@ export default (props) => { return ( - + Първа - + Предишна
{data.meta.currentPage} / {data.meta.totalPages}
- + Следваща - + Последна
@@ -234,50 +235,48 @@ export default (props) => { }; return ( - <> - -

Протоколи

- -
- {!data ? ( - - ) : ( - [ - renderLinks(), - - - - № на секция - Произход - Адрес - Статус + +

Протоколи

+ +
+ {!data ? ( + + ) : ( + <> + {renderLinks()} + + + + № на секция + Произход + Адрес + Статут + + + + {loading ? ( + + + + - - - {loading ? ( - - - + ) : ( + data.items.map((protocol, i) => ( + openProtocol(protocol.id)}> + + {protocol.section.id} + {origin(protocol.origin)} + {protocol.section.place} + {status(protocol.status)} - ) : ( - data.items.map((protocol, i) => ( - openProtocol(protocol.id)}> - - {protocol.section.id} - - {origin(protocol.origin)} - {protocol.section.place} - {status(protocol.status)} - - )) - )} - - , - renderLinks(), - ] - )} -
- + )) + )} + +
+ {renderLinks()} + + )} +
); }; diff --git a/src/components/modules/violations/CommentSection.js b/src/components/modules/violations/CommentSection.js index 7048e82..44c460f 100644 --- a/src/components/modules/violations/CommentSection.js +++ b/src/components/modules/violations/CommentSection.js @@ -10,86 +10,97 @@ import styled from 'styled-components'; import Comment from './Comment'; export const PaginationLinks = styled.div` - padding: 20px; - text-align: center; - - a { - color: #444; - margin: 0 10px; - text-decoration: none; - - &:hover { - color: #777; - } - - &.disabled { - color: #999; - pointer-events: none; - } + padding: 20px; + text-align: center; + + a { + color: #444; + margin: 0 10px; + text-decoration: none; + + &:hover { + color: #777; + } + + &.disabled { + color: #999; + pointer-events: none; } + } `; import { AuthContext } from '../../App'; const useQuery = () => { - return new URLSearchParams(useLocation().search); -} - -export default props => { - const { violation } = useParams(); - const [data, setData] = useState(null); - const { authGet } = useContext(AuthContext); - const query = useQuery(); - - useEffect(() => { - let url = `/violations/${violation}/comments`; - const page = query.get("page"); - const limit = query.get("limit"); - - if(page || limit) url += '?'; - - if(page) url += `page=${page}`; - if(limit) url += `limit=${limit}`; - - authGet(url).then(res => { - setData(res.data); - }); - }, [query.get("page")]); - - - - const newComment = comment => { - setData({...data, meta: {...data.meta, totalItems: data.meta.totalItems + 1}, items: [comment, ...data.items]}); - }; - - return ([ -

- Последни коментари - { - !data? null : data.items.length <= 5? null : - - Виж всички ({data.meta.totalItems}) - - } -

, - !data? : -
- {data.items.length === 0?

Все още няма коментари

: null} - - {data.items.length === 0? null : - data.items.slice(0, 5).map(comment => [ - , -
- ]) - } - { - data.items.length <= 5? null : -

- - Виж всички коментари ({data.meta.totalItems}) - -

- } -
- ]); -}; \ No newline at end of file + return new URLSearchParams(useLocation().search); +}; + +export default (props) => { + const { violation } = useParams(); + const [data, setData] = useState(null); + const { authGet } = useContext(AuthContext); + const query = useQuery(); + + useEffect(() => { + let url = `/violations/${violation}/comments`; + const page = query.get('page'); + const limit = query.get('limit'); + + if (page || limit) url += '?'; + + if (page) url += `page=${page}`; + if (limit) url += `limit=${limit}`; + + authGet(url).then((res) => { + setData(res.data); + }); + }, [query.get('page')]); + + const newComment = (comment) => { + setData({ + ...data, + meta: { ...data.meta, totalItems: data.meta.totalItems + 1 }, + items: [comment, ...data.items], + }); + }; + + return ( + <> +

+ Последни коментари + {!data ? null : data.items.length <= 5 ? null : ( + + Виж всички ({data.meta.totalItems}) + + )} +

+ + {!data ? ( + + ) : ( +
+ {data.items.length === 0 ?

Все още няма коментари

: null} + + {data.items.length === 0 + ? null + : data.items.slice(0, 5).map((comment) => ( + <> + +
+ + ))} + {data.items.length <= 5 ? null : ( +

+ + Виж всички коментари ({data.meta.totalItems}) + +

+ )} +
+ )} + + ); +}; diff --git a/src/components/modules/violations/ViolationDetails.js b/src/components/modules/violations/ViolationDetails.js index d76dfe3..62bf667 100644 --- a/src/components/modules/violations/ViolationDetails.js +++ b/src/components/modules/violations/ViolationDetails.js @@ -3,7 +3,15 @@ import React, { useState, useEffect, useContext } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faChevronLeft, faTimes, faCheck, faEdit, faUpload, faEyeSlash, faDove } from '@fortawesome/free-solid-svg-icons'; +import { + faChevronLeft, + faTimes, + faCheck, + faEdit, + faUpload, + faEyeSlash, + faDove, +} from '@fortawesome/free-solid-svg-icons'; import { ContentPanel } from '../Modules'; import { AuthContext } from '../../App'; @@ -18,343 +26,395 @@ import CommentSection from './CommentSection'; import { formatDateShort, formatTime } from '../../Util'; const UpdatesTable = styled(TableStyle)` - td, td:first-child { - color: #333; - font-weight: normal; - text-align: center; - font-size: 12px; - } + td, + td:first-child { + color: #333; + font-weight: normal; + text-align: center; + font-size: 12px; + } `; const BackButton = styled.button` - cursor: pointer; - border: none; - border-radius: 6px; - background: none; - margin-right: 5px; - font-size: 48px; - - &:hover { - background-color: #eee; - } + cursor: pointer; + border: none; + border-radius: 6px; + background: none; + margin-right: 5px; + font-size: 48px; + + &:hover { + background-color: #eee; + } `; const ViolationStatus = styled.span` - font-weight: bold; - color: ${props => props.color}; - border: 4px solid ${props => props.color}; - border-radius: 5px; - padding: 6px; - vertical-align: top; - display: inline-block; - margin-left: 10px; - font-size: 20px; - margin-top: 5px; + font-weight: bold; + color: ${(props) => props.color}; + border: 4px solid ${(props) => props.color}; + border-radius: 5px; + padding: 6px; + vertical-align: top; + display: inline-block; + margin-left: 10px; + font-size: 20px; + margin-top: 5px; `; const FancyButton = styled.button` - border: none; - padding: 5px 0px; - font-size: 12px; - cursor: pointer; - border-radius: 5px; - box-sizing: border-box; - display: inline-block; - font-weight: bold; - margin: 0 2px; - min-width: 130px; - position: relative; + border: none; + padding: 5px 0px; + font-size: 12px; + cursor: pointer; + border-radius: 5px; + box-sizing: border-box; + display: inline-block; + font-weight: bold; + margin: 0 2px; + min-width: 130px; + position: relative; + color: white; + + &:active:enabled { + top: 5px; + border-bottom: 0; + } + + &:disabled { + cursor: auto; color: white; + } - &:active:enabled { - top: 5px; - border-bottom: 0; - } - - &:disabled { - cursor: auto; - color: white; - } - - &:first-of-type { - margin-left: 0; - } + &:first-of-type { + margin-left: 0; + } `; const FancyButtonGreen = styled(FancyButton)` - background-color: #44e644; - border-bottom: 5px solid #2eae1c; + background-color: #44e644; + border-bottom: 5px solid #2eae1c; - &:hover { - background-color: #2ece2e; - } + &:hover { + background-color: #2ece2e; + } - &:disabled { - background-color: #9efd9e; - border-bottom-color: #9aec8e; - } + &:disabled { + background-color: #9efd9e; + border-bottom-color: #9aec8e; + } `; export const FancyButtonBlue = styled(FancyButton)` - background-color: #36a2e3; - border-bottom: 5px solid #1e70b9; + background-color: #36a2e3; + border-bottom: 5px solid #1e70b9; - &:hover { - background-color: #48b4f4; - } + &:hover { + background-color: #48b4f4; + } - &:disabled { - background-color: #b6e4ff; - border-bottom-color: #b9d4ec; - } + &:disabled { + background-color: #b6e4ff; + border-bottom-color: #b9d4ec; + } `; const FancyButtonYellow = styled(FancyButton)` - background-color: #f9d71c; - border-bottom: 5px solid #c1b718; + background-color: #f9d71c; + border-bottom: 5px solid #c1b718; - &:hover { - background-color: #ffeb47; - } + &:hover { + background-color: #ffeb47; + } - &:disabled { - background-color: #fff6b0; - border-bottom-color: #ece793; - } + &:disabled { + background-color: #fff6b0; + border-bottom-color: #ece793; + } `; const FancyButtonRed = styled(FancyButton)` - background-color: #ff3e3e; - border-bottom: 5px solid #b92121; + background-color: #ff3e3e; + border-bottom: 5px solid #b92121; - &:hover { background-color: #ff5a5a; } + &:hover { + background-color: #ff5a5a; + } - &:disabled { - background-color: #f4d2d2; - border-bottom-color: #dfc1c1; - } + &:disabled { + background-color: #f4d2d2; + border-bottom-color: #dfc1c1; + } `; -export default props => { - const { authGet, authPost, user, authPatch, authDelete } = useContext(AuthContext); - const { violation } = useParams(); - const history = useHistory(); - const [data, setData] = useState(null); - const [buttonLoading, setButtonLoading] = useState({ - assign: false, process: false, reject: false +export default (props) => { + const { authGet, authPost, user, authPatch, authDelete } = + useContext(AuthContext); + const { violation } = useParams(); + const history = useHistory(); + const [data, setData] = useState(null); + const [buttonLoading, setButtonLoading] = useState({ + assign: false, + process: false, + reject: false, + }); + + console.log({ data }); + + useEffect(() => { + authGet(`/violations/${violation}`).then((res) => { + setData(res.data); }); - - console.log({ data }) - - useEffect(() => { - authGet(`/violations/${violation}`).then(res => { - setData(res.data); - }); - }, []); - - const assignYourself = () => { - if(iAmAssignee) { - setButtonLoading({...buttonLoading, assign: true}); - authDelete(`/violations/${violation}/assignees/${user.id}`).then(res => { - setButtonLoading({...buttonLoading, assign: false}); - if(res.data.status === 'Accepted') { - setData({...data, assignees: []}); - } - }); - } else { - setButtonLoading({...buttonLoading, assign: true}); - authPost(`/violations/${violation}/assignees`, {id: user.id}).then(res => { - setButtonLoading({...buttonLoading, assign: false}); - if(res.data.status === 'Accepted') { - setData({...data, assignees: [user], status: 'processing'}); - } - }); + }, []); + + const assignYourself = () => { + if (iAmAssignee) { + setButtonLoading({ ...buttonLoading, assign: true }); + authDelete(`/violations/${violation}/assignees/${user.id}`).then( + (res) => { + setButtonLoading({ ...buttonLoading, assign: false }); + if (res.data.status === 'Accepted') { + setData({ ...data, assignees: [] }); + } } - }; - - const goBack = () => { - history.goBack() - }; - - const processViolation = () => { - setButtonLoading({...buttonLoading, process: true}); - authPost(`/violations/${violation}/process`).then(res => { - setButtonLoading({...buttonLoading, process: false}); - setData(res.data); - }); - }; - - const rejectViolation = () => { - setButtonLoading({...buttonLoading, reject: true}); - authPost(`/violations/${violation}/reject`).then(res => { - setButtonLoading({...buttonLoading, reject: false}); - setData(res.data); - }); - }; - - const publishViolation = () => { - setButtonLoading({...buttonLoading, publish: true}); - authPatch(`/violations/${violation}`, {isPublished: !data.isPublished}).then(res => { - setButtonLoading({...buttonLoading, publish: false}); - setData(res.data); - }); - }; - - const status = apiStatus => { - switch(apiStatus) { - case "received" : return Получен; - case "rejected" : return Отхвърлен; - case "processed" : return Обработен; - case "processing" : return Обработва се; - default: return apiStatus; + ); + } else { + setButtonLoading({ ...buttonLoading, assign: true }); + authPost(`/violations/${violation}/assignees`, { id: user.id }).then( + (res) => { + setButtonLoading({ ...buttonLoading, assign: false }); + if (res.data.status === 'Accepted') { + setData({ ...data, assignees: [user], status: 'processing' }); + } } - }; - - const iAmAssignee = data && data.assignees.length !== 0 && data.assignees[0].id === user.id; - - const assignPossible = () => (data.assignees.length === 0 || iAmAssignee) && !buttonLoading.assign; - const processPossible = () => iAmAssignee && data.status === 'processing' && !buttonLoading.process; - const rejectPossible = () => iAmAssignee && data.status === 'processing' && !buttonLoading.reject; - const publishPossible = () => !buttonLoading.publish; - - return ( - -

- - Обработка на сигнал - {!data? null : status(data.status)} -

- { - !data? : -
-

- {data.assignees.length === 0? 'Свободен за обработка' : [ - `Обработва се от ${data.assignees[0].firstName} ${data.assignees[0].lastName}`, - !iAmAssignee? null : (Вие) - ]} -

- - {buttonLoading.assign? 'Момент...' : - iAmAssignee? [ - , - ' Освободи' - ] : [ - , - ' Обработвай' - ] - } - - - {buttonLoading.process? 'Момент...' : [ - , - ' Приключи' - ]} - - - - {buttonLoading.publish? 'Момент...' : - data.isPublished? [ - , - ' Скрий' - ] : [ - , - ' Публикувай' - ]} - - - {buttonLoading.reject? 'Момент...' : [ - , - ' Отхвърли' - ]} - -
-

Описание

-

{data.description}

-
-

Изпратен от

- - - - Имена - {data.author.firstName} {data.author.lastName} - - - Организация - {data.author.organization? data.author.organization.name : null} - - - Ел. поща - {data.author.email} - - - Телефон - {data.author.phone} - - - -
- { - !data.section? null : -
-

Информация за секция

- - - - Номер - {data.section.id} - - - Град - {data.town.name} - - - Локация - {data.section.place} - - - -
-
- } -

История

-
- - - - Дата - Час - Потребител - Действие - - - - { - data.updates.map(update => - - {formatDateShort(update.timestamp)} - {formatTime(update.timestamp)} - {update.actor.firstName} {update.actor.lastName} - {update.type} - - ) - } - - -
-
- } -
- - {data && data.pictures && data.pictures.length > 0 && - <> -

Снимки

- ({ - original: picture.url - }))} - /> - - } -
- ); + ); + } + }; + + const goBack = () => { + history.goBack(); + }; + + const processViolation = () => { + setButtonLoading({ ...buttonLoading, process: true }); + authPost(`/violations/${violation}/process`).then((res) => { + setButtonLoading({ ...buttonLoading, process: false }); + setData(res.data); + }); + }; + + const rejectViolation = () => { + setButtonLoading({ ...buttonLoading, reject: true }); + authPost(`/violations/${violation}/reject`).then((res) => { + setButtonLoading({ ...buttonLoading, reject: false }); + setData(res.data); + }); + }; + + const publishViolation = () => { + setButtonLoading({ ...buttonLoading, publish: true }); + authPatch(`/violations/${violation}`, { + isPublished: !data.isPublished, + }).then((res) => { + setButtonLoading({ ...buttonLoading, publish: false }); + setData(res.data); + }); + }; + + const status = (apiStatus) => { + switch (apiStatus) { + case 'received': + return Получен; + case 'rejected': + return Отхвърлен; + case 'processed': + return Обработен; + case 'processing': + return ( + Обработва се + ); + default: + return apiStatus; + } + }; + + const iAmAssignee = + data && data.assignees.length !== 0 && data.assignees[0].id === user.id; + + const assignPossible = () => + (data.assignees.length === 0 || iAmAssignee) && !buttonLoading.assign; + const processPossible = () => + iAmAssignee && data.status === 'processing' && !buttonLoading.process; + const rejectPossible = () => + iAmAssignee && data.status === 'processing' && !buttonLoading.reject; + const publishPossible = () => !buttonLoading.publish; + + return ( + +

+ + + + + Обработка на сигнал + + {!data ? null : status(data.status)} +

+ {!data ? ( + + ) : ( +
+

+ {data.assignees.length === 0 + ? 'Свободен за обработка' + : [ + `Обработва се от ${data.assignees[0].firstName} ${data.assignees[0].lastName}`, + !iAmAssignee ? null : ( + (Вие) + ), + ]} +

+ + {buttonLoading.assign + ? 'Момент...' + : iAmAssignee + ? [, ' Освободи'] + : [, ' Обработвай']} + + + {buttonLoading.process + ? 'Момент...' + : [, ' Приключи']} + + + + {buttonLoading.publish + ? 'Момент...' + : data.isPublished + ? [, ' Скрий'] + : [, ' Публикувай']} + + + {buttonLoading.reject + ? 'Момент...' + : [, ' Отхвърли']} + +
+

Описание

+

{data.description}

+
+

Изпратен от

+ + + + Имена + + {data.author.firstName} {data.author.lastName} + + + + Организация + + {data.author.organization + ? data.author.organization.name + : null} + + + + Ел. поща + {data.author.email} + + + Телефон + {data.author.phone} + + + +
+ {!data.section ? null : ( +
+

Информация за секция

+ + + + Номер + {data.section.id} + + + Град + {data.town.name} + + + Локация + {data.section.place} + + + +
+
+ )} +

История

+
+ + + + Дата + Час + Потребител + Действие + + + + {data.updates.map((update) => ( + + {formatDateShort(update.timestamp)} + {formatTime(update.timestamp)} + + {update.actor.firstName} {update.actor.lastName} + + {update.type} + + ))} + + +
+
+ )} +
+ + {data && data.pictures && data.pictures.length > 0 && ( + <> +

Снимки

+ ({ + original: picture.url, + }))} + /> + + )} +
+ ); }; diff --git a/src/components/modules/violations/ViolationList.js b/src/components/modules/violations/ViolationList.js index 8d38a19..5a7fd28 100644 --- a/src/components/modules/violations/ViolationList.js +++ b/src/components/modules/violations/ViolationList.js @@ -1,19 +1,19 @@ -import React, { useState, useEffect, useContext } from "react"; +import React, { useState, useEffect, useContext } from 'react'; -import { Link, useHistory, useLocation } from "react-router-dom"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Link, useHistory, useLocation } from 'react-router-dom'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faChevronLeft, faChevronRight, faFastForward, faFastBackward, -} from "@fortawesome/free-solid-svg-icons"; +} from '@fortawesome/free-solid-svg-icons'; -import { AuthContext } from "../../App"; -import Loading from "../../layout/Loading"; +import { AuthContext } from '../../App'; +import Loading from '../../layout/Loading'; -import styled from "styled-components"; -import ViolationFilter from "./ViolationFilter"; +import styled from 'styled-components'; +import ViolationFilter from './ViolationFilter'; const TableViewContainer = styled.div` padding: 40px; @@ -104,18 +104,18 @@ export default (props) => { const history = useHistory(); useEffect(() => { - let url = "/violations"; - const page = query.get("page"); - const limit = query.get("limit"); - const country = query.get("country"); - const electionRegion = query.get("electionRegion"); - const assignee = query.get("assignee"); - const section = query.get("section"); - const municipality = query.get("municipality"); - const town = query.get("town"); - const cityRegion = query.get("cityRegion"); - const status = query.get("status"); - const published = query.get("published"); + let url = '/violations'; + const page = query.get('page'); + const limit = query.get('limit'); + const country = query.get('country'); + const electionRegion = query.get('electionRegion'); + const assignee = query.get('assignee'); + const section = query.get('section'); + const municipality = query.get('municipality'); + const town = query.get('town'); + const cityRegion = query.get('cityRegion'); + const status = query.get('status'); + const published = query.get('published'); if ( page || @@ -130,7 +130,7 @@ export default (props) => { status || published ) - url += "?"; + url += '?'; if (country) url += `country=${country}`; if (electionRegion) url += `&electionRegion=${electionRegion}`; @@ -150,41 +150,41 @@ export default (props) => { setData(res.data); }); }, [ - query.get("page"), - query.get("country"), - query.get("electionRegion"), - query.get("assignee"), - query.get("section"), - query.get("municipality"), - query.get("town"), - query.get("cityRegion"), - query.get("status"), - query.get("published"), + query.get('page'), + query.get('country'), + query.get('electionRegion'), + query.get('assignee'), + query.get('section'), + query.get('municipality'), + query.get('town'), + query.get('cityRegion'), + query.get('status'), + query.get('published'), ]); const status = (apiStatus) => { switch (apiStatus) { - case "received": + case 'received': return ( - + Получен ); - case "rejected": + case 'rejected': return ( - + Отхвърлен ); - case "processed": + case 'processed': return ( - + Обработен ); - case "processing": + case 'processing': return ( - + Обработва се ); @@ -212,26 +212,26 @@ export default (props) => { return ( - + Първа - + Предишна
{data.meta.currentPage} / {data.meta.totalPages}
- + Следваща - + Последна
@@ -246,36 +246,46 @@ export default (props) => {

Обработване на сигнали

+
{!data ? ( ) : ( - [ - renderLinks(), + <> + {renderLinks()} - № на секция - Град - Автор - Описание - Публикуван - Статус + + Назначен + № на секция + Град + Автор + Описание + Публикуван + Статут + {loading ? ( - + ) : ( data.items.map((violation, i) => ( - openViolation(violation.id)}> + openViolation(violation.id)} + > + {assignees(violation.assignees)} + + {!violation.section ? ( Не е посочена секция ) : ( @@ -286,12 +296,12 @@ export default (props) => { {violation.author.firstName} {violation.author.lastName} - {violation.description.slice(0, 40) + "..."} + {violation.description.slice(0, 40) + '...'} {violation.isPublished ? ( - Да + Да ) : ( - Не + Не )} {status(violation.status)} @@ -299,9 +309,9 @@ export default (props) => { )) )} - , - renderLinks(), - ] + + ,{renderLinks()} + )}
); diff --git a/src/components/process_protocols/ProcessProtocols.js b/src/components/process_protocols/ProcessProtocols.js index bbfed78..44eb2a4 100644 --- a/src/components/process_protocols/ProcessProtocols.js +++ b/src/components/process_protocols/ProcessProtocols.js @@ -10,149 +10,158 @@ import { faFile, faChevronLeft } from '@fortawesome/free-solid-svg-icons'; import Loading from '../layout/Loading'; const ReadyScreen = styled.div` - max-width: 900px; - display: block; - margin: 0 auto; - padding: 50px; - //border: 1px solid #aaa; - border-radius: 50px; - //margin-top: 30px; - - hr { - margin: 20px 0; - border: 1px solid #ddd; - border-top: 0; - } + max-width: 900px; + display: block; + margin: 0 auto; + padding: 50px; + //border: 1px solid #aaa; + border-radius: 50px; + //margin-top: 30px; + + hr { + margin: 20px 0; + border: 1px solid #ddd; + border-top: 0; + } `; const NextProtocolButton = styled.button` - border: none; - background-color: #22c122; - color: white; - padding: 20px 50px; - font-size: 36px; - cursor: pointer; - margin: 0 auto; - border-radius: 20px; - border-bottom: 10px solid #118a00; - display: block; - margin-top: 60px; - box-sizing: border-box; - position: relative; - top: 0; - - &:hover { - background-color: #2ece2e; - } - - &:active { - top: 10px; - border-bottom: 0; - margin-bottom: 10px; - } + border: none; + background-color: #22c122; + color: white; + padding: 20px 50px; + font-size: 36px; + cursor: pointer; + margin: 0 auto; + border-radius: 20px; + border-bottom: 10px solid #118a00; + display: block; + margin-top: 60px; + box-sizing: border-box; + position: relative; + top: 0; + + &:hover { + background-color: #2ece2e; + } + + &:active { + top: 10px; + border-bottom: 0; + margin-bottom: 10px; + } `; const Message = styled.p` - color: green; - font-size: 20px; - border: 1px solid green; - padding: 10px; - background-color: #e0ffe0; - border-radius: 10px; + color: green; + font-size: 20px; + border: 1px solid green; + padding: 10px; + background-color: #e0ffe0; + border-radius: 10px; `; const BackButton = styled.button` - cursor: pointer; - background: none; - border: 1px solid #aaa; - border-radius: 10px; - margin-right: 10px; + cursor: pointer; + background: none; + border: 1px solid #aaa; + border-radius: 10px; + margin-right: 10px; `; import { AuthContext } from '../App'; -export default props => { - const [protocol, setProtocol] = useState(null); - const [loading, setLoading] = useState(true); - const [message, setMessage] = useState(null); - - const history = useHistory() - - const { token, user, authGet, authDelete, authPost } = useContext(AuthContext); - - useEffect(() => { init(); }, []); - - const init = async () => { - const res = await authGet(`/protocols?status=received&assignee=${user.id}`); - - if(res.data.items.length > 0) { - const res2 = await authGet(`/protocols/${res.data.items[0].id}`); - setProtocol(res2.data); - setLoading(false); - } else { - setLoading(false); - } - }; - - const returnProtocol = async () => { - setLoading(true); - const res = await authDelete(`/protocols/${protocol.id}/assignees/${user.id}`); - - setLoading(false); - setMessage(`Протокол ${protocol.id} ВЪРНАТ без взето решение.`); - setProtocol(null); - }; - - const nextProtocol = async () => { - - setLoading(true); - - const res = await authPost('/protocols/assign'); - - if(res.status === 204) { - setMessage(`Опашката за протоколи е празна`); - } else { - const res2 = await authGet(`/protocols/${res.data.id}`); - setProtocol(res2.data); - } - - setLoading(false); - }; - - const processingDone = message => { - setMessage(message); - setProtocol(null); - }; - - const reorderPictures = newPictures => { - setProtocol({...protocol, pictures: newPictures}) - }; - - return( - loading? : - !protocol? - -

- - - - Валидация на протоколи -

-
- {!message? -

Когато сте готови, натиснете бутона долу и ще ви бъде назначен протокол.

: - {message} - } - - Следващ протокол - -
: - +export default (props) => { + const [protocol, setProtocol] = useState(null); + const [loading, setLoading] = useState(true); + const [message, setMessage] = useState(null); + + const history = useHistory(); + + const { token, user, authGet, authDelete, authPost } = + useContext(AuthContext); + + useEffect(() => { + init(); + }, []); + + const init = async () => { + const res = await authGet(`/protocols?status=received&assignee=${user.id}`); + + if (res.data.items.length > 0) { + const res2 = await authGet(`/protocols/${res.data.items[0].id}`); + setProtocol(res2.data); + setLoading(false); + } else { + setLoading(false); + } + }; + + const returnProtocol = async () => { + setLoading(true); + const res = await authDelete( + `/protocols/${protocol.id}/assignees/${user.id}` ); -}; \ No newline at end of file + + setLoading(false); + setMessage(`Протокол ${protocol.id} ВЪРНАТ без взето решение.`); + setProtocol(null); + }; + + const nextProtocol = async () => { + setLoading(true); + + const res = await authPost('/protocols/assign'); + + if (res.status === 204) { + setMessage(`Опашката за протоколи е празна`); + } else { + const res2 = await authGet(`/protocols/${res.data.id}`); + setProtocol(res2.data); + } + + setLoading(false); + }; + + const processingDone = (message) => { + setMessage(message); + setProtocol(null); + }; + + const reorderPictures = (newPictures) => { + setProtocol({ ...protocol, pictures: newPictures }); + }; + + return loading ? ( + + ) : !protocol ? ( + +

+ + + + Валидация на протоколи +

+
+ {!message ? ( +

+ Когато сте готови, натиснете бутона долу и ще ви бъде назначен + протокол. +

+ ) : ( + {message} + )} + + Следващ протокол + +
+ ) : ( + + ); +}; diff --git a/src/components/process_protocols/ProtocolPage.js b/src/components/process_protocols/ProtocolPage.js index f4c1358..d3f5840 100644 --- a/src/components/process_protocols/ProtocolPage.js +++ b/src/components/process_protocols/ProtocolPage.js @@ -5,109 +5,116 @@ import styled from 'styled-components'; import { SpinnerCircularFixed } from 'spinners-react'; const ProtocolPageImage = styled.img` - ${props => { - const scale = props.zoom / 100; - const w = props.dims.width; - const h = props.dims.height; - const ratio = w / h; + ${(props) => { + const scale = props.zoom / 100; + const w = props.dims.width; + const h = props.dims.height; + const ratio = w / h; - if(props.rotation === 90 || props.rotation === 270) { - return ` + if (props.rotation === 90 || props.rotation === 270) { + return ` width: ${w * ratio * scale}px !important; height: ${w * scale}px !important; position: absolute; //padding: 0 ${((w * (1 - ratio)) / 2) * scale}px; `; - } else { - return ` + } else { + return ` width: ${w * scale}px !important; height: ${h * scale}px !important; padding: 0; `; - } - }} - ${props => { - const w = props.dims.width; - const h = props.dims.height; + } + }} + ${(props) => { + const w = props.dims.width; + const h = props.dims.height; - return props.rotation? ` + return props.rotation + ? ` transform-origin: 50% 50%; transform: rotate(${props.rotation}deg) - ${props.rotation === 90? ` + ${ + props.rotation === 90 + ? ` translate( ${0}px, - ${(w * ((w / h) - 1)) / 2}px - );` : - props.rotation === 270? ` + ${(w * (w / h - 1)) / 2}px + );` + : props.rotation === 270 + ? ` translate( ${0}px, - ${(w * (1 - (w / h))) / 2}px - );` : - `;` - }` : ''}} - ${props => !props.hide? '' : 'display: none;'} + ${(w * (1 - w / h)) / 2}px + );` + : `;` + }` + : ''; + }} + ${(props) => (!props.hide ? '' : 'display: none;')} `; -export default props => { +export default (props) => { + const [loading, setLoading] = useState(true); + const [dims, setDims] = useState({ width: 0, height: 0 }); + const ref = useRef(); - const [loading, setLoading] = useState(true); - const [dims, setDims] = useState({width: 0, height: 0}); - const ref = useRef(); - - useEffect(() => { - window.addEventListener('resize', resizeHandler); - - return () => { - window.removeEventListener('resize', resizeHandler); - }; - }, []); + useEffect(() => { + window.addEventListener('resize', resizeHandler); - const resizeHandler = () => { - if(ref.current) { - const containerWidth = ref.current.parentElement.clientWidth; - const ratio = ref.current.naturalWidth / containerWidth; - setDims({ - width: ref.current.naturalWidth / ratio, - height: ref.current.naturalHeight / ratio, - }); - - const aspectRatio = ref.current.naturalWidth / ref.current.naturalHeight; - props.imageLoaded(ref.current.naturalWidth / ratio * aspectRatio); - } + return () => { + window.removeEventListener('resize', resizeHandler); }; + }, []); - const imageLoaded = ev => { - setLoading(false); - const containerWidth = ev.target.parentElement.clientWidth; - const ratio = ev.target.naturalWidth / containerWidth; - setDims({ - width: ev.target.naturalWidth / ratio, - height: ev.target.naturalHeight / ratio, - }); + const resizeHandler = () => { + if (ref.current) { + const containerWidth = ref.current.parentElement.clientWidth; + const ratio = ref.current.naturalWidth / containerWidth; + setDims({ + width: ref.current.naturalWidth / ratio, + height: ref.current.naturalHeight / ratio, + }); - const aspectRatio = ev.target.naturalWidth / ev.target.naturalHeight; - props.imageLoaded(ev.target.naturalWidth / ratio * aspectRatio); - }; + const aspectRatio = ref.current.naturalWidth / ref.current.naturalHeight; + props.imageLoaded((ref.current.naturalWidth / ratio) * aspectRatio); + } + }; + + const imageLoaded = (ev) => { + setLoading(false); + const containerWidth = ev.target.parentElement.clientWidth; + const ratio = ev.target.naturalWidth / containerWidth; + setDims({ + width: ev.target.naturalWidth / ratio, + height: ev.target.naturalHeight / ratio, + }); + + const aspectRatio = ev.target.naturalWidth / ev.target.naturalHeight; + props.imageLoaded((ev.target.naturalWidth / ratio) * aspectRatio); + }; - return([ - !loading || !props.isCurrentPage? null : -
- -
, - !props.isCurrentPage && !props.preload? null : - - ]); -}; \ No newline at end of file + return ( + <> + !loading || !props.isCurrentPage? null : +
+ +
+ , !props.isCurrentPage && !props.preload? null : + + + ); +}; diff --git a/src/components/process_protocols/VerifyProtocolInfo.js b/src/components/process_protocols/VerifyProtocolInfo.js index 5fbb435..abecc33 100644 --- a/src/components/process_protocols/VerifyProtocolInfo.js +++ b/src/components/process_protocols/VerifyProtocolInfo.js @@ -6,919 +6,1061 @@ import useKeypress from 'react-use-keypress'; import styled from 'styled-components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faChevronLeft, faPlus, faChevronDown } from '@fortawesome/free-solid-svg-icons'; +import { + faChevronLeft, + faChevronDown, +} from '@fortawesome/free-solid-svg-icons'; const ProtocolInfoSection = styled.div` - width: 50vw; - height: 100vh; - overflow-y: auto; - position: absolute; - top: 0; - right: 0; + width: 50vw; + height: 100vh; + overflow-y: auto; + position: absolute; + top: 0; + right: 0; `; const ProtocolDetails = styled.div` - padding: 20px; - - h1 { - margin: 10px 0; - font-size: 17px; - } - - h2 { - font-size: 18px; - } - - hr { - margin: 20px 0; - border: 1px solid #ddd; - border-top: 0; - } + padding: 20px; + + h1 { + margin: 10px 0; + font-size: 17px; + } + + h2 { + font-size: 18px; + } + + hr { + margin: 20px 0; + border: 1px solid #ddd; + border-top: 0; + } `; const SectionHeader = styled.div` - //padding: 10px; - background-color: rgb(56,222,203); - color: white; + //padding: 10px; + background-color: rgb(56, 222, 203); + color: white; `; const SectionInput = styled.div` - width: 100%; + width: 100%; - span { - font-size: 12px; - text-align: center; - display: inline-block; - padding: 3px 0; - border-left: 1px solid #eee; - border-bottom: 1px solid #eee; - box-sizing: border-box; - color: #333; - //font-weight: bold; - - &.last { - border-right: 1px solid #eee; - } + span { + font-size: 12px; + text-align: center; + display: inline-block; + padding: 3px 0; + border-left: 1px solid #eee; + border-bottom: 1px solid #eee; + box-sizing: border-box; + color: #333; + //font-weight: bold; + + &.last { + border-right: 1px solid #eee; } + } - .box { - width: 36px; - height: 44px; - border: 1px solid #eee; - display: inline-block; - box-sizing: border-box; - vertical-align: top; - border-right: none; - - &.last { - border-right: 1px solid #eee; - } + .box { + width: 36px; + height: 44px; + border: 1px solid #eee; + display: inline-block; + box-sizing: border-box; + vertical-align: top; + border-right: none; - &.invalid { - background-color: #ff8f8f; - } + &.last { + border-right: 1px solid #eee; + } - &.changed { - background-color: #fdfd97; - } + &.invalid { + background-color: #ff8f8f; } - input { - letter-spacing: 14.4px; - box-sizing: border-box; - border: none; - font-size: 36px; - font-family: 'Courier New', monospace; - padding: 2px 0 0 6px; - z-index: 30; - display: inline-block; - position: relative; - background: none; - color: #333; - - &:focus { - border: none; - background: none; - outline: none; - } + &.changed { + background-color: #fdfd97; } -`; + } -const BackButton = styled.button` - display: inline-block; - color: white; + input { + letter-spacing: 14.4px; + box-sizing: border-box; border: none; - background: none; font-size: 36px; - cursor: pointer; - padding: 15px; - border-right: 1px solid white; - margin-right: 20px; -`; - -const VerificationPanelButton = styled.button` - border: none; - padding: 5px 10px; - font-size: 26px; - cursor: pointer; - border-radius: 5px; - box-sizing: border-box; + font-family: 'Courier New', monospace; + padding: 2px 0 0 6px; + z-index: 30; display: inline-block; - font-weight: bold; - width: calc(50% - 20px); - margin: 0 10px; position: relative; + background: none; + color: #333; - &:active { - top: 5px; - border-bottom: 0; - margin-bottom: 10px; + &:focus { + border: none; + background: none; + outline: none; } + } +`; - &:disabled { - background-color: #aaa; - cursor: not-allowed; - color: #888; - border-bottom-color: #666; +const BackButton = styled.button` + display: inline-block; + color: white; + border: none; + background: none; + font-size: 36px; + cursor: pointer; + padding: 15px; + border-right: 1px solid white; + margin-right: 20px; +`; - &:hover { - background-color: #aaa; - } +const VerificationPanelButton = styled.button` + border: none; + padding: 5px 10px; + font-size: 26px; + cursor: pointer; + border-radius: 5px; + box-sizing: border-box; + display: inline-block; + font-weight: bold; + width: calc(50% - 20px); + margin: 0 10px; + position: relative; + + &:active { + top: 5px; + border-bottom: 0; + margin-bottom: 10px; + } + + &:disabled { + background-color: #aaa; + cursor: not-allowed; + color: #888; + border-bottom-color: #666; + + &:hover { + background-color: #aaa; } + } `; const AcceptButton = styled(VerificationPanelButton)` - background-color: #44e644; - border-bottom: 5px solid #2eae1c; - color: white; + background-color: #44e644; + border-bottom: 5px solid #2eae1c; + color: white; - &:hover { - background-color: #2ece2e; - } + &:hover { + background-color: #2ece2e; + } `; const CorrectButton = styled(VerificationPanelButton)` - background-color: #f9de00; - border-bottom: 5px solid #a69b00; - color: white; + background-color: #f9de00; + border-bottom: 5px solid #a69b00; + color: white; - &:hover { - background-color: #ffe405; - } + &:hover { + background-color: #ffe405; + } `; const RejectButton = styled(VerificationPanelButton)` - background-color: #ff4545; - border-bottom: 5px solid #ce4c4c; - color: white; + background-color: #ff4545; + border-bottom: 5px solid #ce4c4c; + color: white; - &:hover { - background-color: #ff2626; - } + &:hover { + background-color: #ff2626; + } `; const ApproveAndSendViolationButton = styled(VerificationPanelButton)` - background-color: #f19c48; - border-bottom: 5px solid #eeaa67; - color: white; + background-color: #f19c48; + border-bottom: 5px solid #eeaa67; + color: white; - &:hover { - background-color: #ef8a25; - } + &:hover { + background-color: #ef8a25; + } `; const ProtocolDetailsTable = styled.table` - table-layout: fixed; + table-layout: fixed; - button { - width: 100%; - box-sizing: border-box; - } + button { + width: 100%; + box-sizing: border-box; + } - input { - width: 100%; - box-sizing: border-box; - width: 100%; - box-sizing: border-box; - border: 1px solid #ddd; - padding: 8px; - border-top: 2px solid #ddd; - border-radius: 10px; - text-align: right; - - &.changed { - background-color: #fdfd97; - } + input { + width: 100%; + box-sizing: border-box; + width: 100%; + box-sizing: border-box; + border: 1px solid #ddd; + padding: 8px; + border-top: 2px solid #ddd; + border-radius: 10px; + text-align: right; - &.invalid { - background-color: #ff8f8f; - } + &.changed { + background-color: #fdfd97; } - - select { - width: 100%; - box-sizing: border-box; + + &.invalid { + background-color: #ff8f8f; } + } - td:nth-child(1) { width: 80%; } - td:nth-child(2) { width: 20%; } + select { + width: 100%; + box-sizing: border-box; + } + + td:nth-child(1) { + width: 80%; + } + td:nth-child(2) { + width: 20%; + } `; -const svgIcon = ''; +const svgIcon = + ''; const PartyResultsTable = styled.table` - table-layout: fixed; - width: 100%; - border-collapse: collapse; - - tr:nth-child(odd) td { - background: #ECECEC; - } + table-layout: fixed; + width: 100%; + border-collapse: collapse; - button { - width: 100%; - box-sizing: border-box; - background-color: #aaa; - padding: 6px; - border-radius: 10px; - border: none; - color: white; - font-weight: bold; - cursor: pointer; - } + tr:nth-child(odd) td { + background: #ececec; + } - input { - width: 100%; - box-sizing: border-box; - width: 100%; - box-sizing: border-box; - border: 1px solid #ddd; - padding: 2px 8px; - border-top: 2px solid #ddd; - border-radius: 5px; - text-align: right; - - &.invalid { - background-color: #ff8f8f; - } + button { + width: 100%; + box-sizing: border-box; + background-color: #aaa; + padding: 6px; + border-radius: 10px; + border: none; + color: white; + font-weight: bold; + cursor: pointer; + } - &.changed { - background-color: #fdfd97; - } + input { + width: 100%; + box-sizing: border-box; + width: 100%; + box-sizing: border-box; + border: 1px solid #ddd; + padding: 2px 8px; + border-top: 2px solid #ddd; + border-radius: 5px; + text-align: right; + + &.invalid { + background-color: #ff8f8f; } - - select { - width: 100%; - box-sizing: border-box; - appearance: none; - border: 1px solid #ccc; - border-bottom: 2px solid #ccc; - border-radius: 5px; - padding: 3px; - background: url("data:image/svg+xml;base64,${window.btoa(svgIcon)}") no-repeat; - background-position: right 5px top 50%; - cursor: pointer; + + &.changed { + background-color: #fdfd97; } + } + + select { + width: 100%; + box-sizing: border-box; + appearance: none; + border: 1px solid #ccc; + border-bottom: 2px solid #ccc; + border-radius: 5px; + padding: 3px; + background: url('data:image/svg+xml;base64,${window.btoa(svgIcon)}') + no-repeat; + background-position: right 5px top 50%; + cursor: pointer; + } - ${props => props.isMachine? ` + ${(props) => + props.isMachine + ? ` td:nth-child(1) { width: 8%; } td:nth-child(2) { width: 42%; } td:nth-child(3) { width: 30%; } td:nth-child(4) { width: 20%; } - ` : ` + ` + : ` td:nth-child(1) { width: 8%; } td:nth-child(2) { width: 72%; } td:nth-child(3) { width: 20%; } `} - - `; const PartyNumber = styled.span` - color: ${props => props.textColor? props.textColor : 'white'}; - font-weight: bold; - background-color: #${props => props.color}; - width: 21px; - display: block; - padding: 3px 5px; - text-align: right; - border-radius: 3px; + color: ${(props) => (props.textColor ? props.textColor : 'white')}; + font-weight: bold; + background-color: #${(props) => props.color}; + width: 21px; + display: block; + padding: 3px 5px; + text-align: right; + border-radius: 3px; `; import { AuthContext } from '../App'; import ConfirmationModal from './ConfirmationModal'; -export default props => { - const { parties, authPost, authGet } = useContext(AuthContext); - const [allParties, setAllParties] = useState(true);//Math.random() < 0.5); - const [modalState, setModalState] = useState({isOpen: false}); - const [sectionData, setSectionData] = useState({ - country: null, - electionRegion: null, - municipality: null, - town: null, - townId: null, - cityRegion: null, - address: null, - isMachine: false, - }); - - const ref = useRef(); +export default (props) => { + const { parties, authPost, authGet } = useContext(AuthContext); + const [allParties, setAllParties] = useState(true); //Math.random() < 0.5); + const [modalState, setModalState] = useState({ isOpen: false }); + const [sectionData, setSectionData] = useState({ + country: null, + electionRegion: null, + municipality: null, + town: null, + townId: null, + cityRegion: null, + address: null, + isMachine: false, + }); + + const ref = useRef(); + + useKeypress(['ArrowUp'], (event) => { + let lastInput = null; + + const traverseNodeTree = (node) => { + if (node === document.activeElement && lastInput != null) + lastInput.focus(); + else { + if (node.tagName === 'INPUT') lastInput = node; + [...node.children].forEach(traverseNodeTree); + } + }; - useKeypress(['ArrowUp'], event => { - let lastInput = null; + traverseNodeTree(ref.current); + }); - const traverseNodeTree = node => { - if(node === document.activeElement && lastInput != null) - lastInput.focus(); - else { - if(node.tagName === 'INPUT') lastInput = node; - [...node.children].forEach(traverseNodeTree); - } - }; + useKeypress(['ArrowDown', 'Enter'], (event) => { + let shouldFocus = false; - traverseNodeTree(ref.current); - }); + const traverseNodeTree = (node) => { + if (node.tagName === 'INPUT' && shouldFocus) { + node.focus(); + shouldFocus = false; + } else { + if (node === document.activeElement) shouldFocus = true; + [...node.children].forEach(traverseNodeTree); + } + }; - useKeypress(['ArrowDown', 'Enter'], event => { - let shouldFocus = false; + traverseNodeTree(ref.current); + }); - const traverseNodeTree = node => { - if(node.tagName === 'INPUT' && shouldFocus) { - node.focus(); - shouldFocus = false; - } else { - if(node === document.activeElement) shouldFocus = true; - [...node.children].forEach(traverseNodeTree); - } - }; + const zeroIfEmpty = (value) => (value ? value : ''); //0; + const emptyStrIfNull = (value) => (value || value === 0 ? value : ''); - traverseNodeTree(ref.current); - }); + const [formData, setFormData] = useState({ + sectionId: props.protocol.section.id, + votersCount: zeroIfEmpty(props.protocol.results.votersCount), + validVotesCount: zeroIfEmpty(props.protocol.results.validVotesCount), + invalidVotesCount: zeroIfEmpty(props.protocol.results.invalidVotesCount), + }); - const zeroIfEmpty = value => value? value : '';//0; - const emptyStrIfNull = value => (value || value === 0)? value : ''; + const violationMessage = useRef(''); - const [formData, setFormData] = useState({ - sectionId: props.protocol.section.id, - votersCount: zeroIfEmpty(props.protocol.results.votersCount), - validVotesCount: zeroIfEmpty(props.protocol.results.validVotesCount), - invalidVotesCount: zeroIfEmpty(props.protocol.results.invalidVotesCount), + useEffect(() => { + if (formData.sectionId.length === 9) { + updateSectionData(); + } + }, [formData.sectionId]); + + const updateSectionData = async () => { + setSectionData({ + country: null, + electionRegion: null, + municipality: null, + town: null, + townId: null, + cityRegion: null, + address: null, + isMachine: false, }); - const violationMessage = useRef('') + const res = await authGet(`/sections/${formData.sectionId}`); - useEffect(() => { - if(formData.sectionId.length === 9) { - updateSectionData(); - } - }, [formData.sectionId]) - - const updateSectionData = async () => { - setSectionData({ - country: null, - electionRegion: null, - municipality: null, - town: null, - townId: null, - cityRegion: null, - address: null, - isMachine: false, - }); - - const res = await authGet(`/sections/${formData.sectionId}`); - - const { town, electionRegion, cityRegion, place} = res.data; - - setSectionData({ - country: town.country.name, - electionRegion: electionRegion.name, - municipality: town.municipality? town.municipality.name : null, - town: town.name, - townId: town.id, - cityRegion: !cityRegion? null : cityRegion.name, - address: place, - isMachine: res.data.isMachine, - }); - }; + const { town, electionRegion, cityRegion, place } = res.data; - const initResults = () => { - const resultsObj = { '0': '' }; + setSectionData({ + country: town.country.name, + electionRegion: electionRegion.name, + municipality: town.municipality ? town.municipality.name : null, + town: town.name, + townId: town.id, + cityRegion: !cityRegion ? null : cityRegion.name, + address: place, + isMachine: res.data.isMachine, + }); + }; - for(const party of parties) { - if((allParties? true : party.isFeatured) || party.id.toString() === '0') - resultsObj[party.id] = ''; - resultsObj[`${party.id}m`] = ''; - resultsObj[`${party.id}nm`] = ''; - } + const initResults = () => { + const resultsObj = { 0: '' }; - for(const result of props.protocol.results.results) { - resultsObj[result.party.id] = emptyStrIfNull(result.validVotesCount); - resultsObj[`${result.party.id}m`] = emptyStrIfNull(result.machineVotesCount); - resultsObj[`${result.party.id}nm`] = emptyStrIfNull(result.nonMachineVotesCount); - } + for (const party of parties) { + if ((allParties ? true : party.isFeatured) || party.id.toString() === '0') + resultsObj[party.id] = ''; + resultsObj[`${party.id}m`] = ''; + resultsObj[`${party.id}nm`] = ''; + } - return resultsObj; - }; + for (const result of props.protocol.results.results) { + resultsObj[result.party.id] = emptyStrIfNull(result.validVotesCount); + resultsObj[`${result.party.id}m`] = emptyStrIfNull( + result.machineVotesCount + ); + resultsObj[`${result.party.id}nm`] = emptyStrIfNull( + result.nonMachineVotesCount + ); + } - const [resultsData, setResultsData] = useState(initResults()); + return resultsObj; + }; - const fieldStatus = {}; + const [resultsData, setResultsData] = useState(initResults()); - for(let i = 0; i < 9; i ++) { - const char1 = props.protocol.section.id[i]; - const char2 = formData.sectionId[i]; + const fieldStatus = {}; - if(typeof char1 == 'undefined' || typeof char2 == 'undefined') - fieldStatus[`sectionId${i+1}`] = { invalid: true }; - else if(char1.toString() !== char2.toString()) - fieldStatus[`sectionId${i+1}`] = { changed: true }; - else - fieldStatus[`sectionId${i+1}`] = { unchanged: true }; - } + for (let i = 0; i < 9; i++) { + const char1 = props.protocol.section.id[i]; + const char2 = formData.sectionId[i]; - for(const party of parties) { - if((allParties? true : party.isFeatured) || party.id.toString() === '0') { + if (typeof char1 == 'undefined' || typeof char2 == 'undefined') + fieldStatus[`sectionId${i + 1}`] = { invalid: true }; + else if (char1.toString() !== char2.toString()) + fieldStatus[`sectionId${i + 1}`] = { changed: true }; + else fieldStatus[`sectionId${i + 1}`] = { unchanged: true }; + } - const updateFieldStatus = (apiKey, resultSuffix) => { - let originalResult = ''; - for(const result of props.protocol.results.results) { - if(result.party.id === party.id) { - originalResult = emptyStrIfNull(result[apiKey]); - } - } + for (const party of parties) { + if ((allParties ? true : party.isFeatured) || party.id.toString() === '0') { + const updateFieldStatus = (apiKey, resultSuffix) => { + let originalResult = ''; + for (const result of props.protocol.results.results) { + if (result.party.id === party.id) { + originalResult = emptyStrIfNull(result[apiKey]); + } + } - if(resultsData[`${party.id}${resultSuffix}`] === '') - fieldStatus[`party${party.id}${resultSuffix}`] = { invalid: true }; - else if(originalResult.toString() !== resultsData[`${party.id}${resultSuffix}`].toString()) - fieldStatus[`party${party.id}${resultSuffix}`] = { changed: true }; - else - fieldStatus[`party${party.id}${resultSuffix}`] = { unchanged: true }; - }; + if (resultsData[`${party.id}${resultSuffix}`] === '') + fieldStatus[`party${party.id}${resultSuffix}`] = { invalid: true }; + else if ( + originalResult.toString() !== + resultsData[`${party.id}${resultSuffix}`].toString() + ) + fieldStatus[`party${party.id}${resultSuffix}`] = { changed: true }; + else + fieldStatus[`party${party.id}${resultSuffix}`] = { unchanged: true }; + }; - updateFieldStatus('validVotesCount', ''); + updateFieldStatus('validVotesCount', ''); - if(sectionData.isMachine) { - updateFieldStatus('machineVotesCount', 'm'); - updateFieldStatus('nonMachineVotesCount', 'nm'); + if (sectionData.isMachine) { + updateFieldStatus('machineVotesCount', 'm'); + updateFieldStatus('nonMachineVotesCount', 'nm'); + } + } + } + + const addStatusForResultField = (fieldName) => { + if (formData[fieldName] === '') fieldStatus[fieldName] = { invalid: true }; + else if ( + formData[fieldName] !== zeroIfEmpty(props.protocol.results[fieldName]) + ) + fieldStatus[fieldName] = { changed: true }; + else fieldStatus[fieldName] = { unchanged: true }; + }; + + addStatusForResultField('votersCount'); + addStatusForResultField('validVotesCount'); + addStatusForResultField('invalidVotesCount'); + + const partyRow = (party) => { + const status = fieldStatus[`party${party.id}`]; + const statusM = fieldStatus[`party${party.id}m`]; + const statusNM = fieldStatus[`party${party.id}nm`]; + return !sectionData.isMachine ? ( + + + {party.id.toString() === '0' ? null : party.color ? ( + {party.id} + ) : ( + + {party.id} + + )} + + {party.displayName} + + + + + ) : ( + (( + + + {party.id.toString() === '0' ? null : party.color ? ( + {party.id} + ) : ( + + {party.id} + + )} + + + {party.displayName} + + от бюлетини + + + + + ), + ( + + + от маш. гласове + + + + + ), + ( + + + общо Б + МГ + + + + + )) + ); + }; + + const handleProtocolNumberChange = (e) => { + setFormData({ ...formData, sectionId: e.target.value }); + }; + + const handleResultsChange = (e) => { + const key = `${e.target.dataset.partyId}`; + const newValue = filterNumberFieldInput(e.target.value, resultsData[key]); + setResultsData({ ...resultsData, [key]: newValue }); + }; + + const handleNumberChange = (e) => { + const newValue = filterNumberFieldInput( + e.target.value, + formData[e.target.name] + ); + setFormData({ ...formData, [e.target.name]: newValue }); + }; + + const filterNumberFieldInput = (newValue, oldValue) => { + let isNum = /^\d+$/.test(newValue); + if (isNum) { + return newValue; + } else if (newValue === '') { + return ''; + } else { + return oldValue; } + }; + + const getBoxClass = (boxNum) => { + const status = fieldStatus[`sectionId${boxNum}`]; + return status.invalid + ? 'box invalid' + : status.changed + ? 'box changed' + : 'box'; + }; + + let invalidFields = false; + let changedFields = false; + + for (const key of Object.keys(fieldStatus)) { + if (fieldStatus[key].invalid) invalidFields = true; + if (fieldStatus[key].changed) changedFields = true; + } + + const approveProtocol = async () => { + props.setLoading(true); + await authPost(`/protocols/${props.protocol.id}/approve`); + props.setLoading(false); + props.processingDone(`Протокол ${props.protocol.id} ОДОБРЕН`); + }; + + const approveProtocolAndSendViolation = async () => { + props.setLoading(true); + const data = { + description: violationMessage.current, + town: { + id: sectionData.townId, + name: sectionData.town, + }, + }; + await authPost( + `/protocols/${props.protocol.id}/approve-with-violation`, + data + ); + props.setLoading(false); + props.processingDone( + `Протокол ${props.protocol.id} ОДОБРЕН и СИГНАЛ ИЗПРАТЕН` + ); + }; + + const rejectProtocol = async () => { + props.setLoading(true); + await authPost(`/protocols/${props.protocol.id}/reject`); + props.setLoading(false); + props.processingDone(`Протокол ${props.protocol.id} ОТХВЪРЛЕН`); + }; + + const openConfirmModal = () => { + setModalState({ + isOpen: true, + title: 'Сигурни ли сте?', + message: 'Сигурни ли сте, че искате да потвърдите този протокол?', + warningMessage: performSumCheck(), + confirmButtonName: 'Потвърди', + cancelButtonName: 'Върни се', + confirmHandler: replaceProtocol, + cancelHandler: () => setModalState({ isOpen: false }), + }); + }; + + const openRejectModal = () => { + setModalState({ + isOpen: true, + title: 'Сигурни ли сте?', + message: 'Сигурни ли сте, че искате да отвхрълите този протокол?', + confirmButtonName: 'Отхвърли протокола', + cancelButtonName: 'Върни се', + confirmHandler: rejectProtocol, + cancelHandler: () => setModalState({ isOpen: false }), + }); + }; + + const openApproveAndSendViolationModal = () => { + setModalState({ + isOpen: true, + title: 'Сигурни ли сте?', + message: 'Сигурни ли сте, че искате да потвърдите този протокол?', + messageValue: violationMessage, + messageHandler: (e) => (violationMessage.current = e.target.value), + confirmButtonName: 'Потвърди и изпрати сигнал', + cancelButtonName: 'Върни се', + confirmHandler: approveProtocolAndSendViolation, + cancelHandler: () => setModalState({ isOpen: false }), + }); + }; + + const replaceProtocol = async () => { + const results = {}; + + Object.keys(resultsData).forEach((key) => { + if (key[key.length - 2] === 'n') { + let newKey = key.slice(0, key.length - 2); + if (!results[newKey]) results[newKey] = {}; + results[newKey].nonMachineVotesCount = !sectionData.isMachine + ? null + : parseInt(resultsData[key], 10); + } else if (key[key.length - 1] === 'm') { + let newKey = key.slice(0, key.length - 1); + if (!results[newKey]) results[newKey] = {}; + results[newKey].machineVotesCount = !sectionData.isMachine + ? null + : parseInt(resultsData[key], 10); + } else { + if (!results[key]) results[key] = {}; + results[key].validVotesCount = parseInt(resultsData[key], 10); + } + }); - const addStatusForResultField = fieldName => { - if(formData[fieldName] === '') - fieldStatus[fieldName] = { invalid: true }; - else if(formData[fieldName] !== zeroIfEmpty(props.protocol.results[fieldName])) - fieldStatus[fieldName] = { changed: true }; - else - fieldStatus[fieldName] = { unchanged: true }; + const postBody = { + section: { id: formData.sectionId }, + results: { + invalidVotesCount: parseInt(formData.invalidVotesCount, 10), + validVotesCount: parseInt(formData.validVotesCount, 10), + votersCount: parseInt(formData.votersCount, 10), + results: Object.keys(results).map((key) => { + return { + party: parseInt(key, 10), + validVotesCount: results[key].validVotesCount, + machineVotesCount: results[key].machineVotesCount, + nonMachineVotesCount: results[key].nonMachineVotesCount, + }; + }), + }, + pictures: props.protocol.pictures, }; - addStatusForResultField('votersCount'); - addStatusForResultField('validVotesCount'); - addStatusForResultField('invalidVotesCount'); - - const partyRow = party => { - const status = fieldStatus[`party${party.id}`]; - const statusM = fieldStatus[`party${party.id}m`]; - const statusNM = fieldStatus[`party${party.id}nm`]; - return( - !sectionData.isMachine - ? - - {party.id.toString() === '0'? null : - party.color? - {party.id} : - {party.id} - } - - {party.displayName} - - { + let sum = 0; + for (const key of Object.keys(resultsData)) { + if ( + key[0] !== '0' && + key[key.length - 1] !== 'm' && + !isNaN(resultsData[key]) + ) + sum += parseInt(resultsData[key], 10); + } + + if (sum !== parseInt(formData.validVotesCount, 10)) { + return [ + ` + Сборът на гласовете в т. 7 (${sum}) не се равнява на числото + въведено в т. 6.1 (${formData.validVotesCount}).`, +
, +
, + `Ако грешката идва от протокола, моля не го поправяйте! + `, + ]; + } else return null; + }; + + return ( +
+ + + + + + + + +

+ Секция {props.protocol.section.id} +

+
+ + + + + + - - : [ - - + + +
Номер на секция + +
+
+ -
{party.id.toString() === '0'? null : - party.color? - {party.id} : - {party.id} + +
+
+
+
+
+
+
+
+
+ Район + Община + Адм. ед. + + Секция + +
+ +
+

+ {!sectionData.country + ? null + : ['Държава: ', {sectionData.country}, ', ']} + {!sectionData.electionRegion + ? null + : ['Изборен район: ', {sectionData.electionRegion}, ', ']} +
+ {!sectionData.municipality + ? null + : ['Община: ', {sectionData.municipality}, ', ']} + {!sectionData.town + ? null + : ['Населено място: ', {sectionData.town}, ', ']} +
+ {!sectionData.cityRegion + ? null + : ['Район: ', {sectionData.cityRegion}, ', ']} + {!sectionData.address + ? null + : ['Локация: ', {sectionData.address}]} +

+ + + + + + + + + + + +
+ Изпратен от (организация) + + {props.protocol.author.organization.name} +
Машинна секция{sectionData.isMachine ? 'Да' : 'Не'}
+
+

ДАННИ ОТ ИЗБИРАТЕЛНИЯ СПИСЪК

+ + + + + 2. Брой на гласувалите избиратели според положените подписи в + избирателния списък (вкл. под чертата) + + + - - {party.displayName} - - от бюлетини + value={formData.votersCount} + onChange={handleNumberChange} + /> + + + + +

СЛЕД КАТО ОТВОРИ ИЗБИРАТЕЛНАТА КУТИЯ, СИК УСТАНОВИ

+ + + + + 5. Брой намерени в избирателната кутия недействителни гласове + (бюлетини) + + + + + + + + 6.1. Брой на действителните гласове, подадени за кандидатските + листи на партии, коалиции и ИК + + + + + + {!sectionData.isMachine ? null : ( + <> + - + 6.2а. Брой на намерените в избирателна кутия дейстивтелни + гласове "Не подрекпям никого" - , - - - от маш. гласове - + - , - - - общо Б + МГ + + , + - + 6.2б. Брой гласували за "Не подкрепям никого" от машинното + гласуване - - ] - ); - }; - - const handleProtocolNumberChange = e => { - setFormData({...formData, sectionId: e.target.value}); - }; - - const handleResultsChange = e => { - const key = `${e.target.dataset.partyId}`; - const newValue = filterNumberFieldInput(e.target.value, resultsData[key]); - setResultsData({...resultsData, [key]: newValue}); - }; - - const handleNumberChange = e => { - const newValue = filterNumberFieldInput(e.target.value, formData[e.target.name]); - setFormData({...formData, [e.target.name]: newValue}); - }; - - const filterNumberFieldInput = (newValue, oldValue) => { - let isNum = /^\d+$/.test(newValue); - if(isNum) { - return newValue; - } else if(newValue === '') { - return ''; - } else { - return oldValue; - } - }; - - const getBoxClass = boxNum => { - const status = fieldStatus[`sectionId${boxNum}`]; - return status.invalid? 'box invalid' : status.changed? 'box changed' : 'box'; - }; - - let invalidFields = false; - let changedFields = false; - - for(const key of Object.keys(fieldStatus)) { - if(fieldStatus[key].invalid) - invalidFields = true; - if(fieldStatus[key].changed) - changedFields = true; - } - - const approveProtocol = async () => { - props.setLoading(true); - await authPost(`/protocols/${props.protocol.id}/approve`); - props.setLoading(false); - props.processingDone(`Протокол ${props.protocol.id} ОДОБРЕН`); - }; - - const approveProtocolAndSendViolation = async () => { - props.setLoading(true); - const data = { - description: violationMessage.current, - town: { - id: sectionData.townId, - name: sectionData.town - } - }; - await authPost(`/protocols/${props.protocol.id}/approve-with-violation`, data); - props.setLoading(false); - props.processingDone(`Протокол ${props.protocol.id} ОДОБРЕН и СИГНАЛ ИЗПРАТЕН`); - }; - - const rejectProtocol = async () => { - props.setLoading(true); - await authPost(`/protocols/${props.protocol.id}/reject`); - props.setLoading(false); - props.processingDone(`Протокол ${props.protocol.id} ОТХВЪРЛЕН`); - }; - - const openConfirmModal = () => { - setModalState({ - isOpen: true, - title: 'Сигурни ли сте?', - message: 'Сигурни ли сте, че искате да потвърдите този протокол?', - warningMessage: performSumCheck(), - confirmButtonName: 'Потвърди', - cancelButtonName: 'Върни се', - confirmHandler: replaceProtocol, - cancelHandler: () => setModalState({isOpen: false}) - }); - }; - - const openRejectModal = () => { - setModalState({ - isOpen: true, - title: 'Сигурни ли сте?', - message: 'Сигурни ли сте, че искате да отвхрълите този протокол?', - confirmButtonName: 'Отхвърли протокола', - cancelButtonName: 'Върни се', - confirmHandler: rejectProtocol, - cancelHandler: () => setModalState({isOpen: false}) - }); - }; - - const openApproveAndSendViolationModal = () => { - setModalState({ - isOpen: true, - title: 'Сигурни ли сте?', - message: 'Сигурни ли сте, че искате да потвърдите този протокол?', - messageValue: violationMessage, - messageHandler: (e) => violationMessage.current = e.target.value, - confirmButtonName: 'Потвърди и изпрати сигнал', - cancelButtonName: 'Върни се', - confirmHandler: approveProtocolAndSendViolation, - cancelHandler: () => setModalState({isOpen: false}) - }); - }; - - const replaceProtocol = async () => { - const results = {}; - - Object.keys(resultsData).forEach(key => { - if(key[key.length - 2] === 'n') { - let newKey = key.slice(0, key.length - 2); - if(!results[newKey]) results[newKey] = {}; - results[newKey].nonMachineVotesCount = !sectionData.isMachine? null : parseInt(resultsData[key], 10); - } else if(key[key.length - 1] === 'm') { - let newKey = key.slice(0, key.length - 1); - if(!results[newKey]) results[newKey] = {}; - results[newKey].machineVotesCount = !sectionData.isMachine? null : parseInt(resultsData[key], 10); - } else { - if(!results[key]) results[key] = {}; - results[key].validVotesCount = parseInt(resultsData[key], 10); - } - }); - - const postBody = { - section: { id: formData.sectionId }, - results: { - invalidVotesCount: parseInt(formData.invalidVotesCount, 10), - validVotesCount: parseInt(formData.validVotesCount, 10), - votersCount: parseInt(formData.votersCount, 10), - results: Object.keys(results).map(key => { - return { party: parseInt(key, 10), - validVotesCount: results[key].validVotesCount, - machineVotesCount: results[key].machineVotesCount, - nonMachineVotesCount: results[key].nonMachineVotesCount, - }; - }) - }, - pictures: props.protocol.pictures - }; - - props.setLoading(true); - try { - const res = await authPost(`/protocols/${props.protocol.id}/replace`, postBody); - } catch(err) { - props.setLoading(false); - return; - } - props.setLoading(false); - props.processingDone(`Протокол ${props.protocol.id} ОДОБРЕН с КОРЕКЦИЯ`); - }; - - const performSumCheck = () => { - let sum = 0; - for(const key of Object.keys(resultsData)) { - if(key[0] !== '0' && key[key.length-1] !== 'm' && !isNaN(resultsData[key])) - sum += parseInt(resultsData[key], 10); - } - - if(sum !== parseInt(formData.validVotesCount, 10)) { - return [` - Сборът на гласовете в т. 7 (${sum}) не се равнява на числото - въведено в т. 6.1 (${formData.validVotesCount}).`, -
,
, - `Ако грешката идва от протокола, моля не го поправяйте! - `]; - } else return null; - }; - - return( -
- - - - - - - - -

Секция {props.protocol.section.id}

-
- - - - - - - - -
Номер на секция - -
-
- -
-
-
-
-
-
-
-
-
-
- Район - Община - Адм. ед. - Секция -
- -
-

- {!sectionData.country? null : ['Държава: ', {sectionData.country}, ', ']} - {!sectionData.electionRegion? null : ['Изборен район: ', {sectionData.electionRegion}, ', ']} -
- {!sectionData.municipality? null : ['Община: ', {sectionData.municipality}, ', ']} - {!sectionData.town? null : ['Населено място: ', {sectionData.town}, ', ']} -
- {!sectionData.cityRegion? null : ['Район: ', {sectionData.cityRegion}, ', ']} - {!sectionData.address? null : ['Локация: ', {sectionData.address}]} -

- - - - - - - - - - - -
Изпратен от (организация){props.protocol.author.organization.name}
Машинна секция{sectionData.isMachine? 'Да' : 'Не'}
-
-

ДАННИ ОТ ИЗБИРАТЕЛНИЯ СПИСЪК

- - - - 2. Брой на гласувалите избиратели според положените подписи в избирателния списък (вкл. под чертата) - - - - - - -

СЛЕД КАТО ОТВОРИ ИЗБИРАТЕЛНАТА КУТИЯ, СИК УСТАНОВИ

- - - - 5. Брой намерени в избирателната кутия недействителни гласове (бюлетини) - - - - - - 6.1. Брой на действителните гласове, подадени за кандидатските листи на партии, коалиции и ИК - - - - - { - !sectionData.isMachine? null : [ - - 6.2а. Брой на намерените в избирателна кутия дейстивтелни гласове "Не подрекпям никого" - - - - , - - 6.2б. Брой гласували за "Не подкрепям никого" от машинното гласуване - - - - - ] + + - 6.2. Брой на действителните гласове "Не подкрепям никого" - - - - - - -
-

7. РАЗПРЕДЕЛЕНИЕ НА ГЛАСОВЕТЕ ПО КАНДИДАТСКИ ЛИСТИ

- - - {parties.map(party => - !((allParties? true : party.isFeatured) && party.id.toString() !== '0') - ? null - : partyRow(party))} - - -
- { - invalidFields || changedFields? - - Потвърди - : - - Потвърди - + data-party-id={'0m'} + value={resultsData['0m']} + onChange={handleResultsChange} + /> + + + , + + )} + + + 6.2. Брой на действителните гласове "Не подкрепям никого" + + + - Отхвърли - -
-
-
- ); + data-party-id={'0'} + value={resultsData['0']} + onChange={handleResultsChange} + /> + + + +
+
+

7. РАЗПРЕДЕЛЕНИЕ НА ГЛАСОВЕТЕ ПО КАНДИДАТСКИ ЛИСТИ

+ + + {parties.map((party) => + !( + (allParties ? true : party.isFeatured) && + party.id.toString() !== '0' + ) + ? null + : partyRow(party) + )} + + +
+ {invalidFields || changedFields ? ( + + Потвърди + + ) : ( + Потвърди + )} + Отхвърли +
+
+
+ ); }; From 1a6c33118be522c1fd621c094b4e3b81567d6b95 Mon Sep 17 00:00:00 2001 From: Radoslav Popov Date: Sun, 4 Jul 2021 15:17:45 +0200 Subject: [PATCH 02/10] add new admin module and create detail page --- src/components/layout/Tabs.js | 77 +++++++ src/components/modules/Admin.js | 174 ---------------- src/components/modules/Modules.js | 15 +- src/components/modules/admin/Admin.js | 197 ++++++++++++++++++ src/components/modules/admin/AdminDetails.js | 28 +++ src/components/modules/admin/AdminOverview.js | 22 ++ src/components/modules/layout/Navigation.js | 169 +++++++-------- 7 files changed, 421 insertions(+), 261 deletions(-) create mode 100644 src/components/layout/Tabs.js delete mode 100644 src/components/modules/Admin.js create mode 100644 src/components/modules/admin/Admin.js create mode 100644 src/components/modules/admin/AdminDetails.js create mode 100644 src/components/modules/admin/AdminOverview.js diff --git a/src/components/layout/Tabs.js b/src/components/layout/Tabs.js new file mode 100644 index 0000000..922dd6a --- /dev/null +++ b/src/components/layout/Tabs.js @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from "react"; +import styled from 'styled-components'; + + +const TabsStyle = styled.div` +.custom-tab .tab-content { + min-height: 100px; + border: 1px solid #dcdcdc; + border-top: none; + } + +.nav-tabs { + border-bottom: 1px solid #dee2e6; +} + + .nav { + display: flex; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +` + +const Tabs = ({ children, active = 0 }) => { + const [activeTab, setActiveTab] = useState(active); + const [tabsData, setTabsData] = useState([]); + + useEffect(() => { + let data = []; + + React.Children.forEach(children, (element) => { + if (!React.isValidElement(element)) return; + + const { + props: { tab, children }, + } = element; + data.push({ tab, children }); + }); + + setTabsData(data); + }, [children]); + + return ( + <> + +
+ + +
+ {tabsData[activeTab] && tabsData[activeTab].children} +
+
+
+ + ); +}; + +const TabPane = ({ children }) => { + return { children }; +}; + +Tabs.TabPane = TabPane; + +export default Tabs; \ No newline at end of file diff --git a/src/components/modules/Admin.js b/src/components/modules/Admin.js deleted file mode 100644 index 00d0c2c..0000000 --- a/src/components/modules/Admin.js +++ /dev/null @@ -1,174 +0,0 @@ -import React, { useEffect, useContext, useState } from 'react'; -import { Link, useLocation, useHistory } from 'react-router-dom'; - -import { AuthContext } from '../App'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faChevronLeft, faChevronRight, faFastForward, faFastBackward } from '@fortawesome/free-solid-svg-icons'; - -import styled from 'styled-components'; - -import Loading from '../layout/Loading'; - -const TableViewContainer = styled.div` - padding: 40px; - - hr { - border: 1px solid #ccc; - border-bottom: none; - } -`; - -const PaginationLinks = styled.div` - padding: 20px; - text-align: center; - - a { - color: #444; - margin: 0 10px; - text-decoration: none; - - &:hover { - color: #777; - } - - &.disabled { - color: #999; - pointer-events: none; - } - } -`; - -const UserTable = styled.table` - background-color: white; - width: 100%; - border-collapse: collapse; - box-shadow: 0 0 10px #aaa; - - thead { - background-color: #f5f5f5; - text-align: left; - border-bottom: 2px solid #eee; - - th { padding: 10px; } - } - - td { - padding: 8px 15px; - border: none; - } - - tr { - cursor: pointer; - border-bottom: 1px solid #eaeaea; - - &:hover { - background-color: rgb(202, 255, 249); - } - } -`; - -const useQuery = () => { - return new URLSearchParams(useLocation().search); -} - -export default props => { - const { authGet } = useContext(AuthContext); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const query = useQuery(); - const history = useHistory(); - - useEffect(() => { - let url = '/users'; - const page = query.get("page"); - const limit = query.get("limit"); - - if(page || limit) url += '?'; - - if(page) url += `page=${page}`; - if(limit) url += `limit=${limit}`; - - setLoading(true); - authGet(url).then(res => { - setLoading(false); - setData(res.data); - }); - }, [query.get("page")]); - - const renderLinks = () => { - const firstAvail = data.meta.currentPage !== 1; - const lastAvail = data.meta.currentPage !== data.meta.totalPages; - const nextAvail = data.links.next; - const prevAvail = data.links.previous; - - return ( - - - Първа - - - Предишна - -
- {data.meta.currentPage} / {data.meta.totalPages} -
- - Следваща - - - Последна - -
- ); - }; - - return( - -

Административна секция

-
- { - !data? : [ - renderLinks(), - - - Име - Фамилия - Ел. поща - Телефон - ПИН - Съгласие за данни - Регистриран на - Роли - - - { - loading - ? - : data.items.map((user, i) => - - {user.firstName} - {user.lastName} - {user.email} - {user.phone} - {user.pin} - {user.hasAgreedToKeepData? 'Да' : 'Не'} - {user.registeredAt} - {JSON.stringify(user.roles)} - - ) - } - - , - renderLinks(), -
- Потребители на страница: {query.get("limit")} - 10 - 20 - 50 - 100 -
- ] - } -
- ); -}; \ No newline at end of file diff --git a/src/components/modules/Modules.js b/src/components/modules/Modules.js index 8e6186a..95b3ffc 100644 --- a/src/components/modules/Modules.js +++ b/src/components/modules/Modules.js @@ -3,7 +3,10 @@ import React from 'react'; import Navigation from './layout/Navigation'; import { Switch, Route, Redirect } from 'react-router-dom'; -import Admin from './Admin'; + +import Admin from './admin/Admin'; +import AdminDetails from './admin/AdminDetails' + import Posts from './Posts'; import Profile from './Profile'; @@ -52,10 +55,11 @@ export const ContentPanel = styled.div` `; export default props => { - return([ + return( + <> - , + @@ -65,10 +69,11 @@ export default props => { - {/* */} + + - ]); + ); }; diff --git a/src/components/modules/admin/Admin.js b/src/components/modules/admin/Admin.js new file mode 100644 index 0000000..a95e654 --- /dev/null +++ b/src/components/modules/admin/Admin.js @@ -0,0 +1,197 @@ +import React, { useEffect, useContext, useState } from 'react'; +import { Link, useLocation, useHistory } from 'react-router-dom'; + +import { AuthContext } from '../../App'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faChevronLeft, + faChevronRight, + faFastForward, + faFastBackward, +} from '@fortawesome/free-solid-svg-icons'; + +import styled from 'styled-components'; + +import AdminOverview from './AdminOverview'; + +import Loading from '../../layout/Loading'; + +const TableViewContainer = styled.div` + padding: 40px; + + hr { + border: 1px solid #ccc; + border-bottom: none; + } +`; + +const PaginationLinks = styled.div` + padding: 20px; + text-align: center; + + a { + color: #444; + margin: 0 10px; + text-decoration: none; + + &:hover { + color: #777; + } + + &.disabled { + color: #999; + pointer-events: none; + } + } +`; + +const UserTable = styled.table` + background-color: white; + width: 100%; + border-collapse: collapse; + box-shadow: 0 0 10px #aaa; + + thead { + background-color: #f5f5f5; + text-align: left; + border-bottom: 2px solid #eee; + + th { + padding: 10px; + } + } + + td { + padding: 8px 15px; + border: none; + } + + tr { + cursor: pointer; + border-bottom: 1px solid #eaeaea; + + &:hover { + background-color: rgb(202, 255, 249); + } + } +`; + +const useQuery = () => { + return new URLSearchParams(useLocation().search); +}; + +export default (props) => { + const { authGet } = useContext(AuthContext); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const query = useQuery(); + const history = useHistory(); + + useEffect(() => { + let url = '/users'; + const page = query.get('page'); + const limit = query.get('limit'); + + if (page || limit) url += '?'; + + if (page) url += `page=${page}`; + if (limit) url += `limit=${limit}`; + + setLoading(true); + authGet(url).then((res) => { + setLoading(false); + setData(res.data); + }); + }, [query.get('page')]); + + const renderLinks = () => { + const firstAvail = data.meta.currentPage !== 1; + const lastAvail = data.meta.currentPage !== data.meta.totalPages; + const nextAvail = data.links.next; + const prevAvail = data.links.previous; + + return ( + + + Първа + + + Предишна + +
+ {data.meta.currentPage} / {data.meta.totalPages} +
+ + Следваща + + + Последна + +
+ ); + }; + + const openUser = (id) => { + history.push(`/user/${id}`); + }; + + return ( + <> + +

Административна секция

+
+ {!data ? ( + + ) : ( + <> + {renderLinks()} + + + + Име + Фамилия + Ел. поща + Телефон + ПИН + Съгласие за данни + {/* Регистриран на */} + Роли + + + + {loading ? ( + + + + + + ) : ( + data.items.map((user, i) => ( + openUser(user.id)}> + {user.firstName} + {user.lastName} + {user.email} + {user.phone} + {user.pin} + {user.hasAgreedToKeepData ? 'Да' : 'Не'} + {/* {user.registeredAt} */} + {user.roles.join(', ')} + + )) + )} + + + {renderLinks()} + + )} +
+ + ); +}; diff --git a/src/components/modules/admin/AdminDetails.js b/src/components/modules/admin/AdminDetails.js new file mode 100644 index 0000000..89bfc5a --- /dev/null +++ b/src/components/modules/admin/AdminDetails.js @@ -0,0 +1,28 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { ContentPanel } from '../Modules'; +import { useHistory, useParams } from 'react-router-dom'; +import { AuthContext } from '../../App'; + + +export default props => { + const { authGet } = useContext(AuthContext); + const { userId } = useParams(); + const history = useHistory(); + const [data, setData] = useState(null); + + useEffect(() => { + authGet(`/users/${userId}`).then(res => { + setData(res.data); + }); + }, []); + + const goBack = () => { + history.goBack() + } + + return( + + + + ); +} \ No newline at end of file diff --git a/src/components/modules/admin/AdminOverview.js b/src/components/modules/admin/AdminOverview.js new file mode 100644 index 0000000..d657e61 --- /dev/null +++ b/src/components/modules/admin/AdminOverview.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import Tabs from '../../layout/Tabs' + +export default props => { + return ( + <> + +
+ + + + bla bla + + + test test + + +
+ + ) +} \ No newline at end of file diff --git a/src/components/modules/layout/Navigation.js b/src/components/modules/layout/Navigation.js index 50a7d5f..89b8388 100644 --- a/src/components/modules/layout/Navigation.js +++ b/src/components/modules/layout/Navigation.js @@ -2,113 +2,118 @@ import React, { useContext } from 'react'; import { Link } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - faFileInvoice, faIdBadge, faUsersCog, - faNewspaper, faSatelliteDish, faSignOutAlt, - faCheckSquare, - faPersonBooth +import { + faFileInvoice, + faIdBadge, + faUsersCog, + faNewspaper, + faSatelliteDish, + faSignOutAlt, + faCheckSquare, + faPersonBooth, } from '@fortawesome/free-solid-svg-icons'; import styled from 'styled-components'; import { AuthContext } from '../../App'; const NavigationStyle = styled.nav` - height: 100%; - width: 100%; - background-color: rgb(56, 222, 203); - overflow: hidden; - color: white; + height: 100%; + width: 100%; + background-color: rgb(56, 222, 203); + overflow: hidden; + color: white; `; const NavTitle = styled.div` - padding: 10px; - width: 100%; - box-sizing: border-box; + padding: 10px; + width: 100%; + box-sizing: border-box; - img { width: 100%; } - h1 { margin: 0; } + img { + width: 100%; + } + h1 { + margin: 0; + } `; const NavLinks = styled.div` - background-color: rgb(40, 174, 159); - padding: 50px 0; + background-color: rgb(40, 174, 159); + padding: 50px 0; `; const NavLink = styled(Link)` - color: white; - text-decoration: none; - font-size: 18px; - width: 100%; - display: block; - padding: 5px 10px; - box-sizing: border-box; + color: white; + text-decoration: none; + font-size: 18px; + width: 100%; + display: block; + padding: 5px 10px; + box-sizing: border-box; - &:hover { - background-color: rgb(61, 191, 176); - } + &:hover { + background-color: rgb(61, 191, 176); + } `; const NavFooter = styled.div` - width: 100%; - position: absolute; - bottom: 0; - font-weight: bold; - text-align: center; - padding: 15px 5px; - box-sizing: border-box; + width: 100%; + position: absolute; + bottom: 0; + font-weight: bold; + text-align: center; + padding: 15px 5px; + box-sizing: border-box; `; -export default props => { - const { logOut, user } = useContext(AuthContext); +export default (props) => { + const { logOut, user } = useContext(AuthContext); - const requireRoles = roles => { - for(const role of roles) { - if(user.roles.includes(role)) - return true; - } - return false; - }; + const requireRoles = (roles) => { + for (const role of roles) { + if (user.roles.includes(role)) return true; + } + return false; + }; - return( - - - - - - - Профил - - { !requireRoles(['validator', 'external-validator', 'admin'])? null : - - Проверявай - - } - { !requireRoles(['admin'])? null : - - Протоколи - - } - { !requireRoles(['lawyer', 'admin'])? null : - - Сигнали - - } - {/* { !requireRoles(['admin'])? null : - - Администрация - - } */} - {/* + return ( + + + + + + + Профил + + {!requireRoles(['validator', 'external-validator', 'admin']) ? null : ( + + Проверявай + + )} + {!requireRoles(['admin']) ? null : ( + + Протоколи + + )} + {!requireRoles(['lawyer', 'admin']) ? null : ( + + Сигнали + + )} + {!requireRoles(['admin']) ? null : ( + + Администрация + + )} + {/* Статии */} - - Изход - - - - „Демократична България - обединение“ © 2021 - - - ) + + Изход + + + „Демократична България - обединение“ © 2021 + + ); }; From 439e3f1eef3fe849e83d46ca3e6d081594a994e3 Mon Sep 17 00:00:00 2001 From: Radoslav Popov Date: Sun, 4 Jul 2021 15:39:51 +0200 Subject: [PATCH 03/10] commenting out limit query to fix page navigation --- src/components/modules/admin/Admin.js | 6 +++--- src/components/modules/protocols/ProtocolsHome.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/modules/admin/Admin.js b/src/components/modules/admin/Admin.js index a95e654..97208a6 100644 --- a/src/components/modules/admin/Admin.js +++ b/src/components/modules/admin/Admin.js @@ -90,12 +90,12 @@ export default (props) => { useEffect(() => { let url = '/users'; const page = query.get('page'); - const limit = query.get('limit'); + // const limit = query.get('limit'); - if (page || limit) url += '?'; + if (page) url += '?'; if (page) url += `page=${page}`; - if (limit) url += `limit=${limit}`; + // if (limit) url += `limit=${limit}`; setLoading(true); authGet(url).then((res) => { diff --git a/src/components/modules/protocols/ProtocolsHome.js b/src/components/modules/protocols/ProtocolsHome.js index 9444168..a901efb 100644 --- a/src/components/modules/protocols/ProtocolsHome.js +++ b/src/components/modules/protocols/ProtocolsHome.js @@ -139,7 +139,7 @@ export default (props) => { if (organization) url += `&organization=${organization}`; if (origin) url += `&origin=${origin}`; if (page) url += `page=${page}`; - if (limit) url += `limit=${limit}`; + // if (limit) url += `limit=${limit}`; setLoading(true); authGet(url).then((res) => { From 04b1df4eee364e512427b17d25abd812f9166fb0 Mon Sep 17 00:00:00 2001 From: Radoslav Popov Date: Sun, 4 Jul 2021 15:46:49 +0200 Subject: [PATCH 04/10] add new published and ready status of protocols --- src/components/modules/protocols/ProtocolsHome.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/modules/protocols/ProtocolsHome.js b/src/components/modules/protocols/ProtocolsHome.js index a901efb..f4fd0d4 100644 --- a/src/components/modules/protocols/ProtocolsHome.js +++ b/src/components/modules/protocols/ProtocolsHome.js @@ -191,6 +191,16 @@ export default (props) => { Редактиран
); + case 'published': + return ( + + Публикуван + + ); + case 'ready': + return ( + Готов + ); default: return apiStatus; } @@ -256,7 +266,7 @@ export default (props) => { {loading ? ( - + From fd79c6c7b161234991788ff6b044a9a7bb0b7728 Mon Sep 17 00:00:00 2001 From: Radoslav Popov Date: Sun, 4 Jul 2021 23:22:27 +0200 Subject: [PATCH 05/10] add better role visualization within the user table and make console error free --- src/components/Util.js | 103 ------------- src/components/modules/Modules.js | 98 ++++++------ src/components/modules/admin/Admin.js | 76 ++++++++- src/components/modules/admin/AdminDetails.js | 100 +++++++++--- .../modules/protocols/ProtocolDetails.js | 30 +++- src/components/modules/violations/Comment.js | 103 +++++++------ .../modules/violations/ViolationDetails.js | 78 ++++++---- .../modules/violations/ViolationList.js | 4 +- src/components/{ => utils}/ImageGallery.js | 0 src/components/utils/Tooltip.js | 41 +++++ src/components/utils/Util.js | 144 ++++++++++++++++++ 11 files changed, 521 insertions(+), 256 deletions(-) delete mode 100644 src/components/Util.js rename src/components/{ => utils}/ImageGallery.js (100%) create mode 100644 src/components/utils/Tooltip.js create mode 100644 src/components/utils/Util.js diff --git a/src/components/Util.js b/src/components/Util.js deleted file mode 100644 index eb96b35..0000000 --- a/src/components/Util.js +++ /dev/null @@ -1,103 +0,0 @@ -const format = function(number, n, x, s, c) { - var re = '\\d(?=(\\d{' + (x || 3) + '})+' + (n > 0 ? '\\D' : '$') + ')', - num = parseFloat(number).toFixed(Math.max(0, ~~n)); - - return (c ? num.replace('.', c) : num).replace(new RegExp(re, 'g'), '$&' + (s || ',')); -}; - -const formatLv = function(number) { - return format(number, 2, 3, ' ', ','); -}; - -const formatPop = function(number) { - return format(Math.floor(number), 0, 3, ' ', ','); -} - -const pad = num => { - var s = String(num); - while (s.length < 2) {s = "0" + s;} - return s; -}; - -const month = monthStr => { - switch(monthStr) { - case 0: return 'януари'; break; - case 1: return 'февруари'; break; - case 2: return 'март'; break; - case 3: return 'април'; break; - case 4: return 'май'; break; - case 5: return 'юни'; break; - case 6: return 'юли'; break; - case 7: return 'август'; break; - case 8: return 'септември'; break; - case 9: return 'октомври'; break; - case 10: return 'ноември'; break; - case 11: return 'декември'; break; - } -}; - -const formatDay = d => { - if(d >= 10 && d <= 20) - return d + '-ти'; - - switch(d%10) { - case 1: return d + '-ви'; break; - case 2: return d + '-ри'; break; - case 7: case 8: return d + '-ми'; break; - default: return d + '-ти'; break; - } -}; - -const formatDate = dateTime => { - const date = new Date(dateTime); - - return formatDay(date.getDate()) + ' ' + - month(date.getMonth()) + ' '+ - date.getFullYear(); -}; - -const formatTime = dateTime => { - const date = new Date(dateTime); - return pad(date.getHours()) + ':' + - pad(date.getMinutes()); -}; - -const formatDateTime = dateTime => { - return formatTime(dateTime) + ' ' + formatDateShort(dateTime); - -}; - -const formatSecs = secs => { - return pad(Math.floor(secs / 60)) + ':' + pad(secs % 60); -}; - -const formatDateShort = dateTime => { - const date = new Date(dateTime); - - return pad(date.getDate()) + '.' + - pad(date.getMonth()+1) + '.' + - pad(date.getFullYear()); -} - -const checkPaths = (path1, path2) => { - if(!path1 || !path2) return false; - - if(path1[path1.length - 1] !== '/') - path1 = path1 + '/'; - if(path2[path2.length - 1] !== '/') - path2 = path2 + '/'; - - return path1 === path2; -} - -module.exports = { - formatLv, - formatPop, - formatDate, - formatDateShort, - formatDateTime, - formatTime, - formatSecs, - format, - checkPaths, -}; \ No newline at end of file diff --git a/src/components/modules/Modules.js b/src/components/modules/Modules.js index 95b3ffc..39979bb 100644 --- a/src/components/modules/Modules.js +++ b/src/components/modules/Modules.js @@ -5,7 +5,7 @@ import Navigation from './layout/Navigation'; import { Switch, Route, Redirect } from 'react-router-dom'; import Admin from './admin/Admin'; -import AdminDetails from './admin/AdminDetails' +import AdminDetails from './admin/AdminDetails'; import Posts from './Posts'; import Profile from './Profile'; @@ -21,59 +21,63 @@ import styled from 'styled-components'; import Sections from './Sections'; const NavigationHalf = styled.div` - width: 220px; - height: 100vh; - position: absolute; - top: 0; - left: 0; + width: 220px; + height: 100vh; + position: absolute; + top: 0; + left: 0; `; const ContentHalf = styled.div` - width: calc(100% - 220px); - height: 100vh; - overflow-y: auto; - position: absolute; - top: 0; - right: 0; - background-color: #eee; + width: calc(100% - 220px); + height: 100vh; + overflow-y: auto; + position: absolute; + top: 0; + right: 0; + background-color: #eee; `; export const ContentPanel = styled.div` - background-color: white; - margin: 30px auto; - max-width: 800px; - border-radius: 15px; - //box-shadow: 0px 0px 5px #aaa; - border: 1px solid #eee; - padding: 20px 50px; + background-color: white; + margin: 30px auto; + max-width: 840px; + border-radius: 15px; + //box-shadow: 0px 0px 5px #aaa; + border: 1px solid #eee; + padding: 20px 50px; - hr { - margin: 20px 0; - border: 1px solid #ddd; - border-top: 0; - } + hr { + margin: 20px 0; + border: 1px solid #ddd; + border-top: 0; + } `; -export default props => { - return( - <> - - - - - - - - - - - - - - - - - - - ); +export default (props) => { + return ( + <> + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/src/components/modules/admin/Admin.js b/src/components/modules/admin/Admin.js index 97208a6..436761c 100644 --- a/src/components/modules/admin/Admin.js +++ b/src/components/modules/admin/Admin.js @@ -16,6 +16,8 @@ import AdminOverview from './AdminOverview'; import Loading from '../../layout/Loading'; +import Tooltip from '../../utils/Tooltip'; + const TableViewContainer = styled.div` padding: 40px; @@ -76,6 +78,28 @@ const UserTable = styled.table` } `; +const RoleIcon = styled.span` + background-color: #4aa2ff; + border-radius: 50%; + color: white; + font-weight: bold; + height: 34px; + display: block; + width: 34px; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + background-color: #5da2ec; + } +`; + +const RolesContainer = styled.div` + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; +`; const useQuery = () => { return new URLSearchParams(useLocation().search); }; @@ -142,6 +166,54 @@ export default (props) => { history.push(`/user/${id}`); }; + const roles = (roles) => { + return roles.length === 0 ? ( + Без Роля + ) : ( + roles.map((role, idx) => { + return createRoleItem(role, idx); + }) + ); + }; + + const createRoleItem = (role, idx) => { + let roleName = role[0]; + let color = '#9e9e9e'; + switch (role) { + case 'user': + roleName = 'Потребител'; + color = '#4caf50'; + break; + case 'validator': + roleName = 'Валидатор'; + color = '#6c6cff'; + break; + case 'lawyer': + roleName = 'Юрист'; + color = '#00bcd4'; + break; + case 'streamer': + roleName = 'Оператор'; + color = '#ff9800'; + break; + case 'admin': + roleName = 'Администратор'; + color = '#ff3a39'; + break; + default: + break; + } + return roleCircle(roleName, idx, color); + }; + + const roleCircle = (roleName, idx, color) => { + return ( + + {roleName[0]} + + ); + }; + return ( <> @@ -182,7 +254,9 @@ export default (props) => { {user.pin} {user.hasAgreedToKeepData ? 'Да' : 'Не'} {/* {user.registeredAt} */} - {user.roles.join(', ')} + + {roles(user.roles)} + )) )} diff --git a/src/components/modules/admin/AdminDetails.js b/src/components/modules/admin/AdminDetails.js index 89bfc5a..ace07a6 100644 --- a/src/components/modules/admin/AdminDetails.js +++ b/src/components/modules/admin/AdminDetails.js @@ -2,27 +2,89 @@ import React, { useState, useEffect, useContext } from 'react'; import { ContentPanel } from '../Modules'; import { useHistory, useParams } from 'react-router-dom'; import { AuthContext } from '../../App'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { TableStyle } from '../Profile'; +import { BackButton } from '../violations/ViolationDetails'; +import Loading from '../../layout/Loading'; -export default props => { - const { authGet } = useContext(AuthContext); - const { userId } = useParams(); - const history = useHistory(); - const [data, setData] = useState(null); +export default (props) => { + const { authGet } = useContext(AuthContext); + const { userId } = useParams(); + const history = useHistory(); + const [userData, setUserData] = useState(null); + const [roles, setRoles] = useState(null); - useEffect(() => { - authGet(`/users/${userId}`).then(res => { - setData(res.data); - }); - }, []); + useEffect(() => { + authGet(`/users/${userId}`).then((res) => { + setUserData(res.data); + }); - const goBack = () => { - history.goBack() - } + authGet(`/users/roles`).then((res) => { + setRoles(res.roles); + }); + }, []); - return( - - - - ); -} \ No newline at end of file + const goBack = () => { + history.goBack(); + }; + + return ( + +

+ + + + + Лична информация + +

+
+ + {!userData ? ( + + ) : ( + <> +

Профил

+ + + + Имена + + {userData.firstName} {userData.lastName} + + + + Организация + + {userData.organization ? userData.organization.name : null} + + + + Ел. поща + {userData.email} + + + Телефон + {userData.phone} + + + Съгласие за съхранение на данни + {userData.hasAgreedToKeepData ? 'Да' : 'Не'} + + + + + )} +
+

Роли

+
+ ); +}; diff --git a/src/components/modules/protocols/ProtocolDetails.js b/src/components/modules/protocols/ProtocolDetails.js index 4ba94e1..d22e3c2 100644 --- a/src/components/modules/protocols/ProtocolDetails.js +++ b/src/components/modules/protocols/ProtocolDetails.js @@ -5,10 +5,14 @@ import { ContentPanel } from '../Modules'; import { useHistory, useParams } from 'react-router-dom'; import { AuthContext } from '../../App'; -import ImageGallery from '../../ImageGallery'; +import ImageGallery from '../../utils/ImageGallery'; import Loading from '../../layout/Loading'; import { TableStyle } from '../Profile'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { BackButton } from '../violations/ViolationDetails'; + export default (props) => { const { authGet } = useContext(AuthContext); const { protocol } = useParams(); @@ -27,14 +31,26 @@ export default (props) => { return ( - -

Протокол {protocol}

+

+ + + + + Протокол {protocol} + +


{!data ? ( ) : (
-

Секция

+

Секция

@@ -48,7 +64,7 @@ export default (props) => {
-

Протокол

+

Протокол

@@ -78,7 +94,7 @@ export default (props) => {
-

Резултати

+

Резултати

{data.results.results.map((result, idx) => ( @@ -93,7 +109,7 @@ export default (props) => {
-

Снимки

+

Снимки

({ original: picture.url, diff --git a/src/components/modules/violations/Comment.js b/src/components/modules/violations/Comment.js index 37d3b30..6c3bf84 100644 --- a/src/components/modules/violations/Comment.js +++ b/src/components/modules/violations/Comment.js @@ -2,59 +2,66 @@ import React from 'react'; import styled from 'styled-components'; -import { formatTime, formatDateShort } from '../../Util'; +import { formatTime, formatDateShort } from '../../utils/Util'; export const CommentStyle = styled.div` - width: 100%; + width: 100%; - h1 { - font-weight: normal; - font-size: 22px; - margin: 5px 0; - color: #555; - } + h1 { + font-weight: normal; + font-size: 22px; + margin: 5px 0; + color: #555; + } - h2 { - font-weight: normal; - font-size: 16px; - margin: 5px 0; - color: #bbb; - } + h2 { + font-weight: normal; + font-size: 16px; + margin: 5px 0; + color: #bbb; + } - p { - color: #333; - } + p { + color: #333; + } - .comment-type { - color: #888; - border: 3px solid #888; - border-radius: 10px; - font-size: 12px; - padding: 5px; - font-weight: bold; - vertical-align: top; - display: inline-block; - margin-left: 10px; - margin-top: -2px; - } + .comment-type { + color: #888; + border: 3px solid #888; + border-radius: 10px; + font-size: 12px; + padding: 5px; + font-weight: bold; + vertical-align: top; + display: inline-block; + margin-left: 10px; + margin-top: -2px; + } `; -export default props => { - const formatCommentType = commentType => { - switch(commentType) { - case 'internal': return 'Вътрешен'; break; - default: return 'Вътрешен'; break; - } - }; - - return ( - -

- {props.comment.author.firstName} {props.comment.author.lastName} - {formatCommentType(props.comment.type)} -

-

{formatTime(props.comment.createdAt)} – {formatDateShort(props.comment.createdAt)}

-

{props.comment.text}

-
- ); -}; \ No newline at end of file +export default (props) => { + const formatCommentType = (commentType) => { + switch (commentType) { + case 'internal': + return 'Вътрешен'; + default: + return 'Вътрешен'; + } + }; + + return ( + +

+ {props.comment.author.firstName} {props.comment.author.lastName} + + {formatCommentType(props.comment.type)} + +

+

+ {formatTime(props.comment.createdAt)} –{' '} + {formatDateShort(props.comment.createdAt)} +

+

{props.comment.text}

+
+ ); +}; diff --git a/src/components/modules/violations/ViolationDetails.js b/src/components/modules/violations/ViolationDetails.js index 62bf667..8609aa8 100644 --- a/src/components/modules/violations/ViolationDetails.js +++ b/src/components/modules/violations/ViolationDetails.js @@ -15,7 +15,7 @@ import { import { ContentPanel } from '../Modules'; import { AuthContext } from '../../App'; -import ImageGallery from '../../ImageGallery'; +import ImageGallery from '../../utils/ImageGallery'; import Loading from '../../layout/Loading'; import { TableStyle } from '../Profile'; @@ -23,7 +23,7 @@ import { TableStyle } from '../Profile'; import styled from 'styled-components'; import CommentSection from './CommentSection'; -import { formatDateShort, formatTime } from '../../Util'; +import { formatDateShort, formatTime } from '../../utils/Util'; const UpdatesTable = styled(TableStyle)` td, @@ -35,7 +35,7 @@ const UpdatesTable = styled(TableStyle)` } `; -const BackButton = styled.button` +export const BackButton = styled.button` cursor: pointer; border: none; border-radius: 6px; @@ -158,8 +158,6 @@ export default (props) => { reject: false, }); - console.log({ data }); - useEffect(() => { authGet(`/violations/${violation}`).then((res) => { setData(res.data); @@ -270,51 +268,73 @@ export default (props) => { ) : (

- {data.assignees.length === 0 - ? 'Свободен за обработка' - : [ - `Обработва се от ${data.assignees[0].firstName} ${data.assignees[0].lastName}`, - !iAmAssignee ? null : ( - (Вие) - ), - ]} + {data.assignees.length === 0 ? ( + 'Свободен за обработка' + ) : ( + <> + `Обработва се от ${data.assignees[0].firstName} $ + {data.assignees[0].lastName}`, !iAmAssignee ? null : ( + (Вие) + ), + + )}

- {buttonLoading.assign - ? 'Момент...' - : iAmAssignee - ? [, ' Освободи'] - : [, ' Обработвай']} + {buttonLoading.assign ? ( + 'Момент...' + ) : iAmAssignee ? ( + <> + , ' Освободи'{' '} + + ) : ( + <> + , ' Обработвай'{' '} + + )} - {buttonLoading.process - ? 'Момент...' - : [, ' Приключи']} + {buttonLoading.process ? ( + 'Момент...' + ) : ( + <> + , ' Приключи'{' '} + + )} - {buttonLoading.publish - ? 'Момент...' - : data.isPublished - ? [, ' Скрий'] - : [, ' Публикувай']} + {buttonLoading.publish ? ( + 'Момент...' + ) : data.isPublished ? ( + <> + , ' Скрий' + + ) : ( + <> + , ' Публикувай' + + )} - {buttonLoading.reject - ? 'Момент...' - : [, ' Отхвърли']} + {buttonLoading.reject ? ( + 'Момент...' + ) : ( + <> + , ' Отхвърли'{' '} + + )}

Описание

diff --git a/src/components/modules/violations/ViolationList.js b/src/components/modules/violations/ViolationList.js index 5a7fd28..d94fb00 100644 --- a/src/components/modules/violations/ViolationList.js +++ b/src/components/modules/violations/ViolationList.js @@ -268,7 +268,7 @@ export default (props) => { {loading ? ( - + @@ -310,7 +310,7 @@ export default (props) => { )} - ,{renderLinks()} + {renderLinks()} )} diff --git a/src/components/ImageGallery.js b/src/components/utils/ImageGallery.js similarity index 100% rename from src/components/ImageGallery.js rename to src/components/utils/ImageGallery.js diff --git a/src/components/utils/Tooltip.js b/src/components/utils/Tooltip.js new file mode 100644 index 0000000..59e3cdf --- /dev/null +++ b/src/components/utils/Tooltip.js @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; + +import styled from 'styled-components'; + +const TooltipContainer = styled.div` + position: relative; + display: flex; + align-content: center; + justify-content: center; + + .tooltip-box { + position: absolute; + background: rgba(0, 0, 0, 0.7); + color: #fff; + padding: 5px; + border-radius: 5px; + top: calc(100% + 5px); + display: none; + z-index: 1; + } + + .tooltip-box.visible { + display: block; + } +`; + +export default ({ children, text }) => { + const [show, setShow] = useState(false); + + return ( + +
{text}
+
setShow(true)} + onMouseLeave={() => setShow(false)} + > + {children} +
+
+ ); +}; diff --git a/src/components/utils/Util.js b/src/components/utils/Util.js new file mode 100644 index 0000000..95902e4 --- /dev/null +++ b/src/components/utils/Util.js @@ -0,0 +1,144 @@ +const format = function (number, n, x, s, c) { + var re = '\\d(?=(\\d{' + (x || 3) + '})+' + (n > 0 ? '\\D' : '$') + ')', + num = parseFloat(number).toFixed(Math.max(0, ~~n)); + + return (c ? num.replace('.', c) : num).replace( + new RegExp(re, 'g'), + '$&' + (s || ',') + ); +}; + +const formatLv = function (number) { + return format(number, 2, 3, ' ', ','); +}; + +const formatPop = function (number) { + return format(Math.floor(number), 0, 3, ' ', ','); +}; + +const pad = (num) => { + var s = String(num); + while (s.length < 2) { + s = '0' + s; + } + return s; +}; + +const month = (monthStr) => { + switch (monthStr) { + case 0: + return 'януари'; + break; + case 1: + return 'февруари'; + break; + case 2: + return 'март'; + break; + case 3: + return 'април'; + break; + case 4: + return 'май'; + break; + case 5: + return 'юни'; + break; + case 6: + return 'юли'; + break; + case 7: + return 'август'; + break; + case 8: + return 'септември'; + break; + case 9: + return 'октомври'; + break; + case 10: + return 'ноември'; + break; + case 11: + return 'декември'; + break; + } +}; + +const formatDay = (d) => { + if (d >= 10 && d <= 20) return d + '-ти'; + + switch (d % 10) { + case 1: + return d + '-ви'; + break; + case 2: + return d + '-ри'; + break; + case 7: + case 8: + return d + '-ми'; + break; + default: + return d + '-ти'; + break; + } +}; + +const formatDate = (dateTime) => { + const date = new Date(dateTime); + + return ( + formatDay(date.getDate()) + + ' ' + + month(date.getMonth()) + + ' ' + + date.getFullYear() + ); +}; + +const formatTime = (dateTime) => { + const date = new Date(dateTime); + return pad(date.getHours()) + ':' + pad(date.getMinutes()); +}; + +const formatDateTime = (dateTime) => { + return formatTime(dateTime) + ' ' + formatDateShort(dateTime); +}; + +const formatSecs = (secs) => { + return pad(Math.floor(secs / 60)) + ':' + pad(secs % 60); +}; + +const formatDateShort = (dateTime) => { + const date = new Date(dateTime); + + return ( + pad(date.getDate()) + + '.' + + pad(date.getMonth() + 1) + + '.' + + pad(date.getFullYear()) + ); +}; + +const checkPaths = (path1, path2) => { + if (!path1 || !path2) return false; + + if (path1[path1.length - 1] !== '/') path1 = path1 + '/'; + if (path2[path2.length - 1] !== '/') path2 = path2 + '/'; + + return path1 === path2; +}; + +module.exports = { + formatLv, + formatPop, + formatDate, + formatDateShort, + formatDateTime, + formatTime, + formatSecs, + format, + checkPaths, +}; From f61bc2a14e7426fb467650c68e40e285b1d98ddb Mon Sep 17 00:00:00 2001 From: Radoslav Popov Date: Tue, 6 Jul 2021 08:15:12 +0200 Subject: [PATCH 06/10] fixing some small errors after rebase --- .../modules/filter_components/Countries.js | 37 +++--- .../filter_components/Municipalities.js | 14 +-- .../modules/filter_components/Regions.js | 22 +++- .../modules/filter_components/Towns.js | 15 ++- .../modules/protocols/ProtocolFilter.js | 106 +++++++++--------- .../modules/protocols/ProtocolsHome.js | 3 +- .../modules/violations/ViolationFilter.js | 96 ++++++++-------- .../modules/violations/ViolationList.js | 11 +- 8 files changed, 159 insertions(+), 145 deletions(-) diff --git a/src/components/modules/filter_components/Countries.js b/src/components/modules/filter_components/Countries.js index 37473a5..74e1c07 100644 --- a/src/components/modules/filter_components/Countries.js +++ b/src/components/modules/filter_components/Countries.js @@ -1,26 +1,25 @@ -import React, { useContext } from "react"; -import { AuthContext } from "../../App"; +import React, { useContext } from 'react'; +import { AuthContext } from '../../App'; export default (props) => { - const countries = useContext(AuthContext).countries; //things to do when country is selected const optionHandler = (event) => { - var selectCountry = document.getElementById("selectCountry"); + var selectCountry = document.getElementById('selectCountry'); var selectedValue = selectCountry.options[selectCountry.selectedIndex].value; //sets the country code for the get request in the filter component - props.setCountry(selectedValue.split(",")[2]); - props.setSelectedCountry(event.target.value) + props.setCountry(selectedValue.split(',')[2]); + props.setSelectedCountry(event.target.value); //if isAbroad === 'true' - if (selectedValue.split(",")[1] === "true") { - props.setIsAbroad(true); //disables MIR dropdown + if (selectedValue.split(',')[1] === 'true') { + props.setIsAbroad(true); //disables MIR dropdown props.setDisabled(false); //enables the town field - props.setMunicipalities([{name: 'Всички'}]); //sets the visible municipality in the dropdown - props.setRegions([{name: 'Всички'}]) //sets the visible region in the dropdown + props.setMunicipalities([{ name: 'Всички' }]); //sets the visible municipality in the dropdown + props.setRegions([{ name: 'Всички' }]); //sets the visible region in the dropdown props.setTown('00'); props.setMunicipality('00'); props.setCityRegion('00'); @@ -28,7 +27,7 @@ export default (props) => { } else { props.setElectionRegion('00'); props.setIsAbroad(false); //enables the MIR dropdown - props.setDisabled(true); //disables the town dropdown because it should be enabled when MIR is chosen + props.setDisabled(true); //disables the town dropdown because it should be enabled when MIR is chosen props.setMunicipality('00'); } }; @@ -36,17 +35,25 @@ export default (props) => { const filters = countries .sort((a, b) => (a.code > b.code ? 1 : -1)) .map(({ name, code, isAbroad }) => { - {/* to test if the removal of name from the array has any effect??? and If I can make that with key:value pairs */} - {/* e.g. value={[isAbroad: isAbroad, code: code]} */} + { + /* to test if the removal of name from the array has any effect??? and If I can make that with key:value pairs */ + } + { + /* e.g. value={[isAbroad: isAbroad, code: code]} */ + } return ( - ); }); return ( - {filters} ); diff --git a/src/components/modules/filter_components/Municipalities.js b/src/components/modules/filter_components/Municipalities.js index 6de3015..dea87fb 100644 --- a/src/components/modules/filter_components/Municipalities.js +++ b/src/components/modules/filter_components/Municipalities.js @@ -1,11 +1,11 @@ -import React from "react"; +import React from 'react'; export default (props) => { let municipalities = []; if (!props.isAbroad) { municipalities = [ - { code: "00", name: "Всички", isAbroad: false }, + { code: '00', name: 'Всички', isAbroad: false }, ...props.municipalities, ]; } else { @@ -13,13 +13,11 @@ export default (props) => { } const municipalityHandler = (event) => { + props.setSelectedMunicipality(event.target.value); - props.setSelectedMunicipality(event.target.value) - - var selectMunicipality = document.getElementById("selectMunicipality"); + var selectMunicipality = document.getElementById('selectMunicipality'); var selectedValue = selectMunicipality.options[selectMunicipality.selectedIndex].value; - municipalities.map(({ code, name }) => { if (selectedValue === name) { props.setMunicipality(code); @@ -28,9 +26,9 @@ export default (props) => { }; const filters = municipalities .sort((a, b) => (a.code > b.code ? 1 : -1)) - .map(({ code, name }) => { + .map(({ code, name }, idx) => { return ( - ); diff --git a/src/components/modules/filter_components/Regions.js b/src/components/modules/filter_components/Regions.js index e016523..ba55fd6 100644 --- a/src/components/modules/filter_components/Regions.js +++ b/src/components/modules/filter_components/Regions.js @@ -1,11 +1,13 @@ -import React from "react"; +import React from 'react'; export default (props) => { - let regions = []; if (!props.isAbroad) { - regions = [{ code: "00", name: "Всички", isAbroad: false }, ...props.regions]; + regions = [ + { code: '00', name: 'Всички', isAbroad: false }, + ...props.regions, + ]; } else { regions = props.regions; } @@ -14,13 +16,21 @@ export default (props) => { props.setCityRegion(event.target.value); }; - const filters = regions.map(({ name, code }) => { + const filters = regions.map(({ name, code }, idx) => { return ( - ); }); - return ; + return ( + + ); }; diff --git a/src/components/modules/filter_components/Towns.js b/src/components/modules/filter_components/Towns.js index 98dd0dd..3e78b30 100644 --- a/src/components/modules/filter_components/Towns.js +++ b/src/components/modules/filter_components/Towns.js @@ -1,21 +1,21 @@ -import React from "react"; +import React from 'react'; export default (props) => { let towns = []; if (!props.isAbroad) { - towns = [{ id: "00", name: "Всички", isAbroad: false }, ...props.towns]; + towns = [{ id: '00', name: 'Всички', isAbroad: false }, ...props.towns]; } else { - towns = [{ id: "00", name: "Всички", isAbroad: true }, ...props.towns]; + towns = [{ id: '00', name: 'Всички', isAbroad: true }, ...props.towns]; } const townHandler = (event) => { props.setSelectedTown(event.target.value); - var selectTown = document.getElementById("selectTown"); + var selectTown = document.getElementById('selectTown'); var selectedValue = selectTown.options[selectTown.selectedIndex].value; if (!props.isAbroad) { - if (selectedValue !== "Всички") { + if (selectedValue !== 'Всички') { towns.map(({ name, cityRegions, id }) => { if (selectedValue === name) { props.setRegions(cityRegions); @@ -23,10 +23,10 @@ export default (props) => { } }); } else { - props.setTown(""); + props.setTown(''); } } else { - props.setRegions([{ name: "Всички", code: "00" }]); + props.setRegions([{ name: 'Всички', code: '00' }]); towns .sort((a, b) => (a.id > b.id ? 1 : -1)) .map(({ name, id }) => { @@ -49,7 +49,6 @@ export default (props) => { return ( handleOnChange(role)} + /> + +
+
+ + ); + })} + + + + Запис + {rolesUpdateSuccessful === + undefined ? null : rolesUpdateSuccessful ? ( +

+ Ролите бяха актуализирани успешно +

+ ) : ( +

+ Грешка при актуализацията на ролите (Опитайте отново) +

+ )} + + )}
); }; From b0d1f6fe377952e6611d246b39229d20feed31c5 Mon Sep 17 00:00:00 2001 From: Radoslav Popov Date: Thu, 8 Jul 2021 02:32:22 +0200 Subject: [PATCH 09/10] add admin filters --- src/components/App.js | 309 +++++++++++------- src/components/modules/admin/Admin.js | 36 +- src/components/modules/admin/AdminDetails.js | 11 +- src/components/modules/admin/AdminFilter.js | 116 +++++++ .../modules/filter_components/MainFilter.js | 41 ++- .../modules/filter_components/Roles.js | 28 ++ .../{SectionNumber.js => TextInput.js} | 12 +- .../modules/protocols/ProtocolFilter.js | 4 +- .../modules/violations/ViolationFilter.js | 4 +- src/components/utils/Util.js | 5 + 10 files changed, 397 insertions(+), 169 deletions(-) create mode 100644 src/components/modules/admin/AdminFilter.js create mode 100644 src/components/modules/filter_components/Roles.js rename src/components/modules/filter_components/{SectionNumber.js => TextInput.js} (55%) diff --git a/src/components/App.js b/src/components/App.js index 2d0d023..4f9eecf 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; -import firebase from "firebase/app"; -import "firebase/auth"; +import firebase from 'firebase/app'; +import 'firebase/auth'; import axios from 'axios'; import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom'; @@ -13,132 +13,197 @@ import Modules from './modules/Modules'; import styled from 'styled-components'; import ProcessProtocols from './process_protocols/ProcessProtocols'; -import { apiKey, authDomain, databaseURL, projectId } from "../../config/keys"; +import { apiKey, authDomain, databaseURL, projectId } from '../../config/keys'; const AppStyle = styled.div` - font-family: Montserrat, sans-serif; + font-family: Montserrat, sans-serif; `; export const AuthContext = React.createContext(); -export default props => { - - const [state, setState] = useState({user: null, loading: true, token: '', parties: [], countries: [], organizations: []}); - - useEffect(() => { - firebase.initializeApp({ apiKey, authDomain, databaseURL, projectId }); - - firebase.app().auth().onAuthStateChanged(async user => { - if (user) { - const idToken = await user.getIdToken(); - - setState({...state, loading: true}); - const res = await axios.get(`${apiHost()}/me`, { - headers: { 'Authorization': `Bearer ${idToken}` } - }); - - const res2 = await axios.get(`${apiHost()}/parties`, { - headers: { 'Authorization': `Bearer ${idToken}` } - }); - - const res3 = await axios.get(`${apiHost()}/countries`, { - headers: { 'Authorization': `Bearer ${idToken}` } - }); - - const res4 = await axios.get(`${apiHost()}/organizations`, { - headers: { 'Authorization': `Bearer ${idToken}` } - }) - - - setState({user: res.data, loading: false, token: idToken, parties: res2.data, countries: res3.data, organizations: res4.data}); - } else { - setState({user: null, loading: false, token: '', parties: [], countries: [], organizations: []}); - } - }); - }, []); - - const logIn = ({ email, password }) => { - return firebase.auth().signInWithEmailAndPassword(email, password); - }; - - const logOut = () => { - firebase.app().auth().signOut(); - }; - - const apiHost = () => { - if(!process.env.API_HOST) { - return 'https://d1tapi.dabulgaria.bg'; +export default (props) => { + const [state, setState] = useState({ + user: null, + loading: true, + token: '', + parties: [], + countries: [], + organizations: [], + roles: [], + }); + + useEffect(() => { + firebase.initializeApp({ apiKey, authDomain, databaseURL, projectId }); + + firebase + .app() + .auth() + .onAuthStateChanged(async (user) => { + if (user) { + const idToken = await user.getIdToken(); + + setState({ ...state, loading: true }); + const res = await axios.get(`${apiHost()}/me`, { + headers: { Authorization: `Bearer ${idToken}` }, + }); + + const res2 = await axios.get(`${apiHost()}/parties`, { + headers: { Authorization: `Bearer ${idToken}` }, + }); + + const res3 = await axios.get(`${apiHost()}/countries`, { + headers: { Authorization: `Bearer ${idToken}` }, + }); + + const res4 = await axios.get(`${apiHost()}/organizations`, { + headers: { Authorization: `Bearer ${idToken}` }, + }); + + const res5 = await axios.get(`${apiHost()}/users/roles`, { + headers: { Authorization: `Bearer ${idToken}` }, + }); + + setState({ + user: res.data, + loading: false, + token: idToken, + parties: res2.data, + countries: res3.data, + organizations: res4.data, + roles: res5.data, + }); } else { - return process.env.API_HOST; - } - }; - - const authGet = async (path) => { - const res = await axios.get(`${apiHost()}${path}`, { headers: { 'Authorization': `Bearer ${state.token}` }}); - return res; - }; - - const authPost = async (path, body) => { - let res; - try { - res = await axios.post(`${apiHost()}${path}`, body? body : {}, { headers: { 'Authorization': `Bearer ${state.token}` }}); - } catch(err) { - alert(`Error ${err.response.status}: ${err.response.statusText}\n${err.response.data.message.map((m, i) => `\n${i+1}. ${m}`)}`); - throw err; - } - return res; - }; - - const authDelete = async (path) => { - const res = await axios.delete(`${apiHost()}${path}`, { headers: { 'Authorization': `Bearer ${state.token}` }}); - return res; - }; - - const authPut = async (path, body) => { - let res; - try { - res = await axios.put(`${apiHost()}${path}`, body? body : {}, { headers: { 'Authorization': `Bearer ${state.token}` }}); - } catch(err) { - alert(`Error ${err.response.status}: ${err.response.statusText}\n${err.response.data.message.map((m, i) => `\n${i+1}. ${m}`)}`); - throw err; - } - return res; - }; - - const authPatch = async (path, body) => { - let res; - try { - res = await axios.patch(`${apiHost()}${path}`, body? body : {}, { headers: { 'Authorization': `Bearer ${state.token}` }}); - } catch(err) { - alert(`Error ${err.response.status}: ${err.response.statusText}\n${err.response.data.message.map((m, i) => `\n${i+1}. ${m}`)}`); - throw err; + setState({ + user: null, + loading: false, + token: '', + parties: [], + countries: [], + organizations: [], + roles: [], + }); } - return res; - }; - - return( - - - - { - state.loading - ? - : - - {state.user? : } - - - {!state.user? : } - - - {!state.user? : } - - - } - - - - ); + }); + }, []); + + const logIn = ({ email, password }) => { + return firebase.auth().signInWithEmailAndPassword(email, password); + }; + + const logOut = () => { + firebase.app().auth().signOut(); + }; + + const apiHost = () => { + if (!process.env.API_HOST) { + return 'https://d1tapi.dabulgaria.bg'; + } else { + return process.env.API_HOST; + } + }; + + const authGet = async (path) => { + const res = await axios.get(`${apiHost()}${path}`, { + headers: { Authorization: `Bearer ${state.token}` }, + }); + return res; + }; + + const authPost = async (path, body) => { + let res; + try { + res = await axios.post(`${apiHost()}${path}`, body ? body : {}, { + headers: { Authorization: `Bearer ${state.token}` }, + }); + } catch (err) { + alert( + `Error ${err.response.status}: ${ + err.response.statusText + }\n${err.response.data.message.map((m, i) => `\n${i + 1}. ${m}`)}` + ); + throw err; + } + return res; + }; + + const authDelete = async (path) => { + const res = await axios.delete(`${apiHost()}${path}`, { + headers: { Authorization: `Bearer ${state.token}` }, + }); + return res; + }; + + const authPut = async (path, body) => { + let res; + try { + res = await axios.put(`${apiHost()}${path}`, body ? body : {}, { + headers: { Authorization: `Bearer ${state.token}` }, + }); + } catch (err) { + alert( + `Error ${err.response.status}: ${ + err.response.statusText + }\n${err.response.data.message.map((m, i) => `\n${i + 1}. ${m}`)}` + ); + throw err; + } + return res; + }; + + const authPatch = async (path, body) => { + let res; + try { + res = await axios.patch(`${apiHost()}${path}`, body ? body : {}, { + headers: { Authorization: `Bearer ${state.token}` }, + }); + } catch (err) { + alert( + `Error ${err.response.status}: ${ + err.response.statusText + }\n${err.response.data.message.map((m, i) => `\n${i + 1}. ${m}`)}` + ); + throw err; + } + return res; + }; + + return ( + + + + {state.loading ? ( + + ) : ( + + + {state.user ? : } + + + {!state.user ? : } + + + {!state.user ? : } + + + )} + + + + ); }; diff --git a/src/components/modules/admin/Admin.js b/src/components/modules/admin/Admin.js index 3073410..1934b2e 100644 --- a/src/components/modules/admin/Admin.js +++ b/src/components/modules/admin/Admin.js @@ -12,12 +12,14 @@ import { import styled from 'styled-components'; -import AdminOverview from './AdminOverview'; +import AdminFilter from './AdminFilter'; import Loading from '../../layout/Loading'; import Tooltip from '../../utils/Tooltip'; +import { mapRoleLocalization } from '../../utils/Util'; + const TableViewContainer = styled.div` padding: 40px; @@ -110,15 +112,28 @@ export default (props) => { const [loading, setLoading] = useState(false); const query = useQuery(); const history = useHistory(); + const rolesState = useContext(AuthContext).roles; useEffect(() => { let url = '/users'; const page = query.get('page'); + const firstName = query.get('firstName'); + const lastName = query.get('lastName'); + const email = query.get('email'); + const role = query.get('role'); + const organization = query.get('organization'); // const limit = query.get('limit'); - if (page) url += '?'; + if (page || firstName || lastName || email || role || organization) + url += '?'; if (page) url += `page=${page}`; + if (firstName) url += `firstName=${firstName}`; + if (lastName) url += `lastName=${lastName}`; + if (email) url += `email=${email}`; + if (role) url += `role=${role}`; + if (organization) url += `organization=${organization}`; + // if (limit) url += `limit=${limit}`; setLoading(true); @@ -126,7 +141,14 @@ export default (props) => { setLoading(false); setData(res.data); }); - }, [query.get('page')]); + }, [ + query.get('firstName'), + query.get('lastName'), + query.get('email'), + query.get('page'), + query.get('role'), + query.get('organization'), + ]); const renderLinks = () => { const firstAvail = data.meta.currentPage !== 1; @@ -177,27 +199,22 @@ export default (props) => { }; const createRoleItem = (role, idx) => { - let roleName = role[0]; + let roleName = mapRoleLocalization(rolesState, role) ?? role[0]; let color = '#9e9e9e'; switch (role) { case 'user': - roleName = 'Потребител'; color = '#4caf50'; break; case 'validator': - roleName = 'Валидатор'; color = '#6c6cff'; break; case 'lawyer': - roleName = 'Юрист'; color = '#00bcd4'; break; case 'streamer': - roleName = 'Оператор'; color = '#ff9800'; break; case 'admin': - roleName = 'Администратор'; color = '#ff3a39'; break; default: @@ -218,6 +235,7 @@ export default (props) => { <>

Административна секция

+
{!data ? ( diff --git a/src/components/modules/admin/AdminDetails.js b/src/components/modules/admin/AdminDetails.js index e106c59..92b4a68 100644 --- a/src/components/modules/admin/AdminDetails.js +++ b/src/components/modules/admin/AdminDetails.js @@ -61,20 +61,19 @@ export default (props) => { const [userData, setUserData] = useState(null); const [roles, setRoles] = useState(null); const [rolesUpdateSuccessful, setRolesUpdateSuccessful] = useState(undefined); - + const rolesState = useContext(AuthContext).roles; useEffect(async () => { const resUser = await authGet(`/users/${userId}`); setUserData(resUser.data); - const resRoles = await authGet(`/users/roles`); - let rolesData = resRoles.data; - if (resUser.data && rolesData) { - rolesData = rolesData.map((role) => ({ + let allRoles = rolesState; + if (resUser.data && allRoles) { + allRoles = allRoles.map((role) => ({ ...role, isChecked: resUser.data.roles.includes(role.role), })); } - setRoles(rolesData); + setRoles(allRoles); }, []); const goBack = () => { diff --git a/src/components/modules/admin/AdminFilter.js b/src/components/modules/admin/AdminFilter.js new file mode 100644 index 0000000..c2f8d7f --- /dev/null +++ b/src/components/modules/admin/AdminFilter.js @@ -0,0 +1,116 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { Link } from 'react-router-dom'; + +import { AuthContext } from '../../App'; + +import SendBy from '../filter_components/SendBy'; +import Roles from '../filter_components/Roles'; +import TextInput from '../filter_components/TextInput'; + +import styled from 'styled-components'; + +const FilterTable = styled.div` + display: grid; + grid-template-columns: 1fr 1fr 1fr; + row-gap: 24px; +`; + +const ButtonStyle = styled.button` + background-color: #4892e1; + color: white; + border: none; + padding: 7px 13px; + font-size: 20px; + cursor: pointer; + font-weight: bold; + border-radius: 5px; + border-bottom: 3px solid #2a68aa; + margin-top: 0px; + width: 10rem; + + &:hover { + background-color: #5da2ec; + } + + &:active { + background-color: #1d5a9b; + border-bottom: none; + margin-top: 3px; + } +`; + +export default (props) => { + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [email, setEmail] = useState(''); + const [organization, setOrganization] = useState(''); + const [role, setRole] = useState(''); + + let url = ''; + + let params = { + firstName: firstName, + lastName: lastName, + email: email, + organization: organization, + role: role, + }; + + for (const [key, value] of Object.entries(params)) { + if (value !== '00' && value !== '' && value) { + url += `&${key}=${value}`; + } else { + url.replace(`&${key}=${value}`, ''); + } + } + + useEffect(() => {}, [firstName, lastName, email, organization, role]); + + const clearAll = () => { + setOrganization(''); + setRole(''); + setFirstName(''); + setLastName(''); + setEmail(''); + }; + + return ( + <> + +
+ Име:

+ +
+
+ Фамилия:

+ +
+
+ Емейл:

+ +
+ +
+ Организация:

+ +
+
+ Роля:

+ +
+
+ + Изчисти + +
+ + + Търси + +
+ + ); +}; diff --git a/src/components/modules/filter_components/MainFilter.js b/src/components/modules/filter_components/MainFilter.js index 0533211..1835fb1 100644 --- a/src/components/modules/filter_components/MainFilter.js +++ b/src/components/modules/filter_components/MainFilter.js @@ -1,19 +1,19 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect } from 'react'; -import firebase from "firebase/app"; -import "firebase/auth"; +import firebase from 'firebase/app'; +import 'firebase/auth'; -import axios from "axios"; +import axios from 'axios'; -import SectionNumber from "../filters/SectionNumber"; +import TextInput from '../filters/TextInput'; -import Countries from "../filters/Countries"; -import MIRs from "../filters/MIRs"; -import Municipalities from "../filters/Municipalities"; -import Towns from "../filters/Towns"; -import Regions from "../filters/Regions"; +import Countries from '../filters/Countries'; +import MIRs from '../filters/MIRs'; +import Municipalities from '../filters/Municipalities'; +import Towns from '../filters/Towns'; +import Regions from '../filters/Regions'; -import styled from "styled-components"; +import styled from 'styled-components'; const FilterlTable = styled.table` width: 100%; @@ -52,22 +52,21 @@ const ButtonStyle = styled.button` `; export default (props) => { - const [disabled, setDisabled] = useState(true); //sets callback function for disabling field - const [country, setCountry] = useState("00"); + const [country, setCountry] = useState('00'); - const [mirs, setMirs] = useState([]); //sets all MIRs in Bulgaria - const [chosenMir, setChosenMir] = useState("00"); //gets the chosen MIR + const [mirs, setMirs] = useState([]); //sets all MIRs in Bulgaria + const [chosenMir, setChosenMir] = useState('00'); //gets the chosen MIR const [municipalities, setMunicipalities] = useState([]); //sets the municipalities in one MIR - const [chosenMunicipality, setChosenMunicipality] = useState("00"); //gets the chosen municipality + const [chosenMunicipality, setChosenMunicipality] = useState('00'); //gets the chosen municipality const [towns, setTowns] = useState([]); //sets all towns in one municipality const [regions, setRegions] = useState([]); //sets the election regions in one town - const [status, setStatus] = useState(""); + const [status, setStatus] = useState(''); const [isAbroad, setIsAbroad] = useState(false); //sets if country is Bulgaria or not const submitHandler = () => { @@ -76,7 +75,7 @@ export default (props) => { const apiHost = () => { if (!process.env.API_HOST) { - return "https://d1tapi.dabulgaria.bg"; + return 'https://d1tapi.dabulgaria.bg'; } else { return process.env.API_HOST; } @@ -95,8 +94,8 @@ export default (props) => { headers: { Authorization: `Bearer ${idToken}` }, }); - //if country is NOT Bulgaria: gets all the cities in the foreign country - if (country !== "00") { + //if country is NOT Bulgaria: gets all the cities in the foreign country + if (country !== '00') { const res2 = await axios.get( `${apiHost()}/towns?country=${country}`, { @@ -127,7 +126,7 @@ export default (props) => { - N на секция:

+ N на секция:

Произход:

diff --git a/src/components/modules/filter_components/Roles.js b/src/components/modules/filter_components/Roles.js new file mode 100644 index 0000000..0afb00f --- /dev/null +++ b/src/components/modules/filter_components/Roles.js @@ -0,0 +1,28 @@ +import React, { useContext } from 'react'; +import { AuthContext } from '../../App'; + +export default (props) => { + const roles = useContext(AuthContext).roles; + + const { setRole, role } = props; + + const allRoles = [{ role: '00', roleLocalized: 'Всички' }, ...roles]; + + const changeHandler = (event) => { + setRole(event.target.value); + }; + + const filters = allRoles.map((role, idx) => { + return ( + + ); + }); + + return ( + + ); +}; diff --git a/src/components/modules/filter_components/SectionNumber.js b/src/components/modules/filter_components/TextInput.js similarity index 55% rename from src/components/modules/filter_components/SectionNumber.js rename to src/components/modules/filter_components/TextInput.js index 4d3dc23..179bb78 100644 --- a/src/components/modules/filter_components/SectionNumber.js +++ b/src/components/modules/filter_components/TextInput.js @@ -1,6 +1,6 @@ -import React from "react"; +import React from 'react'; -import styled from "styled-components"; +import styled from 'styled-components'; const InputField = styled.input` background: white; @@ -12,14 +12,12 @@ const InputField = styled.input` `; export default (props) => { + const { setTextInput } = props; - const { setSection } = props; - const changeHandler = (event) => { event.preventDefault(); - setSection(event.target.value); + setTextInput(event.target.value); }; - - return ; + return ; }; diff --git a/src/components/modules/protocols/ProtocolFilter.js b/src/components/modules/protocols/ProtocolFilter.js index d07acda..1a10ff1 100644 --- a/src/components/modules/protocols/ProtocolFilter.js +++ b/src/components/modules/protocols/ProtocolFilter.js @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import { AuthContext } from '../../App'; import Statuses from '../filter_components/Statuses'; -import SectionNumber from '../filter_components/SectionNumber'; +import TextInput from '../filter_components/TextInput'; import Origins from '../filter_components/Origins'; import SendBy from '../filter_components/SendBy'; @@ -162,7 +162,7 @@ export default (props) => { N на секция:

{' '} - + Произход:

{' '} diff --git a/src/components/modules/violations/ViolationFilter.js b/src/components/modules/violations/ViolationFilter.js index 038e888..7c7fca3 100644 --- a/src/components/modules/violations/ViolationFilter.js +++ b/src/components/modules/violations/ViolationFilter.js @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import { AuthContext } from '../../App'; import Statuses from '../filter_components/Statuses'; -import SectionNumber from '../filter_components/SectionNumber'; +import TextInput from '../filter_components/TextInput'; import Published from '../filter_components/Published'; import Countries from '../filter_components/Countries'; @@ -153,7 +153,7 @@ export default function ViolationFilter(props) { N на секция:

{' '} - + Публикуван:

{' '} diff --git a/src/components/utils/Util.js b/src/components/utils/Util.js index 95902e4..9464457 100644 --- a/src/components/utils/Util.js +++ b/src/components/utils/Util.js @@ -122,6 +122,10 @@ const formatDateShort = (dateTime) => { ); }; +const mapRoleLocalization = (roles, roleValue) => { + return roles?.find((role) => role.role === roleValue).roleLocalized; +}; + const checkPaths = (path1, path2) => { if (!path1 || !path2) return false; @@ -141,4 +145,5 @@ module.exports = { formatSecs, format, checkPaths, + mapRoleLocalization, }; From 4b7b7f56aa946391d9e1ee864dd2d51b6f192d6f Mon Sep 17 00:00:00 2001 From: Radoslav Popov Date: Thu, 8 Jul 2021 02:49:24 +0200 Subject: [PATCH 10/10] delete unused files --- src/components/layout/Tabs.js | 77 ------------------- src/components/modules/admin/AdminOverview.js | 22 ------ 2 files changed, 99 deletions(-) delete mode 100644 src/components/layout/Tabs.js delete mode 100644 src/components/modules/admin/AdminOverview.js diff --git a/src/components/layout/Tabs.js b/src/components/layout/Tabs.js deleted file mode 100644 index 922dd6a..0000000 --- a/src/components/layout/Tabs.js +++ /dev/null @@ -1,77 +0,0 @@ -import React, { useEffect, useState } from "react"; -import styled from 'styled-components'; - - -const TabsStyle = styled.div` -.custom-tab .tab-content { - min-height: 100px; - border: 1px solid #dcdcdc; - border-top: none; - } - -.nav-tabs { - border-bottom: 1px solid #dee2e6; -} - - .nav { - display: flex; - flex-wrap: wrap; - padding-left: 0; - margin-bottom: 0; - list-style: none; -} -` - -const Tabs = ({ children, active = 0 }) => { - const [activeTab, setActiveTab] = useState(active); - const [tabsData, setTabsData] = useState([]); - - useEffect(() => { - let data = []; - - React.Children.forEach(children, (element) => { - if (!React.isValidElement(element)) return; - - const { - props: { tab, children }, - } = element; - data.push({ tab, children }); - }); - - setTabsData(data); - }, [children]); - - return ( - <> - -
- - -
- {tabsData[activeTab] && tabsData[activeTab].children} -
-
-
- - ); -}; - -const TabPane = ({ children }) => { - return { children }; -}; - -Tabs.TabPane = TabPane; - -export default Tabs; \ No newline at end of file diff --git a/src/components/modules/admin/AdminOverview.js b/src/components/modules/admin/AdminOverview.js deleted file mode 100644 index d657e61..0000000 --- a/src/components/modules/admin/AdminOverview.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -import Tabs from '../../layout/Tabs' - -export default props => { - return ( - <> - -
- - - - bla bla - - - test test - - -
- - ) -} \ No newline at end of file