Commit 2a6bcd87 authored by Djordje's avatar Djordje

Implement documents and tenant invoices tabs

parent 73caab57
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.9.3", "@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3", "@emotion/styled": "^11.9.3",
"@mui/icons-material": "^5.10.9",
"@mui/material": "^5.8.5", "@mui/material": "^5.8.5",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",
...@@ -1810,11 +1811,11 @@ ...@@ -1810,11 +1811,11 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.18.9", "version": "7.20.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.0.tgz",
"integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", "integrity": "sha512-NDYdls71fTXoU8TZHfbBWg7DiZfNzClcKui/+kyi6ppD2L1qnWW3VV6CjtaBXSUGGhiTWJ6ereOIkUvenif66Q==",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.10"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
...@@ -3126,6 +3127,31 @@ ...@@ -3126,6 +3127,31 @@
} }
} }
}, },
"node_modules/@mui/icons-material": {
"version": "5.10.9",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.10.9.tgz",
"integrity": "sha512-sqClXdEM39WKQJOQ0ZCPTptaZgqwibhj2EFV9N0v7BU1PO8y4OcX/a2wIQHn4fNuDjIZktJIBrmU23h7aqlGgg==",
"dependencies": {
"@babel/runtime": "^7.19.0"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
},
"peerDependencies": {
"@mui/material": "^5.0.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": { "node_modules/@mui/material": {
"version": "5.8.5", "version": "5.8.5",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.5.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.5.tgz",
...@@ -14513,9 +14539,9 @@ ...@@ -14513,9 +14539,9 @@
} }
}, },
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.13.9", "version": "0.13.10",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw=="
}, },
"node_modules/regenerator-transform": { "node_modules/regenerator-transform": {
"version": "0.15.0", "version": "0.15.0",
...@@ -18358,11 +18384,11 @@ ...@@ -18358,11 +18384,11 @@
} }
}, },
"@babel/runtime": { "@babel/runtime": {
"version": "7.18.9", "version": "7.20.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.0.tgz",
"integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", "integrity": "sha512-NDYdls71fTXoU8TZHfbBWg7DiZfNzClcKui/+kyi6ppD2L1qnWW3VV6CjtaBXSUGGhiTWJ6ereOIkUvenif66Q==",
"requires": { "requires": {
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.10"
} }
}, },
"@babel/runtime-corejs3": { "@babel/runtime-corejs3": {
...@@ -19286,6 +19312,14 @@ ...@@ -19286,6 +19312,14 @@
"react-is": "^17.0.2" "react-is": "^17.0.2"
} }
}, },
"@mui/icons-material": {
"version": "5.10.9",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.10.9.tgz",
"integrity": "sha512-sqClXdEM39WKQJOQ0ZCPTptaZgqwibhj2EFV9N0v7BU1PO8y4OcX/a2wIQHn4fNuDjIZktJIBrmU23h7aqlGgg==",
"requires": {
"@babel/runtime": "^7.19.0"
}
},
"@mui/material": { "@mui/material": {
"version": "5.8.5", "version": "5.8.5",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.5.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.5.tgz",
...@@ -27311,9 +27345,9 @@ ...@@ -27311,9 +27345,9 @@
} }
}, },
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.13.9", "version": "0.13.10",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw=="
}, },
"regenerator-transform": { "regenerator-transform": {
"version": "0.15.0", "version": "0.15.0",
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.9.3", "@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3", "@emotion/styled": "^11.9.3",
"@mui/icons-material": "^5.10.9",
"@mui/material": "^5.8.5", "@mui/material": "^5.8.5",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",
......
...@@ -43,7 +43,6 @@ function fetchRequestBase(jwt, api_prefix, address, callback, dispatch, method = ...@@ -43,7 +43,6 @@ function fetchRequestBase(jwt, api_prefix, address, callback, dispatch, method =
if (typeof (json) === "string") { if (typeof (json) === "string") {
json = { errorMessage: json } json = { errorMessage: json }
} }
console.log(json)
json.statusText = res.statusText json.statusText = res.statusText
json.status = res.status json.status = res.status
console.error(json) console.error(json)
......
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import jwtDecode from "jwt-decode";
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { getBuilding, getTenantInfo } from "../actions/data-manager";
import { JWT_TENANT } from "../actions/network-manager" import { JWT_TENANT } from "../actions/network-manager"
import { ApplicationTab } from "./ApplicationTab"; import { ApplicationTab } from "./ApplicationTab";
import { NavBar } from "./NavBar/NavBar"; import { NavBar } from "./NavBar/NavBar";
...@@ -18,33 +20,58 @@ const classes = { ...@@ -18,33 +20,58 @@ const classes = {
} }
} as const; } as const;
export const getBuildingId = () => {
let res: any = jwtDecode(localStorage.getItem(JWT_TENANT)!);
return res.bl[0].id;
}
export const Application = () => { export const Application = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [logedIn, setLogedIn] = useState(false); const [logedIn, setLogedIn] = useState(false);
const [sideMenuOpen, setSideMenuOpen] = useState(false); const [sideMenuOpen, setSideMenuOpen] = useState(false);
const [tenantInfo, setTenantInfo] = useState<any>();
const [selectedUnit, setSelectedUnit] = useState<any>();
const [buildingId, setBuildingId] = useState(getBuildingId());
const [building, setBuilding] = useState<any>();
const toggleSideMenu = (open: boolean) => { const toggleSideMenu = (open: boolean) => {
setSideMenuOpen(open); setSideMenuOpen(open);
} }
const handleUnitChange = (unitId: any) => {
setSelectedUnit(building.units.filter((unit: any) => unit._id === unitId)[0]);
};
useEffect(() => { useEffect(() => {
if (!!localStorage.getItem(JWT_TENANT)) { if (!!localStorage.getItem(JWT_TENANT)) {
setLogedIn(true); getBuilding(buildingId, (err: any, data: any) => {
setBuilding(data);
});
return; return;
} }
navigate('../login'); navigate('../login');
}, [navigate]) }, [navigate]);
useEffect(() => {
if (!!building)
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 ( return (
<> <>
{logedIn && {logedIn && building && selectedUnit &&
<Box sx={classes.mainWrapper}> <Box sx={classes.mainWrapper}>
<SideMenu active={sideMenuOpen} toggleSideMenu={toggleSideMenu}/> <SideMenu active={sideMenuOpen} toggleSideMenu={toggleSideMenu} email={tenantInfo.email} firstName={tenantInfo.firstName} />
<Box sx={classes.rightSideWrapper}> <Box sx={classes.rightSideWrapper}>
<NavBar toggleSideMenu={toggleSideMenu}/> <NavBar toggleSideMenu={toggleSideMenu} units={building.units.filter((unit: any) => tenantInfo.buildings[0].units.includes(unit._id))} selectedUnit={selectedUnit} onUnitChange={handleUnitChange} />
<ApplicationTab/> <ApplicationTab building={building} selectedUnit={selectedUnit}/>
</Box> </Box>
</Box> </Box>
} }
......
import { Box } from "@mui/material" import { Box } from "@mui/material"
import jwtDecode from "jwt-decode";
import { useEffect, useState } from "react";
import { Navigate, Route, Routes } from "react-router-dom"; import { Navigate, Route, Routes } from "react-router-dom";
import { getBuilding } from "../actions/data-manager";
import { JWT_TENANT } from "../actions/network-manager";
import { BuildingInvoices } from "./BuildingInvoices/BuildingInvoices"; import { BuildingInvoices } from "./BuildingInvoices/BuildingInvoices";
import { Documents } from "./Documents/Documents";
import { Issues } from "./Issues/Issues"; import { Issues } from "./Issues/Issues";
import { Notifications } from "./Notifications/Notifications"; import { Notifications } from "./Notifications/Notifications";
import { Settings } from "./Settings/Settings"; import { Settings } from "./Settings/Settings";
import { TenantInvoices } from "./TenantInvoices/TenantInvoices";
import { Voting } from "./Voting/Voting"; import { Voting } from "./Voting/Voting";
const classes = { const classes = {
...@@ -24,35 +22,21 @@ const classes = { ...@@ -24,35 +22,21 @@ const classes = {
} }
} as const; } as const;
export const ApplicationTab = () => { export const ApplicationTab = ({ building, selectedUnit }: { building: any, selectedUnit: any }) => {
const getBuildingId = () => {
let res: any = jwtDecode(localStorage.getItem(JWT_TENANT)!);
return res.bl[0].id;
}
const [buildingId, setBuildingId] = useState(getBuildingId());
const [building, setBuilding] = useState<any>();
useEffect(() => {
getBuilding(buildingId, (err: any, data: any) => { setBuilding(data) });
}, []);
return ( return (
<Box sx={classes.wrapper}> <Box sx={classes.wrapper}>
{ building &&
<Routes> <Routes>
<Route path='overview' element={<div>Hello from overview</div>} /> <Route path='overview' element={<div>Hello from overview</div>} />
<Route path='notifications' element={<Notifications notifications={building.notifications}/>} /> <Route path='notifications' element={<Notifications notifications={building.notifications} />} />
<Route path='issues' element={<Issues issues={building.issues}/>} /> <Route path='issues' element={<Issues issues={building.issues} />} />
<Route path='voting' element={<Voting buildingId={buildingId} />} /> <Route path='voting' element={<Voting />} />
<Route path='invoices' element={<div>Hello from invoices</div>} /> <Route path='invoices' element={<TenantInvoices building={building} unit={selectedUnit}/>} />
<Route path='building-invoices' element={<BuildingInvoices buildingFinance={building.finance} />} /> <Route path='building-invoices' element={<BuildingInvoices buildingFinance={building.finance} />} />
<Route path='documents' element={<div>Hello from documents</div>} /> <Route path='documents' element={<Documents documents={building.docs.all || []} />} />
<Route path='settings' element={<Settings />} /> <Route path='settings' element={<Settings />} />
<Route path='*' element={<Navigate replace to="overview" />} /> <Route path='*' element={<Navigate replace to="overview" />} />
</Routes> </Routes>
}
</Box> </Box>
) )
} }
\ No newline at end of file
import { TabWrapper } from "../TabWrapper/TabWrapper";
import { DocumentsList } from "./DocumentsList";
export const Documents = ({ documents }: { documents: any }) => {
return (
<TabWrapper header="Dokumenta">
<DocumentsList documents={documents} />
</TabWrapper>
);
}
\ No newline at end of file
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
import { useTranslation } from "react-i18next";
import { Card } from "../Card/Card";
import { DownloadIcon } from "../icons/DownloadIcon";
import { LinkIcon } from "../icons/LinkIcon";
const classes = {
tableHead: {
'& .MuiTableCell-root': {
color: '#64748b',
fontWeight: 'bold',
paddingBlock: '8px',
paddingInline: '0px',
}
},
tableBody: {
'& .MuiTableCell-root': {
paddingInline: '0px',
}
},
bolder: {
fontWeight: 'bolder'
},
status: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}
} as const;
export const DocumentsList = ({ documents }: { documents: any }) => {
const { t } = useTranslation();
return (
<Card>
<TableContainer >
<Table size="medium">
<TableHead>
<TableRow sx={classes.tableHead}>
<TableCell align="left">{t("Name")}</TableCell>
<TableCell align="center">{t("Date")}</TableCell>
<TableCell align="center">{t("Action")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{documents.map((payable: any) => (
<TableRow key={payable._id} sx={classes.tableBody}>
<TableCell align="left">{payable.name} </TableCell>
<TableCell align="center">{t("formatDate", { date: new Date(payable.created) })}</TableCell>
<TableCell align="center"><a href={payable.link}>{(payable.link.includes('dropboxusercontent')) ? <DownloadIcon /> : <LinkIcon />} </a></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Card>
);
}
\ No newline at end of file
import { Box, Divider, Typography } from "@mui/material"; import { Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { Card } from "../Card/Card";
import { TabWrapper } from "../TabWrapper/TabWrapper"; import { TabWrapper } from "../TabWrapper/TabWrapper";
import { Issue } from "./Issue"; import { Issue } from "./Issue";
export const Issues = ({ issues }: { issues: any }) => { export const Issues = ({ issues }: { issues: any }) => {
const [open, setOpen] = useState(new Array(issues.length).fill(false));
useEffect(() => {
console.log(issues);
}, []);
return ( return (
<TabWrapper header="Aktivnosti"> <TabWrapper header="Aktivnosti">
...@@ -20,6 +13,7 @@ export const Issues = ({ issues }: { issues: any }) => { ...@@ -20,6 +13,7 @@ export const Issues = ({ issues }: { issues: any }) => {
<Issue issue={issue} key={index}/> <Issue issue={issue} key={index}/>
)) ))
} }
{issues.length === 0 && <Typography>Trenutno nema aktivnosti za ovu jedinicu.</Typography>}
</TabWrapper> </TabWrapper>
); );
} }
\ No newline at end of file
...@@ -7,4 +7,6 @@ ...@@ -7,4 +7,6 @@
z-index: 0; z-index: 0;
top: 0; top: 0;
padding: 0.5vh; padding: 0.5vh;
display: flex;
align-items: center;
} }
\ No newline at end of file
import { Box } from "@mui/material";
import { ActivityIcon } from "../icons/ActivityIcon";
import { HamburgerIcon } from "../icons/HamburgerIcon"; import { HamburgerIcon } from "../icons/HamburgerIcon";
import './NavBar.css'; import './NavBar.css';
import { UnitSelect } from "./UnitSelect";
export const NavBar = ({ toggleSideMenu, units, selectedUnit, onUnitChange }: { toggleSideMenu: (open: boolean) => void, units: any, selectedUnit: string, onUnitChange?: (unit: any) => void }) => {
const classes = {
wrapper: {
height: '5vh',
backgroundColor: 'white',
boxSizing: 'border-box',
border: '2px solid rgb(213, 225, 239)',
top: '0'
},
} as const;
export const NavBar = ({toggleSideMenu} : {toggleSideMenu: (open: boolean) => void}) => {
return ( return (
<div className="navbar"> <div className="navbar">
<HamburgerIcon toggleSideMenu={toggleSideMenu}/> <HamburgerIcon toggleSideMenu={toggleSideMenu} />
<UnitSelect units={units} selectedUnit={selectedUnit} onUnitChange={onUnitChange} />
</div> </div>
); );
} }
\ No newline at end of file
import { Box, InputAdornment, MenuItem, Select } from "@mui/material";
import ApartmentIcon from '@mui/icons-material/Apartment';
const classes = {
mainContainer: {
marginLeft: 'auto',
marginRight: '30px'
}
} as const;
export const UnitSelect = ({ units, selectedUnit, onUnitChange }: { units: any[], selectedUnit: any, onUnitChange?: (unit: any) => void }) => {
return (
<Box sx={classes.mainContainer}>
<Select
size="small"
variant='standard'
value={selectedUnit._id}
label="Age"
onChange={(e) => { if (!!onUnitChange) onUnitChange(e.target.value) }}
startAdornment={<InputAdornment position="start">
<ApartmentIcon />
</InputAdornment>}
>
{units.map((unit: any, ind: number) => (<MenuItem value={unit._id} key={ind}>{unit.address}</MenuItem>))}
</Select>
</Box>);
}
\ No newline at end of file
import { Typography } from "@mui/material";
import { TabWrapper } from "../TabWrapper/TabWrapper"; import { TabWrapper } from "../TabWrapper/TabWrapper";
import { Notification } from "./Notification"; import { Notification } from "./Notification";
...@@ -10,5 +11,6 @@ export const Notifications = ({ notifications }: { notifications: any }) => { ...@@ -10,5 +11,6 @@ export const Notifications = ({ notifications }: { notifications: any }) => {
<Notification title={notification.title} description={notification.description} /> <Notification title={notification.title} description={notification.description} />
)) ))
} }
{notifications.length === 0 && <Typography>Trenutno nema obavestenja za ovu jedinicu.</Typography>}
</TabWrapper>); </TabWrapper>);
} }
\ No newline at end of file
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import { useEffect, useState } from "react"; import { useState } from "react";
import { getTenantInfo } from "../../actions/data-manager";
import { LogoutIcon } from "../icons/LogoutIcon"; import { LogoutIcon } from "../icons/LogoutIcon";
import { UserIcon } from "../icons/UserIcon"; import { UserIcon } from "../icons/UserIcon";
import { subMenu } from "./SideMenuData"; import { subMenu } from "./SideMenuData";
...@@ -36,9 +35,8 @@ const classes = { ...@@ -36,9 +35,8 @@ const classes = {
} }
} as const; } as const;
export const SideMenu = ({ active, toggleSideMenu }: { active: boolean, toggleSideMenu: (open: boolean) => void }) => { export const SideMenu = ({ active, toggleSideMenu, email, firstName }: { active: boolean, toggleSideMenu: (open: boolean) => void, email: string, firstName: string }) => {
const [tenantInfo, setTenantInfo] = useState<any>();
const location = useLocation(); const location = useLocation();
const [selectedItem, setSelectedItem] = useState(location.pathname.split('/').at(-1)!); const [selectedItem, setSelectedItem] = useState(location.pathname.split('/').at(-1)!);
...@@ -47,24 +45,15 @@ export const SideMenu = ({ active, toggleSideMenu }: { active: boolean, toggleSi ...@@ -47,24 +45,15 @@ export const SideMenu = ({ active, toggleSideMenu }: { active: boolean, toggleSi
toggleSideMenu(false); toggleSideMenu(false);
}; };
useEffect(() => {
getTenantInfo((err: any, data: any) => {
setTenantInfo(data);
});
}, []);
return ( return (
<>
{tenantInfo &&
<div className={active ? 'side-menu active' : 'side-menu'}> <div className={active ? 'side-menu active' : 'side-menu'}>
<div className="content"> <div className="content">
{/* TODO Create component for logo as well. */} {/* TODO Create component for logo as well. */}
<img src={process.env.PUBLIC_URL + '/assets/logo-svg.svg'} alt='logo' style={classes.logo} /> <img src={process.env.PUBLIC_URL + '/assets/logo-svg.svg'} alt='logo' style={classes.logo} />
<Box sx={classes.accountInfo}> <Box sx={classes.accountInfo}>
<UserIcon /> <UserIcon />
<Typography sx={{ ...classes.menuText, ...classes.boldText }}>{tenantInfo.firstName}</Typography> <Typography sx={{ ...classes.menuText, ...classes.boldText }}>{firstName}</Typography>
<Typography sx={classes.menuText}>{tenantInfo.email}</Typography> <Typography sx={classes.menuText}>{email}</Typography>
<Box sx={classes.signOut}> <Box sx={classes.signOut}>
<Typography style={classes.menuText}>Sign out</Typography> <Typography style={classes.menuText}>Sign out</Typography>
<LogoutIcon /> <LogoutIcon />
...@@ -76,7 +65,5 @@ export const SideMenu = ({ active, toggleSideMenu }: { active: boolean, toggleSi ...@@ -76,7 +65,5 @@ export const SideMenu = ({ active, toggleSideMenu }: { active: boolean, toggleSi
</div> </div>
<div className="transparent" onClick={() => { toggleSideMenu(false) }}></div> <div className="transparent" onClick={() => { toggleSideMenu(false) }}></div>
</div> </div>
}
</>
); );
} }
\ No newline at end of file
import { Box, Divider, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { Card } from "../Card/Card";
import { PaymentOrderItem } from "./PaymentOrderItem";
const classes = {
title: {
marginLeft: 'auto'
},
mainContainer: {
display: 'flex',
alignItems: 'center'
},
itemsContainer: {
flex: 1,
display: 'flex',
flexDirection: 'column',
boxSizing: 'border-box',
paddingBlock: '20px',
gap: '5px'
},
divider: {
marginInline: '10px'
},
itemsGroup: {
display: 'flex',
gap: '10px',
alignItems: 'flex-end'
}
} as const;
export const PaymentOrder = ({ recipient, debit, bankAccount, payReference }: { recipient: string, debit: number, bankAccount: string, payReference: string }) => {
const { t } = useTranslation();
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="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='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} />
<Box sx={classes.itemsGroup}>
<PaymentOrderItem flex={1} label='model' size="small" />
<PaymentOrderItem flex={4} label='poziv na broj' size="small" value={payReference} />
</Box>
</Box>
</Box>
</Card>
);
}
\ No newline at end of file
import { Box, Button, Typography } from "@mui/material";
import { useState } from "react";
import { CopyIcon } from "../icons/CopyIcon";
const classes = {
mainContainer: {
display: 'flex',
flexDirection: 'column',
userSelect: 'none'
},
valueContainer: {
border: '1px solid #64748b',
display: 'flex',
alignItems: 'center',
paddingLeft: '0.5rem',
position: 'relative',
boxSizing: 'border-box'
},
clipboard: {
position: 'absolute',
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.15)',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.15)'
},
left: 0
}
} as const;
export const PaymentOrderItem = ({ flex, label, value, size }: { flex: number, label: string, value?: string, size: string }) => {
const [hovered, setHovered] = useState(false);
return (
<Box sx={{ flex, ...classes.mainContainer }}>
<Typography variant={size === 'large' ? 'body2' : 'caption'}>{label}</Typography>
<Box
sx={{ minHeight: size === 'large' ? '40px' : '25px', ...classes.valueContainer }}
onMouseEnter={() => { setHovered(true); }}
onMouseLeave={() => { setHovered(false); }}
>
{hovered && value && <Button sx={classes.clipboard} onClick={() => { navigator.clipboard.writeText(value) }}> <CopyIcon /> </Button>}
<Typography sx={{ fontWeight: 'bolder' }}>{value}</Typography>
</Box>
</Box>
);
}
\ No newline at end of file
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { PayableStatus } from "../BuildingInvoices/PayableStatus";
import { Card } from "../Card/Card";
import { TabWrapper } from "../TabWrapper/TabWrapper";
import { PaymentOrder } from "./PaymentOrder";
const classes = {
tableContainer: {
minHeight: '150px',
position: 'relative'
},
tableHead: {
'& .MuiTableCell-root': {
color: '#64748b',
fontWeight: 'bold',
paddingBlock: '8px',
paddingInline: '0px',
}
},
tableBody: {
'& .MuiTableCell-root': {
paddingInline: '0px',
}
},
bolder: {
fontWeight: 'bolder'
},
status: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
noRecords: {
position: 'absolute',
top: `calc(50% - 10px)`,
left: 'calc(50% - 30px)'
}
} as const;
type Test = {
invoices: any[],
bankAccount: any,
lastUnpaid: any,
payReference: string
}
export const TenantInvoices = ({ building, unit }: { building: any, unit: any }) => {
const getInfo = (): 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 };
}
const { t } = useTranslation();
const [info, setInfo] = useState<Test>(getInfo());
useEffect(() => {
setInfo(getInfo());
}, [unit, building]);
return (
<TabWrapper header="Moj Račun">
<Card>
<TableContainer sx={classes.tableContainer}>
<Table size="medium">
<TableHead>
<TableRow sx={classes.tableHead}>
<TableCell align="left">{t("Invoice Id")}</TableCell>
<TableCell align="center">{t("Period")}</TableCell>
<TableCell align="center">{t("Total")}</TableCell>
<TableCell align="center">{t("Status")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{info.invoices.map((invoice: any) => (
<TableRow key={invoice._id} sx={classes.tableBody}>
<TableCell align="left">{invoice.invoiceId}</TableCell>
<TableCell align="center">{t("formatDate", { date: new Date(invoice.periodStart) }) + " - " + t("formatDate", { date: new Date(invoice.periodEnd) })} </TableCell>
<TableCell align="center" sx={classes.bolder}>{t("priceFormat", { num: invoice.total })} </TableCell>
<TableCell sx={classes.status}><PayableStatus status={invoice.status === "paid" || invoice.type === "fund"} /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
{info.invoices.length === 0 && <Typography sx={classes.noRecords} >No records</Typography>}
</TableContainer>
</Card>
<PaymentOrder
recipient={building.name}
debit={unit.debit}
bankAccount={info.bankAccount}
payReference={info.payReference} />
</TabWrapper>
);
}
\ No newline at end of file
import { Box, Button, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Typography } from "@mui/material"; import { Button, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Typography } from "@mui/material";
import jwtDecode from "jwt-decode"; import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import nacl from "tweetnacl"; import nacl from "tweetnacl";
import { getTenentVoting, postVote } from "../../actions/data-manager"; import { getTenentVoting, postVote } from "../../actions/data-manager";
import { JWT_TENANT } from "../../actions/network-manager";
import { VotingEntity } from "../../internal-types"; import { VotingEntity } from "../../internal-types";
import { keyToBase64, PRIVATE_KEY } from "../register/RegisterForm"; import { keyToBase64, PRIVATE_KEY } from "../register/RegisterForm";
import { Warning } from "./Warning"; import { Warning } from "./Warning";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import { TabWrapper } from "../TabWrapper/TabWrapper"; import { TabWrapper } from "../TabWrapper/TabWrapper";
import { Card } from "../Card/Card"; import { Card } from "../Card/Card";
import { getBuildingId } from "../Application";
const classes = { const classes = {
wrapper: { wrapper: {
...@@ -34,7 +33,7 @@ const classes = { ...@@ -34,7 +33,7 @@ const classes = {
} }
} as const; } as const;
export const Voting = ({ buildingId }: { buildingId: string }): JSX.Element => { export const Voting = (): JSX.Element => {
const [status, setStatus] = useState(); const [status, setStatus] = useState();
const [voting, setVoting] = useState<VotingEntity>(); const [voting, setVoting] = useState<VotingEntity>();
...@@ -43,7 +42,7 @@ export const Voting = ({ buildingId }: { buildingId: string }): JSX.Element => { ...@@ -43,7 +42,7 @@ export const Voting = ({ buildingId }: { buildingId: string }): JSX.Element => {
const [privateKey, setPrivateKey] = useState(localStorage.getItem(PRIVATE_KEY)); const [privateKey, setPrivateKey] = useState(localStorage.getItem(PRIVATE_KEY));
useEffect(() => { useEffect(() => {
getTenentVoting(buildingId, (err: any, res: any, disp: any) => { getTenentVoting(getBuildingId(), (err: any, res: any, disp: any) => {
setStatus(res.status || "not allowed"); setStatus(res.status || "not allowed");
setVoting(res); setVoting(res);
}) })
...@@ -60,7 +59,7 @@ export const Voting = ({ buildingId }: { buildingId: string }): JSX.Element => { ...@@ -60,7 +59,7 @@ export const Voting = ({ buildingId }: { buildingId: string }): JSX.Element => {
let vote = { vote: voteDataStr, signature }; let vote = { vote: voteDataStr, signature };
postVote(buildingId, vote, (err: any, data: any) => { postVote(getBuildingId(), vote, (err: any, data: any) => {
if (err) { if (err) {
if (err.status === 406) { if (err.status === 406) {
setErrorText("Signature not valid!"); setErrorText("Signature not valid!");
...@@ -72,9 +71,6 @@ export const Voting = ({ buildingId }: { buildingId: string }): JSX.Element => { ...@@ -72,9 +71,6 @@ export const Voting = ({ buildingId }: { buildingId: string }): JSX.Element => {
} }
})(null); })(null);
} }
return (<> return (<>
...@@ -94,7 +90,7 @@ export const Voting = ({ buildingId }: { buildingId: string }): JSX.Element => { ...@@ -94,7 +90,7 @@ export const Voting = ({ buildingId }: { buildingId: string }): JSX.Element => {
<FormControl> <FormControl>
<FormLabel>Opcije</FormLabel> <FormLabel>Opcije</FormLabel>
<RadioGroup value={selectedCandidateId} onChange={(event) => { setSelectedCandidateId(event.target.value) }}> <RadioGroup value={selectedCandidateId} onChange={(event) => { setSelectedCandidateId(event.target.value) }}>
{voting.candidates.map((candidate, ind: number) => (<FormControlLabel value={candidate._id} control={<Radio size="small"/>} label={candidate.name} key={ind} />))} {voting.candidates.map((candidate, ind: number) => (<FormControlLabel value={candidate._id} control={<Radio size="small" />} label={candidate.name} key={ind} />))}
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
<Button variant='contained' sx={classes.button} disabled={selectedCandidateId === ''} onClick={handleVote}>Glasaj</Button> <Button variant='contained' sx={classes.button} disabled={selectedCandidateId === ''} onClick={handleVote}>Glasaj</Button>
......
import './Icons.css';
export const CopyIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="copy-icon">
<path d="M7 3.5A1.5 1.5 0 018.5 2h3.879a1.5 1.5 0 011.06.44l3.122 3.12A1.5 1.5 0 0117 6.622V12.5a1.5 1.5 0 01-1.5 1.5h-1v-3.379a3 3 0 00-.879-2.121L10.5 5.379A3 3 0 008.379 4.5H7v-1z" />
<path d="M4.5 6A1.5 1.5 0 003 7.5v9A1.5 1.5 0 004.5 18h7a1.5 1.5 0 001.5-1.5v-5.879a1.5 1.5 0 00-.44-1.06L9.44 6.439A1.5 1.5 0 008.378 6H4.5z" />
</svg>
);
}
import './Icons.css';
export const DownloadIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="action-icons">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
);
}
...@@ -31,6 +31,21 @@ ...@@ -31,6 +31,21 @@
cursor: pointer; cursor: pointer;
} }
.copy-icon {
width: 20px;
fill: rgb(86, 107, 130);
}
.action-icons {
width: 20px;
cursor: pointer;
stroke: rgb(86, 107, 130);
}
.action-icons:hover {
stroke: rgb(19, 71, 175);
scale: 1.1;
}
@media (max-width: 800px) { @media (max-width: 800px) {
.alert { .alert {
width: 100px; width: 100px;
......
import './Icons.css';
export const LinkIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="action-icons">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
);
}
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
"Status": "Status", "Status": "Status",
"formatDate": "{{date, DD/MM/YYYY}}", "formatDate": "{{date, DD/MM/YYYY}}",
"priceFormat": "{{num, num2format}}", "priceFormat": "{{num, num2format}}",
"nalog": "{{num, nalog}}",
"per group/per unit":"ukupno", "per group/per unit":"ukupno",
"per unit/per unit": "po stanu", "per unit/per unit": "po stanu",
"per unit/per area": "po kvadraturi" "per unit/per area": "po kvadraturi"
......
...@@ -12,7 +12,7 @@ i18next ...@@ -12,7 +12,7 @@ i18next
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
resources, resources,
debug: true, debug: false,
fallbackLng: 'sr', fallbackLng: 'sr',
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
...@@ -25,6 +25,10 @@ i18next ...@@ -25,6 +25,10 @@ i18next
if (format === 'num2format') if (format === 'num2format')
return new Intl.NumberFormat(language, { maximumFractionDigits: 2, minimumFractionDigits: 2 }).format(data) + ' RSD'; return new Intl.NumberFormat(language, { maximumFractionDigits: 2, minimumFractionDigits: 2 }).format(data) + ' RSD';
if (format === 'nalog')
return new Intl.NumberFormat(language, { maximumFractionDigits: 2, minimumFractionDigits: 2 }).format(data);
return '' return ''
} }
}, },
......
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