Commit fa7f194c authored by Djordje's avatar Djordje

Add protected route, qr code on payment order, fix missing translations and eslint warnings

parent daccff3f
......@@ -26,6 +26,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^11.18.4",
"react-qr-code": "^2.0.8",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"tweetnacl": "^1.0.3",
......@@ -14049,6 +14050,11 @@
"teleport": ">=0.2.0"
}
},
"node_modules/qr.js": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
"integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="
},
"node_modules/qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
......@@ -14337,6 +14343,24 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"node_modules/react-qr-code": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.8.tgz",
"integrity": "sha512-zYO9EAPQU8IIeD6c6uAle7NlKOiVKs8ji9hpbWPTGxO+FLqBN2on+XCXQvnhm91nrRd306RvNXUkUNcXXSfhWA==",
"dependencies": {
"prop-types": "^15.8.1",
"qr.js": "0.0.0"
},
"peerDependencies": {
"react": "^16.x || ^17.x || ^18.x",
"react-native-svg": "*"
},
"peerDependenciesMeta": {
"react-native-svg": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
......@@ -26990,6 +27014,11 @@
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw=="
},
"qr.js": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
"integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="
},
"qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
......@@ -27190,6 +27219,15 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"react-qr-code": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.8.tgz",
"integrity": "sha512-zYO9EAPQU8IIeD6c6uAle7NlKOiVKs8ji9hpbWPTGxO+FLqBN2on+XCXQvnhm91nrRd306RvNXUkUNcXXSfhWA==",
"requires": {
"prop-types": "^15.8.1",
"qr.js": "0.0.0"
}
},
"react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
......@@ -21,6 +21,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^11.18.4",
"react-qr-code": "^2.0.8",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"tweetnacl": "^1.0.3",
......
import React from 'react';
import './App.css';
import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'
import { EntryPage } from './components/entry-page/EntryPage';
......@@ -6,6 +5,7 @@ import { RegisterForm } from './components/register/RegisterForm';
import { LoginForm } from './components/LoginForm';
import { Application } from './components/Application';
import { createTheme, ThemeProvider } from '@mui/material';
import { ProtectedRoute } from './components/ProtectedRoute';
const theme = createTheme({
palette: {
......@@ -19,7 +19,7 @@ const theme = createTheme({
fontSize: '1rem',
},
allVariants: {
color: 'rgb(39, 49, 58)'
color: 'rgb(39, 49, 58)'
}
},
});
......@@ -33,7 +33,7 @@ function App() {
<Route path='register/:buildingId' element={<EntryPage children={<RegisterForm />} />} />
<Route path='register/error' element={<EntryPage children={<div>Invalid invitational link - screen to be implemented</div>} />} />
<Route path='login' element={<EntryPage children={<LoginForm />} />} />
<Route path='tenant/*' element={<Application />} />
<Route path='tenant/*' element={<ProtectedRoute><Application /></ProtectedRoute>} />
<Route path='*' element={<Navigate replace to="login" />} />
</Routes>
</Router>
......
import { fetchRequestManager, fetchRequestTenant } from './network-manager'
import { fetchBuildingsSuccess, genericAction, TENANT_BUILDING_RECEIVED, UNAUTHENTICATED, TENANT_VOTING_RECEIVED } from './index'
import { fetchBuildingsSuccess, genericAction, UNAUTHENTICATED } from './index'
export function fetchBuildings() {
......
import { Box } from "@mui/material";
import jwtDecode from "jwt-decode";
import { useEffect, useRef, useState } from "react"
import { useNavigate } from "react-router-dom"
import React from "react";
import { useEffect, useState } from "react"
import { getBuilding, getTenantInfo } from "../actions/data-manager";
import { JWT_TENANT } from "../actions/network-manager"
import { ApplicationTab } from "./ApplicationTab";
......@@ -25,10 +25,10 @@ export const getBuildingId = () => {
return res.bl[0].id;
}
export const ApplicationContext = React.createContext<any>(null);
export const Application = () => {
const navigate = useRef(useNavigate());
const [logedIn, setLogedIn] = useState(false);
const [sideMenuOpen, setSideMenuOpen] = useState(false);
const [tenantInfo, setTenantInfo] = useState<any>();
const [selectedUnit, setSelectedUnit] = useState<any>('');
......@@ -44,15 +44,9 @@ export const Application = () => {
};
useEffect(() => {
if (!!localStorage.getItem(JWT_TENANT)) {
getBuilding(buildingId, (err: any, data: any) => {
setBuilding(data);
});
return;
}
navigate.current('../login');
getBuilding(buildingId, (err: any, data: any) => {
setBuilding(data);
});
}, [buildingId]);
useEffect(() => {
......@@ -60,13 +54,12 @@ export const Application = () => {
getTenantInfo((err: any, data: any) => {
setTenantInfo(data);
setSelectedUnit(building.units.filter((unit: any) => unit._id === data.buildings[0].units[0])[0] || '');
setLogedIn(true);
});
}, [building]);
return (
<>
{logedIn && building &&
<ApplicationContext.Provider value={{ tenantInfo }}>
{!!building && !!tenantInfo &&
<Box sx={classes.mainWrapper}>
<SideMenu active={sideMenuOpen} toggleSideMenu={toggleSideMenu} email={tenantInfo.email} firstName={tenantInfo.firstName} />
<Box sx={classes.rightSideWrapper}>
......@@ -75,6 +68,6 @@ export const Application = () => {
</Box>
</Box>
}
</>
</ApplicationContext.Provider>
)
}
\ No newline at end of file
......@@ -4,7 +4,7 @@ import { BuildingInvoicesList } from "./BuildingInvoicesList";
export const BuildingInvoices = ({ buildingFinance }: { buildingFinance: any }) => {
return (
<TabWrapper header='Finansije Zgrade'>
<TabWrapper header='Building finance'>
<BuildingInvoicesList buildingFinance={buildingFinance} />
</TabWrapper>
);
......
......@@ -43,15 +43,15 @@ export const BuildingInvoicesList = ({ buildingFinance }: { buildingFinance: any
let finance = buildingFinance || {}
let buildingPayable = buildingFinance && buildingFinance.payable && buildingFinance.payable.reduce((acc: any, x: any) => acc + x.price, 0)
let inflowSum = buildingFinance && buildingFinance.inflow && buildingFinance.inflow.reduce((acc: any, x: any) => acc + x.amount, 0)
let outflowSum = buildingFinance && buildingFinance.outflow && buildingFinance.outflow.reduce((acc: any, x: any) => acc + x.amount, 0)
let sumFundsPayables = (buildingFinance && buildingFinance.fund && buildingFinance.fund.reduce((acc: any, x: any) => acc + (x.receivable || 0), 0) || 0)
// let buildingPayable = buildingFinance && buildingFinance.payable && buildingFinance.payable.reduce((acc: any, x: any) => acc + x.price, 0)
// let inflowSum = buildingFinance && buildingFinance.inflow && buildingFinance.inflow.reduce((acc: any, x: any) => acc + x.amount, 0)
// let outflowSum = buildingFinance && buildingFinance.outflow && buildingFinance.outflow.reduce((acc: any, x: any) => acc + x.amount, 0)
// let sumFundsPayables = (buildingFinance && buildingFinance.fund && buildingFinance.fund.reduce((acc: any, x: any) => acc + (x.receivable || 0), 0) || 0)
let fullBalance = buildingFinance && inflowSum && buildingPayable && (inflowSum - buildingPayable - sumFundsPayables)
let cashBalance = buildingFinance && buildingFinance.balance
let calculatedCashBalance = buildingFinance && (inflowSum - outflowSum)
// let fullBalance = buildingFinance && inflowSum && buildingPayable && (inflowSum - buildingPayable - sumFundsPayables)
// let cashBalance = buildingFinance && buildingFinance.balance
// let calculatedCashBalance = buildingFinance && (inflowSum - outflowSum)
let outflows = buildingFinance ? buildingFinance.outflow : []
......
......@@ -14,7 +14,7 @@ export const Issues = ({ issues }: { issues: any }) => {
<Issue issue={issue} key={index} />
))
}
{issues.length === 0 && <Typography>{t("Trenutno nema aktivnosti za ovu jedinicu")}.</Typography>}
{issues.length === 0 && <Typography>{t("Currently there are not notifications for this unit")}.</Typography>}
</TabWrapper>
);
}
\ No newline at end of file
......@@ -15,7 +15,7 @@ export const UnitSelect = ({ units, selectedUnit, onUnitChange }: { units: any[]
<Select
size="small"
variant='standard'
value={selectedUnit._id}
value={(!!selectedUnit && selectedUnit._id) || ''}
label="Age"
onChange={(e) => { if (!!onUnitChange) onUnitChange(e.target.value) }}
startAdornment={<InputAdornment position="start">
......
import { ReactNode } from "react";
import { Navigate, useLocation } from "react-router";
import { JWT_TENANT } from "../actions/network-manager";
export const ProtectedRoute = ({ children }: { children: ReactNode }): JSX.Element => {
const location = useLocation();
const isAuthenticated = !!localStorage.getItem(JWT_TENANT);
if (isAuthenticated) {
return <>{children}</>
} else {
return <Navigate to={{ pathname: '/app/login' }} state={{ from: location }} replace />;
}
}
\ No newline at end of file
import { Box, Divider, Typography } from "@mui/material";
import { useContext } from "react";
import { useTranslation } from "react-i18next";
import QRCode from "react-qr-code";
import { ApplicationContext } from "../Application";
import { Card } from "../Card/Card";
import { PaymentOrderItem } from "./PaymentOrderItem";
......@@ -9,7 +12,7 @@ const classes = {
},
mainContainer: {
display: 'flex',
alignItems: 'center'
alignItems: 'baseline'
},
itemsContainer: {
flex: 1,
......@@ -33,28 +36,40 @@ const classes = {
export const PaymentOrder = ({ recipient, debit, bankAccount, payReference }: { recipient: string, debit: number, bankAccount: string, payReference: string }) => {
const { t } = useTranslation();
const { tenantInfo } = useContext(ApplicationContext);
const qrCode = `K:PR|V:01|C:1|R:${bankAccount}|N:${recipient}|I:RSD${(debit > 0 ? debit : 0).toFixed(2).toString().replace('.', ',')}` +
`|P:${tenantInfo.firstName} ${tenantInfo.lastName}|SF:289|S:Po nalogu gradjana|RO:00${payReference.replaceAll('-', '')}`;
return (
<Card>
<Typography sx={classes.title}>Nalog za uplatu</Typography>
<Box sx={classes.mainContainer}>
<Box sx={classes.itemsContainer}>
<PaymentOrderItem flex={1} label="Uplatilac" size="large" />
<PaymentOrderItem flex={1} label="Uplatilac" size="large" value={`${tenantInfo.firstName} ${tenantInfo.lastName}`} />
<PaymentOrderItem flex={1} label="Svrha uplate" size="large" />
<PaymentOrderItem flex={1} label="Primalac" size="large" value={recipient} />
</Box>
<Divider sx={classes.divider} orientation="vertical" variant="middle" flexItem />
<Box sx={classes.itemsContainer}>
<Box sx={classes.itemsGroup}>
<PaymentOrderItem flex={1} label='sifra placanja' size="small" />
<PaymentOrderItem flex={1} label='sifra placanja' size="small" value='221' />
<PaymentOrderItem flex={1} label='valuta' size="small" value="RSD" />
<PaymentOrderItem flex={4} label='iznos' size="small" value={t("nalog", { num: (debit > 0 ? debit : 0) })} />
</Box>
<PaymentOrderItem flex={1} label='Racun primaonca' size="small" value={bankAccount} />
<PaymentOrderItem flex={1} label='Racun primaoca' size="small" value={bankAccount} />
<Box sx={classes.itemsGroup}>
<PaymentOrderItem flex={1} label='model' size="small" />
<PaymentOrderItem flex={4} label='poziv na broj' size="small" value={payReference} />
<PaymentOrderItem flex={1} label='model' size="small" value='00' />
<PaymentOrderItem flex={4} label='poziv na broj' size="small" value={payReference.replaceAll('-', '')} />
</Box>
<div style={{ marginLeft: "auto", maxWidth: 80 }}>
<QRCode
size={256}
style={{ height: "auto", maxWidth: "100%", width: "100%" }}
value={qrCode}
viewBox={`0 0 256 256`}
/>
</div>
</Box>
</Box>
</Card>
......
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { PayableStatus } from "../BuildingInvoices/PayableStatus";
import { Card } from "../Card/Card";
......@@ -49,22 +49,22 @@ type Test = {
export const TenantInvoices = ({ building, unit }: { building: any, unit: any }) => {
const getInfo = (): Test => {
const getInfo = useCallback((): Test => {
let invoices = unit.invoices || [];
let bankAccount = (building.finance || {}).bankAccount;
let lastUnpaid = invoices.find((x: any) => x.status !== "paid") || {};
let payReference = lastUnpaid.invoiceId || (invoices[invoices.length - 1] || {}).invoiceId;
return { invoices, bankAccount, lastUnpaid, payReference };
}
}, [building.finance, unit.invoices]);
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const [info, setInfo] = useState<Test>(getInfo());
useEffect(() => {
setInfo(getInfo());
}, [unit, building]);
}, [unit, building, getInfo]);
return (
<TabWrapper header="My Account">
......@@ -93,12 +93,13 @@ export const TenantInvoices = ({ building, unit }: { building: any, unit: any })
{info.invoices.length === 0 && <Typography sx={classes.noRecords} >{t('No records')}</Typography>}
</TableContainer>
</Card>
<PaymentOrder
recipient={building.name}
debit={unit.debit}
bankAccount={info.bankAccount}
payReference={info.payReference} />
{i18n.language === 'sr' &&
<PaymentOrder
recipient={building.name}
debit={unit.debit}
bankAccount={info.bankAccount}
payReference={info.payReference} />
}
</TabWrapper>
);
}
\ No newline at end of file
......@@ -40,7 +40,7 @@ export const Voting = (): JSX.Element => {
const [voting, setVoting] = useState<VotingEntity>();
const [selectedCandidateId, setSelectedCandidateId] = useState('');
const [errorText, setErrorText] = useState('');
const [privateKey, setPrivateKey] = useState(localStorage.getItem(PRIVATE_KEY));
const [privateKey] = useState(localStorage.getItem(PRIVATE_KEY));
const { t } = useTranslation();
......@@ -97,6 +97,7 @@ export const Voting = (): JSX.Element => {
</RadioGroup>
</FormControl>
<Button variant='contained' sx={classes.button} disabled={selectedCandidateId === ''} onClick={handleVote}>{t("Vote")}</Button>
{!!errorText && <Typography color='error'>{errorText}</Typography>}
</>
}
{status === "voted" && <Typography>{t('Thank you for voting')}. </Typography>}
......@@ -105,7 +106,7 @@ export const Voting = (): JSX.Element => {
</>
}
{
!privateKey && <Warning text={t("Voting is disabled because private key isn't saved in application. Please, set up key in settings")} />
!privateKey && <Warning text={"Voting is disabled because private key isn't saved in application. Please, set up key in settings"} />
}
</TabWrapper>
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment