diff --git a/.env.development b/.env.development index 8ee8ab0..75bdb63 100644 --- a/.env.development +++ b/.env.development @@ -3,3 +3,4 @@ VITE_API_URL="https://apisandbox.denguechatplus.org" VITE_HOST="develop.denguechatplus.org" VITE_DEFAULT_LANG="es" VITE_LOCALE="es-ES" +ROLLBAR_ACCESS_TOKEN= diff --git a/README.md b/README.md index 4d5ad42..3d43d40 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# DengueChat+ +# DengueChatPlus Web project for DengueChat diff --git a/index.html b/index.html index d161e46..ce30f63 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ /> - DengueChat+ + DengueChatPlus
diff --git a/package.json b/package.json index 077be9b..e5ae0e2 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@mui/icons-material": "^5.16.1", "@mui/material": "^5.15.20", "@mui/x-date-pickers": "^7.7.1", + "@rollbar/react": "^0.12.0-beta", "@tanstack/react-query": "^5.59.16", "axios": "^1.7.2", "axios-auth-refresh": "^3.3.6", @@ -44,6 +45,7 @@ "react-infinite-scroll-component": "^6.1.0", "react-number-format": "^5.4.0", "react-router-dom": "^6.24.0", + "rollbar": "^2.26.4", "tailwind-merge": "^2.3.0", "vite-plugin-svgr": "^4.2.0", "zod": "^3.23.8" diff --git a/src/App.tsx b/src/App.tsx index 20bff9c..2203a7f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ const App: React.FC = () => ( <>
-
DengueChat+
+
DengueChatPlus
); diff --git a/src/assets/icons/fact-check.svg b/src/assets/icons/fact-check.svg new file mode 100644 index 0000000..abd9652 --- /dev/null +++ b/src/assets/icons/fact-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/help.svg b/src/assets/icons/help.svg new file mode 100644 index 0000000..cbeb881 --- /dev/null +++ b/src/assets/icons/help.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/reports.svg b/src/assets/icons/reports.svg new file mode 100644 index 0000000..3dbb2f0 --- /dev/null +++ b/src/assets/icons/reports.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/CommentBox.tsx b/src/components/CommentBox.tsx index 69e14bf..828fe6e 100644 --- a/src/components/CommentBox.tsx +++ b/src/components/CommentBox.tsx @@ -55,7 +55,7 @@ const CommentBox = ({ postId }: CommentBoxProps) => { return ( - + {acronym} diff --git a/src/components/PostBox.tsx b/src/components/PostBox.tsx index e2c12d7..bd84ece 100644 --- a/src/components/PostBox.tsx +++ b/src/components/PostBox.tsx @@ -6,11 +6,13 @@ import ThumbsUp from '@/assets/icons/thumbs-up.svg'; import Trash from '@/assets/icons/trash.svg'; import Text from '@/themed/text/Text'; import CommentBox from './CommentBox'; +import { formatDateFromString } from '@/util'; +import useLangContext from '@/hooks/useLangContext'; interface PostProps { id: number; author: string; - date: number; + date: string; location: string; text: string; image?: { photo_url: string }; @@ -20,20 +22,21 @@ interface PostProps { } const PostBox = ({ author, date, location, text, likes, image, id, comments, acronym }: PostProps) => { + const langContext = useLangContext(); const { t } = useTranslation('feed'); const [imageLoaded, setImageLoaded] = useState(false); const [openComments, setOpenComments] = useState(false); return ( - + - + {acronym} {author} - {date} • {location} + {formatDateFromString(langContext.state.selected, date)} • {location} diff --git a/src/components/RiskChart.tsx b/src/components/RiskChart.tsx index 876ae20..01081e9 100644 --- a/src/components/RiskChart.tsx +++ b/src/components/RiskChart.tsx @@ -24,10 +24,13 @@ const RiskChart = () => { const [{ data, loading }] = useAxios({ url: `reports/house_status`, + headers: { + source: 'visits', + }, }); const label = `${(user?.team as BaseObject)?.name}: ${t('riskChart.title')}`; - + const totalQuantity = (data?.redQuantity || 0) + (data?.orangeQuantity || 0) + (data?.greenQuantity || 0); return ( @@ -35,13 +38,24 @@ const RiskChart = () => { {loading && <Loader />} {!loading && ( <> - <ProgressBar label={t('riskChart.greenSites')} progress={data?.greenQuantity || 0} color="bg-green-600" /> + <ProgressBar + label={t('riskChart.greenSites')} + value={data?.greenQuantity || 0} + progress={totalQuantity > 0 ? Math.round(((data?.greenQuantity || 0) / totalQuantity) * 100) : 0} + color="bg-green-600" + /> <ProgressBar label={t('riskChart.yellowSites')} - progress={data?.orangeQuantity || 0} + value={data?.orangeQuantity || 0} + progress={totalQuantity > 0 ? Math.round(((data?.orangeQuantity || 0) / totalQuantity) * 100) : 0} color="bg-yellow-600" /> - <ProgressBar label={t('riskChart.redSites')} progress={data?.redQuantity || 0} color="bg-red-600" /> + <ProgressBar + label={t('riskChart.redSites')} + value={data?.redQuantity || 0} + progress={totalQuantity > 0 ? Math.round(((data?.redQuantity || 0) / totalQuantity) * 100) : 0} + color="bg-red-600" + /> </> )} </Box> diff --git a/src/components/SelectLanguageComponent.tsx b/src/components/SelectLanguageComponent.tsx index a4cda3d..19773e0 100644 --- a/src/components/SelectLanguageComponent.tsx +++ b/src/components/SelectLanguageComponent.tsx @@ -31,7 +31,7 @@ const getLocaleDisplayName = (locale: string, displayLocale?: string) => { return displayName.charAt(0).toLocaleUpperCase() + displayName.slice(1); }; -function SelectLanguageComponent() { +function SelectLanguageComponent({ className }: { className?: string }) { const { i18n } = useTranslation(); const { resolvedLanguage: currentLanguage } = i18n; @@ -70,6 +70,7 @@ function SelectLanguageComponent() { onChange(e.target.value); } }} + className={className} > {options.map((option) => ( <MenuItem key={`key-${option.value}`} value={option.value}> diff --git a/src/components/SitesReport.tsx b/src/components/SitesReport.tsx index cd95b55..75c9786 100644 --- a/src/components/SitesReport.tsx +++ b/src/components/SitesReport.tsx @@ -7,12 +7,24 @@ const SitesReport = () => { const { t } = useTranslation('feed'); return ( - <Box className="border-solid border-neutral-100 rounded-md p-6 mb-4"> + <Box className="border-solid border-neutral-100 rounded-md p-6 mb-10"> <Title label={t('sitesReport.title')} type="subsection" className="mb-0" /> <Box className="flex flex-col mt-6"> <> - <ProgressBar label={t('sitesReport.title')} progress={60} color="bg-green-600" /> - <ProgressBar label={t('sitesReport.quantity')} progress={80} color="bg-green-800" /> + <ProgressBar + label={t('sitesReport.title')} + value={60} + progress={60} + color="bg-green-600" + tooltip={t('sitesReport.tarikiSiteInfo')} + /> + <ProgressBar + label={t('sitesReport.quantity')} + value={80} + progress={80} + color="bg-green-800" + tooltip={t('sitesReport.greenContainersInfo')} + /> </> </Box> </Box> diff --git a/src/components/dialog/CreateCityDialog.tsx b/src/components/dialog/CreateCityDialog.tsx index 6c389c4..03418ba 100644 --- a/src/components/dialog/CreateCityDialog.tsx +++ b/src/components/dialog/CreateCityDialog.tsx @@ -120,7 +120,7 @@ export function CreateCityDialog({ handleClose, updateTable }: CreateCityDialogP <FormInput className="mt-2" name="name" - label={t('admin:cities.form.name')} + label={t('admin:cities.form.name_placeholder')} type="text" placeholder={t('admin:cities.form.name_placeholder')} /> diff --git a/src/components/dialog/CreateTeamDialog.tsx b/src/components/dialog/CreateTeamDialog.tsx index fc46ff8..431e6aa 100644 --- a/src/components/dialog/CreateTeamDialog.tsx +++ b/src/components/dialog/CreateTeamDialog.tsx @@ -11,7 +11,7 @@ import { ErrorResponse } from 'react-router-dom'; import useAxios from 'axios-hooks'; import { zodResolver } from '@hookform/resolvers/zod'; import useCreateMutation from '@/hooks/useCreateMutation'; -import { FormSelectOption } from '@/schemas'; +import { BaseObject, FormSelectOption } from '@/schemas'; import { CreateTeam, CreateTeamInputType, createTeamSchema } from '@/schemas/create'; import { Team } from '@/schemas/entities'; import FormMultipleSelect from '@/themed/form-multiple-select/FormMultipleSelect'; @@ -21,6 +21,7 @@ import { Button } from '../../themed/button/Button'; import { FormInput } from '../../themed/form-input/FormInput'; import { Title } from '../../themed/title/Title'; import { convertToFormSelectOptions, extractAxiosErrorData } from '../../util'; +import useStateContext from '@/hooks/useStateContext'; export interface EditUserProps { user: IUser; @@ -32,8 +33,8 @@ interface CreateTeamDialogProps { } export function CreateTeamDialog({ handleClose, updateTable }: CreateTeamDialogProps) { - // const { state } = useStateContext(); - // const user = state.user as IUser; + const { state } = useStateContext(); + const user = state.user as IUser; const { t } = useTranslation(['register', 'errorCodes', 'admin', 'translation']); const { createMutation: createTeamMutation, loading: mutationLoading } = useCreateMutation<CreateTeam, Team>( `/teams`, @@ -103,6 +104,21 @@ export function CreateTeamDialog({ handleClose, updateTable }: CreateTeamDialogP } }, [wedgesData]); + const [cityOptions, setCityOptions] = useState<FormSelectOption[]>([]); + + const [{ data: cityData, loading: loadingCities }] = useAxios<ExistingDocumentObject, unknown, ErrorResponse>({ + url: `countries/${(user.country as BaseObject).id}/states/${user.state.id}/cities`, + }); + + useEffect(() => { + if (!cityData) return; + const deserializedData = deserialize(cityData); + if (Array.isArray(deserializedData)) { + const cities = convertToFormSelectOptions(deserializedData); + setCityOptions(cities); + } + }, [cityData]); + const { enqueueSnackbar } = useSnackbar(); const methods = useForm<CreateTeamInputType>({ @@ -166,7 +182,7 @@ export function CreateTeamDialog({ handleClose, updateTable }: CreateTeamDialogP autoComplete="off" className="w-full p-8" > - <Title type="section" className="self-center mb-8i w-full" label="Create team" /> + <Title type="section" className="self-center mb-8i w-full" label={t('admin:teams.create_team')} /> <Grid container spacing={2}> <Grid item xs={12} sm={12}> <FormInput @@ -188,22 +204,12 @@ export function CreateTeamDialog({ handleClose, updateTable }: CreateTeamDialogP </Grid> <Grid item xs={12} sm={12}> <FormSelect - name="leaderId" + name="cityId" className="mt-2" - label={t('admin:teams.form.team_leader')} - loading={loadingUsers} - options={userOptions} - placeholder={t('admin:teams.form.team_leader_placeholder')} - /> - </Grid> - <Grid item xs={12} sm={12}> - <FormSelect - name="organizationId" - className="mt-2" - label={t('admin:teams.form.organization')} - loading={loadingOrganizations} - options={organizationOptions} - placeholder={t('admin:teams.form.organization_placeholder')} + label={t('admin:teams.form.city')} + loading={loadingCities} + options={cityOptions} + placeholder={t('admin:teams.form.city_placeholder')} /> </Grid> <Grid item xs={12} sm={12}> @@ -226,6 +232,16 @@ export function CreateTeamDialog({ handleClose, updateTable }: CreateTeamDialogP placeholder={t('admin:teams.form.wedge_placeholder')} /> </Grid> + <Grid item xs={12} sm={12}> + <FormSelect + name="organizationId" + className="mt-2" + label={t('admin:teams.form.organization')} + loading={loadingOrganizations} + options={organizationOptions} + placeholder={t('admin:teams.form.organization_placeholder')} + /> + </Grid> </Grid> <div className="mt-8 grid grid-cols-1 gap-4 md:flex md:justify-end md:gap-0"> diff --git a/src/components/icon/index.tsx b/src/components/icon/index.tsx index 81d8de0..86e6ad7 100644 --- a/src/components/icon/index.tsx +++ b/src/components/icon/index.tsx @@ -15,6 +15,7 @@ import Community from '@/assets/icons/community.svg?react'; import Data from '@/assets/icons/data.svg?react'; import Register from '@/assets/icons/register.svg?react'; import Export from '@/assets/icons/export.svg?react'; +import FactCheck from '@/assets/icons/fact-check.svg?react'; import { COLORS } from '@/constants'; const IconMap = { @@ -29,6 +30,7 @@ const IconMap = { Comment, Trash, ThumbsUp, + FactCheck, }; const Icon = ({ diff --git a/src/components/list/CityList.tsx b/src/components/list/CityList.tsx index 0b9bd38..c883a0b 100644 --- a/src/components/list/CityList.tsx +++ b/src/components/list/CityList.tsx @@ -22,7 +22,7 @@ const headCells: HeadCell<City>[] = [ }, { id: 'name', - label: 'name', + label: 'city', filterable: true, sortable: true, }, diff --git a/src/components/list/FilteredDataTable.tsx b/src/components/list/FilteredDataTable.tsx index afb7868..c39adfc 100644 --- a/src/components/list/FilteredDataTable.tsx +++ b/src/components/list/FilteredDataTable.tsx @@ -15,7 +15,7 @@ import { deserialize } from 'jsonapi-fractal'; import { useSnackbar } from 'notistack'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { PAGE_SIZES } from '../../constants'; +import { PAGE_SIZES, PageSizes } from '../../constants'; import { PaginationInput } from '../../schemas/entities'; import Button from '../../themed/button/Button'; import DataTable, { DataTableProps, Order } from '../../themed/table/DataTable'; @@ -32,6 +32,7 @@ interface FilteredDataTableProps<T> extends Omit<DataTableProps<T>, 'rows'> { updateControl?: number; actions?: (row: T, loading?: boolean) => JSX.Element; create?: () => JSX.Element; + pageSize?: PageSizes; } interface FilterOptionsObject { @@ -49,6 +50,7 @@ export default function FilteredDataTable<T>({ updateControl, actions, create, + pageSize = PAGE_SIZES[0], ...otherDataTableProps }: FilteredDataTableProps<T>) { const { t } = useTranslation('translation'); @@ -59,7 +61,7 @@ export default function FilteredDataTable<T>({ const [searchText, setSearchText] = useState(''); const [searchSelect, setSearchSelect] = useState(''); const [selectedOption, setSelectedOption] = useState(defaultFilter || ''); - const options = headCells.filter((cell) => cell.filterable).map((cell) => cell.id); + const options = headCells.filter((cell) => cell.filterable).map((cell) => ({ value: cell.id, label: cell.label })); const filterOptions = useMemo(() => { const filterOptionsObject: FilterOptionsObject = {}; @@ -74,7 +76,7 @@ export default function FilteredDataTable<T>({ const [payload, setPayload] = useState<PaginationInput>({ 'page[number]': 1, - 'page[size]': PAGE_SIZES[0], + 'page[size]': pageSize, sort: defaultSort, order: defaultSort ? defaultOrder : undefined, }); @@ -225,11 +227,12 @@ export default function FilteredDataTable<T>({ <MenuItem disabled value=""> {t(`table.selectAttribute`)} </MenuItem> + {options.map((option, index) => ( - <MenuItem key={`${option}-${index}`} value={option}> + <MenuItem key={`${option.value}-${index}`} value={option.value}> {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} {/* @ts-expect-error */} - {t(`columns.${option}`)} + {t(`columns.${option.label}`)} </MenuItem> ))} </Select> @@ -257,6 +260,7 @@ export default function FilteredDataTable<T>({ }} isLoading={loading} actions={actions} + pageSize={pageSize} /> </> ); diff --git a/src/components/list/OrganizationList.tsx b/src/components/list/OrganizationList.tsx index 265edd2..be422b4 100644 --- a/src/components/list/OrganizationList.tsx +++ b/src/components/list/OrganizationList.tsx @@ -11,7 +11,7 @@ const headCells: HeadCell<Organization>[] = [ }, { id: 'name', - label: 'name', + label: 'organization', filterable: true, sortable: true, }, diff --git a/src/components/list/RankViewBox.tsx b/src/components/list/RankViewBox.tsx index 02e83fe..bce38bc 100644 --- a/src/components/list/RankViewBox.tsx +++ b/src/components/list/RankViewBox.tsx @@ -62,7 +62,9 @@ const RankViewBox = () => { data[rankView].map((item) => ( <Box className="flex justify-between mb-6" key={item.first_name + item.quantity}> <Box className="flex flex-row items-center gap-3"> - <Box className="rounded-full h-10 w-10 bg-neutral-100" /> + <Box className="rounded-full h-10 w-10 bg-neutral-100 flex items-center justify-center"> + <p className="font-semibold opacity-50 text-sm">{`${item.first_name?.[0].toUpperCase()}${item.last_name?.[0].toUpperCase()}`}</p> + </Box> <Text className="font-semibold text-neutral-400 opacity-70 mb-0"> {item.first_name} {item.last_name} </Text> diff --git a/src/components/list/RoleList.tsx b/src/components/list/RoleList.tsx index c934f09..edaf5e0 100644 --- a/src/components/list/RoleList.tsx +++ b/src/components/list/RoleList.tsx @@ -18,7 +18,7 @@ const headCells: HeadCell<Role>[] = [ }, { id: 'name', - label: 'name', + label: 'rol', filterable: true, sortable: true, }, diff --git a/src/components/list/SpecialPlacesList.tsx b/src/components/list/SpecialPlacesList.tsx index 67a3949..bcebf6e 100644 --- a/src/components/list/SpecialPlacesList.tsx +++ b/src/components/list/SpecialPlacesList.tsx @@ -12,7 +12,7 @@ const headCells: HeadCell<SpecialPlace>[] = [ }, { id: 'name', - label: 'name', + label: 'specialPlace', filterable: true, sortable: true, }, diff --git a/src/components/list/TeamsList.tsx b/src/components/list/TeamsList.tsx index af3fedb..b18eda3 100644 --- a/src/components/list/TeamsList.tsx +++ b/src/components/list/TeamsList.tsx @@ -20,7 +20,7 @@ function headCells(isAdmin: boolean): HeadCell<Team>[] { }, { id: 'name', - label: 'name', + label: 'team', sortable: true, filterable: true, }, diff --git a/src/constants/index.ts b/src/constants/index.ts index ff3487d..7eb9019 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -22,7 +22,8 @@ export const ACCESS_TOKEN_LOCAL_STORAGE_KEY = 'DENGUECHAT_USER_ACCESS_TOKEN'; export const REFRESH_TOKEN_LOCAL_STORAGE_KEY = 'DENGUECHAT_USER_REFRESH_TOKEN'; export const LANG_STORAGE_KEY = 'DENGUECHAT_LANG'; -export const PAGE_SIZES = [5, 10, 25]; +export const PAGE_SIZES = [5, 10, 15, 25] as const; +export type PageSizes = (typeof PAGE_SIZES)[number]; export const DISPATCH_ACTIONS = { SET_USER: 'SET_USER', diff --git a/src/i18n/locales/en/admin.json b/src/i18n/locales/en/admin.json index 570f01a..f1f060c 100644 --- a/src/i18n/locales/en/admin.json +++ b/src/i18n/locales/en/admin.json @@ -33,23 +33,25 @@ } }, "teams": { - "create_team": "Create brigade", + "create_team": "Create Brigade", "form": { - "name": "Name", + "name": "Brigade name", "name_placeholder": "Enter a name", - "members": "Members", - "members_placeholder": "Select members", - "team_leader": "Team Leader", - "team_leader_placeholder": "Select team leader", + "members": "Brigadists", + "members_placeholder": "Select brigadists", + "team_leader": "Team Facilitator", + "team_leader_placeholder": "Select team facilitator", "organization": "Organization", "organization_placeholder": "Select organization", "sector": "Sector", "sector_placeholder": "Select sector", "wedge": "Wedge", - "wedge_placeholder": "Select wedge" + "wedge_placeholder": "Select wedge", + "city": "City", + "city_placeholder": "Select city" }, "edit": { - "edit_team": "Assign team members", + "edit_team": "Assign team brigadists", "success": "Brigade updated successfully" } } diff --git a/src/i18n/locales/en/feed.json b/src/i18n/locales/en/feed.json index e72e4b0..af9a6d6 100644 --- a/src/i18n/locales/en/feed.json +++ b/src/i18n/locales/en/feed.json @@ -1,9 +1,18 @@ { - "community": "My community's progress", + "community": { + "title": "Statistical data", + "riskChart": { + "title": "Change in Dengue Risk in Your Community", + "description": "DengueChatPlus focuses its actions on identifying and eliminating water storage containers as well as barrels and drains. A location can be counted twice if it has both positive and potential breeding sites." + }, + "postAndComments": "Posts and Comments" + }, "city": "City of", "sitesReport": { "title": "Tariki Sites", - "quantity": "Amount of green containers" + "quantity": "Amount of green containers", + "tarikiSiteInfo": "A Tariki site is one that has been a green site for at least four consecutive visits", + "greenContainersInfo": "Status of green containers, up to the end date of the last month (monthly data)" }, "layout": { "filters": "Filters", @@ -29,5 +38,10 @@ "greenSites": "Green sites", "yellowSites": "Yellow sites", "redSites": "Red sites" + }, + "reports": { + "houseResume": "Site Report", + "heatMap": "Heat Map", + "visits": "Visit Report" } } diff --git a/src/i18n/locales/en/splash.json b/src/i18n/locales/en/splash.json index db6a38b..093f871 100644 --- a/src/i18n/locales/en/splash.json +++ b/src/i18n/locales/en/splash.json @@ -1,13 +1,13 @@ { "cta": { "learnMore": "Learn more", - "whatIs": "What is DengueChat+", + "whatIs": "What is DengueChatPlus", "register": "Register", "watchMore": "Watch more videos" }, "main": { "joinTeam": "Join your community's team", - "dengueChatPlus": "DengueChat+", + "dengueChatPlus": "DengueChatPlus", "citizensCopy": "Citizens in the fight against dengue and mosquitoes." }, "participants": { @@ -15,16 +15,16 @@ "actorsInvolved": "See the actors involved in the community of Managua, Nicaragua" }, "cities": { - "citiesWithDengueChat": "Cities with DengueChat+" + "citiesWithDengueChat": "Cities with DengueChatPlus" }, "community": { - "dengueChatCommunity": "Community in DengueChat+", - "communityInvolvement": "DengueChat+ involves communities in the fight against dengue fever", + "dengueChatCommunity": "Community in DengueChatPlus", + "communityInvolvement": "DengueChatPlus involves communities in the fight against dengue fever", "chatWithMembers": "Chat with other community members to report and control mosquito breeding sites", "joinTeams": "Join teams to fight dengue and earn points" }, "data": { - "dengueChatData": "Data in DengueChat+", + "dengueChatData": "Data in DengueChatPlus", "analyzeAndMeasure": "Organize, analyze, and measure your community's progress in reducing Aedes mosquitoes, the vector of the dengue virus", "versatileTools": "Versatile tools to analyze the spread or containment of dengue vectors", "generateReports": "At the community or city level, generate reports to motivate leaders" @@ -32,5 +32,11 @@ "register": { "platformRegister": "Register on the platform", "joinCommunity": "Join your community and fight against the Aedes aegypti mosquito." + }, + "footer": { + "about": "", + "faqs": "", + "mosquito": "", + "userGuide": "" } } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 1d82928..aa34377 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -24,20 +24,28 @@ "roles": "Roles", "specialPlaces": "Special Places", "myCity": "My City", - "myCommunity": "My Community", + "myCommunity": "My Community (Sector)", "teams": "Brigades", "myProfile": "My Profile", "settings": "Settings", "organizations": "Organizations", "breedingSites": "Breeding Sites", "cities": "Cities", + "visits": "Visits", "descriptions": { "organizations": "List of Organizations", "cities": "List of Cities", "users": "List of Users", "roles": "List of Roles", "teams": "List of Brigades", - "specialPlaces": "List of Special Places" + "specialPlaces": "List of Special Places", + "visits": "List of Visits" + }, + "reports": { + "name": "Reports", + "sites": "Sites", + "heatMap": "Heat amp", + "visits": "Visits" } }, "table": { @@ -76,9 +84,16 @@ "sector": "Sector", "wedge": "Wedge", "members": "Members", - "leader": "Leader", - "memberCount": "Number of members", - "city": "City" + "leader": "Facilitator", + "memberCount": "Number of brigadists", + "city": "City", + "brigade": "Brigade", + "brigadist": "Brigadist", + "site": "Site", + "visitStatus": "Visit Status", + "visitedAt": "Visited at", + "team": "Brigade", + "house": "Site" }, "options": { "notDefined": "Not Defined", @@ -90,7 +105,7 @@ }, "footer": { "userGuide": "User Guide", - "about": "About DengueChat+", + "about": "About DengueChatPlus", "mosquito": "The Mosquito", "faqs": "Frequently Asked Questions" } diff --git a/src/i18n/locales/es/admin.json b/src/i18n/locales/es/admin.json index 3c2382c..a084ac5 100644 --- a/src/i18n/locales/es/admin.json +++ b/src/i18n/locales/es/admin.json @@ -18,7 +18,7 @@ "neighborhoods": "Sectores", "form": { "name": "Nombre", - "name_placeholder": "Ingrese un nombre", + "name_placeholder": "Ingrese un el nombre de la ciudad", "neighborhood": "Sector", "neighborhood_placeholder": "Ingrese el nombre del sector", "add_button": "Añadir" @@ -33,23 +33,25 @@ } }, "teams": { - "create_team": "Crear brigada", + "create_team": "Crear Brigada", "form": { - "name": "Nombre", + "name": "Nombre de la brigada", "name_placeholder": "Ingresa un nombre", - "members": "Miembros", - "members_placeholder": "Selecciona miembros", - "team_leader": "Líder del equipo", - "team_leader_placeholder": "Selecciona líder del equipo", + "members": "Brigadistas", + "members_placeholder": "Selecciona a los brigadistas", + "team_leader": "Facilitador del equipo", + "team_leader_placeholder": "Selecciona un facilitador del equipo", "organization": "Organización", - "organization_placeholder": "Selecciona organización", + "organization_placeholder": "Selecciona una organización", "sector": "Sector", - "sector_placeholder": "Selecciona sector", + "sector_placeholder": "Selecciona un sector", "wedge": "Cuña", - "wedge_placeholder": "Selecciona cuña" + "wedge_placeholder": "Selecciona una cuña", + "city": "Ciudad", + "city_placeholder": "Selecciona una ciudad" }, "edit": { - "edit_team": "Asignar miembros del equipo", + "edit_team": "Asignar brigadistas del equipo", "success": "Brigada actualizada con éxito" } } diff --git a/src/i18n/locales/es/auth.json b/src/i18n/locales/es/auth.json index 805eb87..c9d1abc 100644 --- a/src/i18n/locales/es/auth.json +++ b/src/i18n/locales/es/auth.json @@ -16,6 +16,7 @@ "email": "Por favor ingrese un correo electrónico válido", "password": "Por favor ingrese una contraseña válida", "login": "Algo falló. Por favor intente de nuevo.", - "empty": "Por favor llene todos los campos" + "empty": "Por favor llene todos los campos", + "generic": "" } } diff --git a/src/i18n/locales/es/feed.json b/src/i18n/locales/es/feed.json index 4bd0dbb..a17e621 100644 --- a/src/i18n/locales/es/feed.json +++ b/src/i18n/locales/es/feed.json @@ -1,9 +1,18 @@ { - "community": "El progreso de mi comunidad", + "community": { + "title": "Datos Estadísticos", + "riskChart": { + "title": "Cambio en Riesgo en Tu Comunidad de Dengue", + "description": "DengueChatPlus enfoca sus acciones en la identificación y eliminación de contenedores de almacenamiento de agua como también de barriles y sumideros. Un lugar puede ser contado doble si tiene criaderos positivos y también potenciales." + }, + "postAndComments": "Posts y Comentarios" + }, "city": "Ciudad de", "sitesReport": { "title": "Sitios Tariki", - "quantity": "Cantidad de contenedores verdes" + "quantity": "Cantidad de contenedores verdes", + "tarikiSiteInfo": "Un sitio Tariki es aquel que ha sido un sitio verde por lo menos durante cuatro visitas consecutivas", + "greenContainersInfo": "Estado de contenedores verdes, hasta la fecha del fin del último mes (dato mensual)" }, "layout": { "filters": "Filtros", @@ -16,7 +25,7 @@ "post": { "comment": "Comentarios", "likes": "Me gusta", - "delete": "Delete" + "delete": "Borrar" }, "rankView": { "ranking": "Ranking", @@ -29,5 +38,10 @@ "greenSites": "Sitios verdes", "yellowSites": "Sitios amarillos", "redSites": "Sitios rojos" + }, + "reports": { + "houseResume": "Reporte de sitios", + "heatMap": "Mapa de calor", + "visits": "Reporte de visitas" } } diff --git a/src/i18n/locales/es/splash.json b/src/i18n/locales/es/splash.json index 573c3af..5c7facc 100644 --- a/src/i18n/locales/es/splash.json +++ b/src/i18n/locales/es/splash.json @@ -1,13 +1,13 @@ { "cta": { "learnMore": "Aprende más", - "whatIs": "Qué es DengueChat+", + "whatIs": "Qué es DengueChatPlus", "register": "Registrarse", "watchMore": "Ver más videos" }, "main": { "joinTeam": "Únete al equipo de tu comunidad", - "dengueChatPlus": "DengueChat+", + "dengueChatPlus": "DengueChatPlus", "citizensCopy": "Ciudadanos en el combate contra el dengue y los zancudos." }, "participants": { @@ -15,16 +15,16 @@ "actorsInvolved": "Ve a los actores involucrados en la comunidad de Managua, Nicaragua" }, "cities": { - "citiesWithDengueChat": "Ciudades con DengueChat+" + "citiesWithDengueChat": "Ciudades con DengueChatPlus" }, "community": { - "dengueChatCommunity": "Comunidad en DengueChat+", - "communityInvolvement": "DengueChat+ involucra a las comunidades en la lucha contra el dengue", + "dengueChatCommunity": "Comunidad en DengueChatPlus", + "communityInvolvement": "DengueChatPlus involucra a las comunidades en la lucha contra el dengue", "chatWithMembers": "Chatea con otros miembros de la comunidad para reportar y controlar criaderos de mosquitos", "joinTeams": "Únete a equipos para combatir el dengue y gane puntos" }, "data": { - "dengueChatData": "Datos en DengueChat+", + "dengueChatData": "Datos en DengueChatPlus", "analyzeAndMeasure": "Organiza, analiza y mide el progreso de su comunidad en la reducción del Aedes, el mosquito transmisor de los viruses del dengue", "versatileTools": "Herramientas versátiles para analizar la expansión o el contenimiento del vector del dengue", "generateReports": "Al nivel de la comunidad o de la ciudad generan reportes para motivar a líderes" @@ -32,5 +32,11 @@ "register": { "platformRegister": "Regístrate en la plataforma", "joinCommunity": "Únete a tu comunidad y lucha contra el mosquito del Aedes aegypti." + }, + "footer": { + "about": "", + "faqs": "", + "mosquito": "", + "userGuide": "" } } diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 557f4a2..aab423a 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -23,7 +23,7 @@ "roles": "Roles", "myCity": "Mi Ciudad", "specialPlaces": "Lugares Especiales", - "myCommunity": "Mi Comunidad", + "myCommunity": "Mi Comunidad (Sector)", "teams": "Brigadas", "myProfile": "Mi Perfil", "settings": "Configuraciones", @@ -36,8 +36,17 @@ "users": "Lista de Usuarios", "roles": "Lista de Roles", "teams": "Lista de Brigadas", - "specialPlaces": "Lista de Lugares Especiales" - } + "specialPlaces": "Lista de Lugares Especiales", + "visits": "Lista de Visitas" + }, + "reports": { + "name": "Reportes", + "sites": "Sitios", + "heatMap": "Mapa de calor", + "visits": "Visitas" + }, + "visits": "Visitas", + "homes": "Sites" }, "table": { "actions": { @@ -75,9 +84,18 @@ "sector": "Sector", "wedge": "Cuña", "members": "Miembros", - "leader": "Líder", - "memberCount": "Número de miembros", - "city": "Ciudad" + "leader": "Facilitador", + "memberCount": "Número de brigadistas", + "city": "Ciudad", + "brigade": "Brigada", + "brigadist": "Brigadista", + "site": "Sitio", + "visitStatus": "Estado de Visita", + "visitedAt": "Visitado en", + "team": "Brigada", + "house": "Sitio", + "rol": "Rol", + "specialPlace": "Lugar especial" }, "options": { "notDefined": "No definido", @@ -89,7 +107,7 @@ }, "footer": { "userGuide": "Guía de Usuario", - "about": "Sobre DengueChat+", + "about": "Sobre DengueChatPlus", "mosquito": "El Zancudo", "faqs": "Preguntas Frecuentes" } diff --git a/src/i18n/locales/pt/admin.json b/src/i18n/locales/pt/admin.json index b373a96..daf6146 100644 --- a/src/i18n/locales/pt/admin.json +++ b/src/i18n/locales/pt/admin.json @@ -33,20 +33,22 @@ } }, "teams": { - "create_team": "Criar brigada", + "create_team": "Criar Brigada", "form": { - "name": "Nome", + "name": "Nome da brigada", "name_placeholder": "Insira um nome", - "members": "Membros", - "members_placeholder": "Selecione membros", - "team_leader": "Líder da equipe", - "team_leader_placeholder": "Selecione o líder da equipe", + "members": "Brigadistas", + "members_placeholder": "Selecione brigadistas", + "team_leader": "Facilitador da equipe", + "team_leader_placeholder": "Selecione o facilitador da equipe", "organization": "Organização", "organization_placeholder": "Selecione a organização", "sector": "Setor", "sector_placeholder": "Selecione o setor", "wedge": "Cunha", - "wedge_placeholder": "Selecione a cunha" + "wedge_placeholder": "Selecione a cunha", + "city": "Cidade", + "city_placeholder": "Selecione uma cidade" }, "edit": { "edit_team": "Atribuir membros da equipe", diff --git a/src/i18n/locales/pt/auth.json b/src/i18n/locales/pt/auth.json index 6905c45..60e4e5a 100644 --- a/src/i18n/locales/pt/auth.json +++ b/src/i18n/locales/pt/auth.json @@ -16,6 +16,7 @@ "email": "Por favor, insira um e-mail válido", "password": "Por favor, insira uma senha válida", "login": "Algo deu errado. Por favor, tente novamente.", - "empty": "Por favor, preencha todos os campos" + "empty": "Por favor, preencha todos os campos", + "generic": "" } } diff --git a/src/i18n/locales/pt/feed.json b/src/i18n/locales/pt/feed.json index ba7c765..fe13967 100644 --- a/src/i18n/locales/pt/feed.json +++ b/src/i18n/locales/pt/feed.json @@ -1,9 +1,18 @@ { - "community": "O progresso da minha comunidade", + "community": { + "title": "Dados estatísticos", + "riskChart": { + "title": "Mudança no Risco de Dengue na Sua Comunidade", + "description": "O DengueChatPlus concentra suas ações na identificação e eliminação de recipientes de armazenamento de água, bem como barris e ralos. Um local pode ser contado duas vezes se tiver criadouros positivos e também potenciais." + }, + "postAndComments": "Publicações e Comentários" + }, "city": "Cidade de", "sitesReport": { "title": "Locais Tariki", - "quantity": "Quantidade de contêineres verdes" + "quantity": "Quantidade de contêineres verdes", + "tarikiSiteInfo": "Um site Tariki é aquele que foi um site verde por pelo menos quatro visitas consecutivas", + "greenContainersInfo": "Estado dos contêineres verdes, até a data de término do último mês (dados mensais)" }, "layout": { "filters": "Filtros", @@ -29,5 +38,10 @@ "greenSites": "Locais verdes", "yellowSites": "Locais amarelos", "redSites": "Locais vermelhos" + }, + "reports": { + "houseResume": "Relatório de locais", + "heatMap": "Mapa de calor", + "visits": "Relatório de visitas" } } diff --git a/src/i18n/locales/pt/splash.json b/src/i18n/locales/pt/splash.json index 595b461..2b040fb 100644 --- a/src/i18n/locales/pt/splash.json +++ b/src/i18n/locales/pt/splash.json @@ -1,13 +1,13 @@ { "cta": { "learnMore": "Saber mais", - "whatIs": "O que é DengueChat+", + "whatIs": "O que é DengueChatPlus", "register": "Cadastar", "watchMore": "Ver mais vídeos" }, "main": { "joinTeam": "Junte-se à equipa da sua comunidade", - "dengueChatPlus": "DengueChat+", + "dengueChatPlus": "DengueChatPlus", "citizensCopy": "Cidadãos na luta contra a dengue e os mosquitos." }, "participants": { @@ -15,16 +15,16 @@ "actorsInvolved": "Veja os atores envolvidos na comunidade de Manágua, Nicarágua" }, "cities": { - "citiesWithDengueChat": "Cidades com DengueChat+" + "citiesWithDengueChat": "Cidades com DengueChatPlus" }, "community": { - "dengueChatCommunity": "Comunidade no DengueChat+", - "communityInvolvement": "DengueChat+ envolve as comunidades na luta contra a febre da dengue", + "dengueChatCommunity": "Comunidade no DengueChatPlus", + "communityInvolvement": "DengueChatPlus envolve as comunidades na luta contra a febre da dengue", "chatWithMembers": "Converse com outros membros da comunidade para reportar e controlar criadouros de mosquitos", "joinTeams": "Junte-se a equipes para combater dengue" }, "data": { - "dengueChatData": "Dados no DengueChat+", + "dengueChatData": "Dados no DengueChatPlus", "analyzeAndMeasure": "Organize, analise e meça o progresso da sua comunidade na redução do Aedes, o mosquito transmissor dos vírus do dengue", "versatileTools": "Ferramentas versáteis para analisar a expansão ou contenção do vetor do dengue", "generateReports": "Ao nível da comunidade ou da cidade, gere relatórios para motivar líderes" @@ -35,7 +35,7 @@ }, "footer": { "userGuide": "Guia do Utilizador", - "about": "Sobre o DengueChat+", + "about": "Sobre o DengueChatPlus", "mosquito": "O Mosquito", "faqs": "Perguntas Frequentes" } diff --git a/src/i18n/locales/pt/translation.json b/src/i18n/locales/pt/translation.json index fa5773f..b457538 100644 --- a/src/i18n/locales/pt/translation.json +++ b/src/i18n/locales/pt/translation.json @@ -23,7 +23,7 @@ "roles": "Funções", "myCity": "Minha Cidade", "specialPlaces": "Lugares Especiais", - "myCommunity": "Minha Comunidade", + "myCommunity": "Minha Comunidade (Setor)", "teams": "Brigadas", "myProfile": "Meu Perfil", "settings": "Configurações", @@ -36,8 +36,17 @@ "users": "Lista de Utilizadores", "roles": "Lista de Funções", "teams": "Lista de Brigadas", - "specialPlaces": "Lista de Lugares Especiais" - } + "specialPlaces": "Lista de Lugares Especiais", + "visits": "Visitas" + }, + "reports": { + "name": "Relatórios", + "sites": "Locais", + "heatMap": "Mapa de Calor", + "visits": "Visitas" + }, + "visits": "Visitas", + "homes": "Locals" }, "table": { "actions": { @@ -74,10 +83,17 @@ "organization": "Organização", "sector": "Setor", "wedge": "Subsetor", - "members": "Membros", - "leader": "Líder", - "memberCount": "Número de membros", - "city": "Cidade" + "members": "Brigadistas", + "leader": "Facilitador", + "memberCount": "Número de brigadistas", + "city": "Cidade", + "brigade": "Brigada", + "brigadist": "Brigadista", + "site": "Local", + "visitStatus": "Status da Visita", + "visitedAt": "Visitado em", + "team": "Brigada", + "house": "Local" }, "options": { "notDefined": "Não definido", @@ -89,7 +105,7 @@ }, "footer": { "userGuide": "Guia do Usuário", - "about": "Sobre o DengueChat+", + "about": "Sobre o DengueChatPlus", "mosquito": "O Mosquito", "faqs": "Perguntas Frequentes" } diff --git a/src/layout/AppBar.tsx b/src/layout/AppBar.tsx index 4226458..342b479 100644 --- a/src/layout/AppBar.tsx +++ b/src/layout/AppBar.tsx @@ -3,7 +3,6 @@ import { Collapse, Container, Drawer, - IconButton, List, ListItemButton, ListItemIcon, @@ -17,8 +16,6 @@ import { useTranslation } from 'react-i18next'; import { Link, useLocation } from 'react-router-dom'; -import MenuIcon from '@mui/icons-material/Menu'; - import React, { useState } from 'react'; import ExpandLess from '@mui/icons-material/ExpandLess'; @@ -28,12 +25,13 @@ import { CITIES_INDEX, ORGANIZATIONS_INDEX, ROLES_INDEX, - USERS_INDEX, SPECIAL_PLACES_INDEX, TEAMS_INDEX, + USERS_INDEX, } from '@/constants/permissions'; import BugIcon from '../assets/icons/bug.svg'; import SettingsIcon from '../assets/icons/settings.svg'; +import ReportsIcon from '../assets/icons/reports.svg'; import TeamsIcon from '../assets/icons/teams.svg'; import Logo from '../assets/images/logo.svg'; import SelectLanguageComponent from '../components/SelectLanguageComponent'; @@ -52,6 +50,9 @@ export interface AppBarProps { // routes const ADMIN_USERS = '/admin/users'; +const ADMIN_SITES = '/reports/sites'; +const ADMIN_HEATMAP = '/reports/heat-map'; +const ADMIN_VISITS = '/reports/visits'; const ADMIN_ROLES = '/admin/roles'; const ADMIN_ORGANIZATIONS = '/admin/organizations'; const ADMIN_CITIES = '/admin/cities'; @@ -60,6 +61,7 @@ const ADMIN_TEAMS = '/admin/teams'; const MY_CITY = '/my-city'; const MY_COMMUNITY = '/my-community'; +const VISITS = '/visits'; export function AppBar({ auth = false, signUp = false, logout }: AppBarProps) { const { t } = useTranslation('translation'); @@ -68,7 +70,7 @@ export function AppBar({ auth = false, signUp = false, logout }: AppBarProps) { const matches = useMediaQuery('(min-width:600px)'); const [mobileOpen, setMobileOpen] = React.useState(false); - const [isClosing, setIsClosing] = React.useState(false); + const [, setIsClosing] = React.useState(false); const handleDrawerClose = () => { setIsClosing(true); @@ -79,11 +81,11 @@ export function AppBar({ auth = false, signUp = false, logout }: AppBarProps) { setIsClosing(false); }; - const handleDrawerToggle = () => { - if (!isClosing) { - setMobileOpen(!mobileOpen); - } - }; + // const handleDrawerToggle = () => { + // if (!isClosing) { + // setMobileOpen(!mobileOpen); + // } + // }; const [searchText, setSearchText] = useState(''); @@ -91,136 +93,223 @@ export function AppBar({ auth = false, signUp = false, logout }: AppBarProps) { setSearchText(event.target.value); }; - const [open, setOpen] = React.useState(true); + const [openMenus, setOpenMenus] = React.useState<{ [key: string]: boolean }>({ + settingsMenu: false, + reportsMenu: false, + }); - const handleClick = () => { - setOpen(!open); + const genericHandleClick = (menuId: string) => { + setOpenMenus((prev) => { + const isOpen = prev[menuId]; + return { + ...prev, + [menuId]: !isOpen, + }; + }); }; const drawer = ( - <Box sx={{ paddingLeft: { xs: 2, sm: 0 } }}> - <Toolbar className="ml-4" disableGutters sx={{ height: '80px' }}> - <div className="flex flex-1 flex-col align-middle justify-center"> - <Link to="/" style={{ textDecoration: 'none', color: '#fff' }}> - <img className={!matches ? 'right-4' : ''} src={Logo} alt="logo" /> - </Link> - </div> - </Toolbar> + <Box sx={{ paddingLeft: { xs: 2, sm: 0 } }} className="flex flex-col justify-between min-h-full"> + <Box> + <Toolbar className="ml-4" disableGutters sx={{ height: '80px' }}> + <div className="flex flex-1 flex-col align-middle justify-center"> + <Link to="/" style={{ textDecoration: 'none', color: '#fff' }}> + <img className={!matches ? 'right-4' : ''} src={Logo} alt="logo" /> + </Link> + </div> + </Toolbar> - <List - aria-labelledby="nested-list-subheader" - subheader={ - <Box className="w-full px-4 py-2"> - <TextField - size="small" - label={t(`menu.search`)} - variant="outlined" - value={searchText} - onChange={handleTextChange} - /> - </Box> - } - > - <ListItemButton component={Link} to={MY_CITY} selected={pathname.includes(MY_CITY)}> - <ListItemIcon> - <Icon type="City" /> - </ListItemIcon> - <ListItemText primary={<Text type="menuItem">{t('menu.myCity')}</Text>} /> - </ListItemButton> - <ListItemButton component={Link} to={MY_COMMUNITY} selected={pathname.includes(MY_COMMUNITY)}> - <ListItemIcon> - <Icon type="Community" /> - </ListItemIcon> - <ListItemText primary={<Text type="menuItem">{t('menu.myCommunity')}</Text>} /> - </ListItemButton> - <ListItemButton> - <ListItemIcon> - <img src={TeamsIcon} alt="teams-icon" /> - </ListItemIcon> - <ListItemText primary={<Text type="menuItem">{t('menu.teams')}</Text>} /> - </ListItemButton> - <ListItemButton> - <ListItemIcon> - <img src={BugIcon} alt="breedingSites-icon" /> - </ListItemIcon> - <ListItemText primary={<Text type="menuItem">{t('menu.breedingSites')}</Text>} /> - </ListItemButton> - <ProtectedView - hasSomePermission={[ROLES_INDEX, ORGANIZATIONS_INDEX, USERS_INDEX, CITIES_INDEX, SPECIAL_PLACES_INDEX]} + <List + aria-labelledby="nested-list-subheader" + subheader={ + <Box className="min-w-full px-4 py-2 mb-4"> + <TextField + size="small" + label={t(`menu.search`)} + variant="outlined" + value={searchText} + onChange={handleTextChange} + className="min-w-full" + /> + </Box> + } > - <ListItemButton onClick={handleClick}> + <ListItemButton component={Link} to={MY_CITY} selected={pathname.includes(MY_CITY)}> <ListItemIcon> - <img src={SettingsIcon} alt="settings-icon" /> + <Icon type="City" /> </ListItemIcon> - <ListItemText primary={<Text type="menuItem">{t('menu.settings')}</Text>} /> - {open ? <ExpandLess /> : <ExpandMore />} + <ListItemText primary={<Text type="menuItem">{t('menu.myCity')}</Text>} /> </ListItemButton> - <Collapse in={open} timeout="auto" unmountOnExit> - <List component="div" disablePadding> - <ProtectedView hasPermission={[USERS_INDEX]}> - <ListItemButton - sx={{ pl: 4 }} - component={Link} - to={ADMIN_USERS} - selected={pathname.includes(ADMIN_USERS)} - > - <ListItemText primary={<Text type="menuItem">{t('menu.users')}</Text>} /> - </ListItemButton> - </ProtectedView> - <ProtectedView hasPermission={[ROLES_INDEX]}> - <ListItemButton - sx={{ pl: 4 }} - component={Link} - to={ADMIN_ROLES} - selected={pathname.includes(ADMIN_ROLES)} - > - <ListItemText primary={<Text type="menuItem">{t('menu.roles')}</Text>} /> - </ListItemButton> - </ProtectedView> - <ProtectedView hasPermission={[ORGANIZATIONS_INDEX]}> - <ListItemButton - sx={{ pl: 4 }} - component={Link} - to={ADMIN_ORGANIZATIONS} - selected={pathname.includes(ADMIN_ORGANIZATIONS)} - > - <ListItemText primary={<Text type="menuItem">{t('menu.organizations')}</Text>} /> - </ListItemButton> - </ProtectedView> - <ProtectedView hasPermission={[CITIES_INDEX]}> - <ListItemButton - sx={{ pl: 4 }} - component={Link} - to={ADMIN_CITIES} - selected={pathname.includes(ADMIN_CITIES)} - > - <ListItemText primary={<Text type="menuItem">{t('menu.cities')}</Text>} /> - </ListItemButton> - </ProtectedView> - <ProtectedView hasPermission={[SPECIAL_PLACES_INDEX]}> - <ListItemButton - sx={{ pl: 4 }} - component={Link} - to={ADMIN_SPECIAL_PLACES} - selected={pathname.includes(ADMIN_SPECIAL_PLACES)} - > - <ListItemText primary={<Text type="menuItem">{t('menu.specialPlaces')}</Text>} /> - </ListItemButton> - </ProtectedView> - <ProtectedView hasPermission={[TEAMS_INDEX]}> - <ListItemButton - sx={{ pl: 4 }} - component={Link} - to={ADMIN_TEAMS} - selected={pathname.includes(ADMIN_TEAMS)} - > - <ListItemText primary={<Text type="menuItem">{t('menu.teams')}</Text>} /> - </ListItemButton> - </ProtectedView> - </List> - </Collapse> - </ProtectedView> - </List> + <ListItemButton component={Link} to={MY_COMMUNITY} selected={pathname.includes(MY_COMMUNITY)}> + <ListItemIcon> + <Icon type="Community" /> + </ListItemIcon> + <ListItemText primary={<Text type="menuItem">{t('menu.myCommunity')}</Text>} /> + </ListItemButton> + <ListItemButton> + <ListItemIcon> + <img src={TeamsIcon} alt="teams-icon" /> + </ListItemIcon> + <ListItemText primary={<Text type="menuItem">{t('menu.teams')}</Text>} /> + </ListItemButton> + <ListItemButton> + <ListItemIcon> + <img src={BugIcon} alt="breedingSites-icon" /> + </ListItemIcon> + <ListItemText primary={<Text type="menuItem">{t('menu.breedingSites')}</Text>} /> + </ListItemButton> + <ListItemButton component={Link} to={VISITS} selected={pathname.includes(VISITS)}> + <ListItemIcon> + <Icon type="FactCheck" /> + </ListItemIcon> + <ListItemText primary={<Text type="menuItem">{t('menu.visits')}</Text>} /> + </ListItemButton> + <ProtectedView + hasSomePermission={[ROLES_INDEX, ORGANIZATIONS_INDEX, USERS_INDEX, CITIES_INDEX, SPECIAL_PLACES_INDEX]} + > + <ListItemButton onClick={() => genericHandleClick('reportsMenu')}> + <ListItemIcon> + <img src={ReportsIcon} alt="reports-icon" /> + </ListItemIcon> + <ListItemText primary={<Text type="menuItem">{t('menu.reports.name')}</Text>} /> + {openMenus.reportsMenu ? <ExpandLess /> : <ExpandMore />} + </ListItemButton> + <Collapse in={openMenus.reportsMenu} timeout="auto" unmountOnExit> + <List component="div" disablePadding> + <ProtectedView hasPermission={[USERS_INDEX]}> + <ListItemButton + sx={{ pl: 4 }} + component={Link} + to={ADMIN_SITES} + selected={pathname.includes(ADMIN_SITES)} + > + <ListItemText primary={<Text type="menuItem">{t('menu.reports.sites')}</Text>} /> + </ListItemButton> + </ProtectedView> + <ProtectedView hasPermission={[USERS_INDEX]}> + <ListItemButton + sx={{ pl: 4 }} + component={Link} + to={ADMIN_HEATMAP} + selected={pathname.includes(ADMIN_HEATMAP)} + > + <ListItemText primary={<Text type="menuItem">{t('menu.reports.heatMap')}</Text>} /> + </ListItemButton> + </ProtectedView> + <ProtectedView hasPermission={[USERS_INDEX]}> + <ListItemButton + sx={{ pl: 4 }} + component={Link} + to={ADMIN_VISITS} + selected={pathname.includes(ADMIN_VISITS)} + > + <ListItemText primary={<Text type="menuItem">{t('menu.reports.visits')}</Text>} /> + </ListItemButton> + </ProtectedView> + </List> + </Collapse> + </ProtectedView> + <ProtectedView + hasSomePermission={[ROLES_INDEX, ORGANIZATIONS_INDEX, USERS_INDEX, CITIES_INDEX, SPECIAL_PLACES_INDEX]} + > + <ListItemButton onClick={() => genericHandleClick('settingsMenu')}> + <ListItemIcon> + <img src={SettingsIcon} alt="settings-icon" /> + </ListItemIcon> + <ListItemText primary={<Text type="menuItem">{t('menu.settings')}</Text>} /> + {openMenus.settingsMenu ? <ExpandLess /> : <ExpandMore />} + </ListItemButton> + <Collapse in={openMenus.settingsMenu} timeout="auto" unmountOnExit> + <List component="div" disablePadding> + <ProtectedView hasPermission={[USERS_INDEX]}> + <ListItemButton + sx={{ pl: 4 }} + component={Link} + to={ADMIN_USERS} + selected={pathname.includes(ADMIN_USERS)} + > + <ListItemText primary={<Text type="menuItem">{t('menu.users')}</Text>} /> + </ListItemButton> + </ProtectedView> + <ProtectedView hasPermission={[ROLES_INDEX]}> + <ListItemButton + sx={{ pl: 4 }} + component={Link} + to={ADMIN_ROLES} + selected={pathname.includes(ADMIN_ROLES)} + > + <ListItemText primary={<Text type="menuItem">{t('menu.roles')}</Text>} /> + </ListItemButton> + </ProtectedView> + <ProtectedView hasPermission={[ORGANIZATIONS_INDEX]}> + <ListItemButton + sx={{ pl: 4 }} + component={Link} + to={ADMIN_ORGANIZATIONS} + selected={pathname.includes(ADMIN_ORGANIZATIONS)} + > + <ListItemText primary={<Text type="menuItem">{t('menu.organizations')}</Text>} /> + </ListItemButton> + </ProtectedView> + <ProtectedView hasPermission={[CITIES_INDEX]}> + <ListItemButton + sx={{ pl: 4 }} + component={Link} + to={ADMIN_CITIES} + selected={pathname.includes(ADMIN_CITIES)} + > + <ListItemText primary={<Text type="menuItem">{t('menu.cities')}</Text>} /> + </ListItemButton> + </ProtectedView> + <ProtectedView hasPermission={[SPECIAL_PLACES_INDEX]}> + <ListItemButton + sx={{ pl: 4 }} + component={Link} + to={ADMIN_SPECIAL_PLACES} + selected={pathname.includes(ADMIN_SPECIAL_PLACES)} + > + <ListItemText primary={<Text type="menuItem">{t('menu.specialPlaces')}</Text>} /> + </ListItemButton> + </ProtectedView> + <ProtectedView hasPermission={[TEAMS_INDEX]}> + <ListItemButton + sx={{ pl: 4 }} + component={Link} + to={ADMIN_TEAMS} + selected={pathname.includes(ADMIN_TEAMS)} + > + <ListItemText primary={<Text type="menuItem">{t('menu.teams')}</Text>} /> + </ListItemButton> + </ProtectedView> + </List> + </Collapse> + </ProtectedView> + </List> + </Box> + <Box> + <Box className="px-4 min-w-full"> + <SelectLanguageComponent className="min-w-full" /> + </Box> + + <Box className="px-4 mb-4 mt-4 min-w-full"> + {auth && !!logout && ( + <Button className="mr-4 min-w-full" buttonType="small" label={t('logout')} onClick={() => logout()} /> + )} + {!auth && location.pathname !== '/login' && ( + <Button + primary={false} + className="mr-4" + buttonType="large" + label={t('login')} + component={Link} + to="/login" + /> + )} + {signUp && !auth && ( + <Button className="mr-4" buttonType="small" label={t('register')} component={Link} to="/register" /> + )} + </Box> + </Box> </Box> ); @@ -262,80 +351,33 @@ export function AppBar({ auth = false, signUp = false, logout }: AppBarProps) { } return ( - <> - <MuiAppBar - // position="static" - className="bg-white border-solid border-b border-neutral mb-4" - elevation={0} - position="fixed" + <Box component="nav" sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }} aria-label="drawer menu"> + <Drawer + variant="temporary" + open={mobileOpen} + onTransitionEnd={handleDrawerTransitionEnd} + onClose={handleDrawerClose} + ModalProps={{ + keepMounted: true, // Better open performance on mobile. + }} sx={{ - width: { sm: `calc(100% - ${drawerWidth}px)` }, - ml: { sm: `${drawerWidth}px` }, + display: { xs: 'block', sm: 'none' }, + '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }, }} > - <Container maxWidth={false} className="lg:px-20 md:px-12 sm:px-10 px-6 mx-0"> - <Toolbar disableGutters sx={{ height: '80px' }}> - <IconButton - color="inherit" - aria-label="open drawer" - edge="start" - onClick={handleDrawerToggle} - sx={{ mr: 2, display: { sm: 'none' } }} - > - <MenuIcon color="primary" /> - </IconButton> - - <Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}> - {auth && !!logout && ( - <Button className="mr-4" buttonType="small" label={t('logout')} onClick={() => logout()} /> - )} - {!auth && location.pathname !== '/login' && ( - <Button - primary={false} - className="mr-4" - buttonType="small" - label={t('login')} - component={Link} - to="/login" - /> - )} - {signUp && !auth && ( - <Button className="mr-4" buttonType="small" label={t('register')} component={Link} to="/register" /> - )} - - <SelectLanguageComponent /> - </Box> - </Toolbar> - </Container> - </MuiAppBar> - <Box component="nav" sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }} aria-label="drawer menu"> - <Drawer - variant="temporary" - open={mobileOpen} - onTransitionEnd={handleDrawerTransitionEnd} - onClose={handleDrawerClose} - ModalProps={{ - keepMounted: true, // Better open performance on mobile. - }} - sx={{ - display: { xs: 'block', sm: 'none' }, - '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }, - }} - > - {drawer} - </Drawer> - <Drawer - variant="permanent" - sx={{ - display: { xs: 'none', sm: 'block' }, - '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }, - }} - open - > - {drawer} - </Drawer> - </Box> - </> + {drawer} + </Drawer> + <Drawer + variant="permanent" + sx={{ + display: { xs: 'none', sm: 'block' }, + '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }, + }} + open + > + {drawer} + </Drawer> + </Box> ); } diff --git a/src/layout/BaseLayout.tsx b/src/layout/BaseLayout.tsx index 988405f..5d2afa6 100644 --- a/src/layout/BaseLayout.tsx +++ b/src/layout/BaseLayout.tsx @@ -1,4 +1,4 @@ -import { Box, Container, Toolbar } from '@mui/material'; +import { Box, Container } from '@mui/material'; import { PropsWithChildren } from 'react'; import { ScrollToTop } from '../components/ScrollToTop'; @@ -29,7 +29,6 @@ export default function BaseLayout({ children, auth, signUp, logout, footer }: A <ScrollToTop /> <AppBar auth={auth} signUp={signUp} logout={logout} /> <Box component="main" sx={{ flexGrow: 1, p: 3, width: { sm: `calc(100% - ${drawerWidth}px)` } }}> - <Toolbar disableGutters /> {children} </Box> </Box> diff --git a/src/main.tsx b/src/main.tsx index 1568c62..dc3194a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,6 @@ import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { Provider } from '@rollbar/react'; import { StrictMode } from 'react'; import ReactDOM from 'react-dom/client'; @@ -17,10 +18,27 @@ dayjs.locale(import.meta.env.VITE_DEFAULT_LANG ?? 'es'); const rootElement = document.getElementById('root-app') as HTMLElement; const root = ReactDOM.createRoot(rootElement); +const rollbarConfig = { + accessToken: import.meta.env.ROLLBAR_ACCESS_TOKEN, + captureUncaught: true, + captureUnhandledRejections: true, + payload: { + client: { + javascript: { + code_version: '1.0.0', + source_map_enabled: true, + }, + }, + }, +}; + root.render( <StrictMode> - <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={import.meta.env.VITE_DEFAULT_LANG ?? 'es'}> - <AppRouter /> - </LocalizationProvider> + <Provider config={rollbarConfig}> + <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={import.meta.env.VITE_DEFAULT_LANG ?? 'es'}> + <AppRouter /> + </LocalizationProvider> + </Provider> + , </StrictMode>, ); diff --git a/src/pages/my-city/MyCityPage.tsx b/src/pages/my-city/MyCityPage.tsx index c4de288..ace9394 100644 --- a/src/pages/my-city/MyCityPage.tsx +++ b/src/pages/my-city/MyCityPage.tsx @@ -1,6 +1,6 @@ import { Box, Divider, FormControl, InputLabel, MenuItem, Select, SelectChangeEvent } from '@mui/material'; import { deserialize } from 'jsonapi-fractal'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import InfiniteScroll from 'react-infinite-scroll-component'; import { authApi } from '@/api/axios'; @@ -27,73 +27,75 @@ const MyCity = () => { const [sortFilter, setSortFilter] = useState<Sort>('desc'); const [loading, setLoading] = useState(false); - const fetchData = async (page: number, teamId?: number | string, sort?: Sort) => { - setError(''); - try { - const response = await authApi.get('posts', { - headers: { - source: 'mobile', - }, - params: { - 'filter[team_id]': teamId, - 'page[number]': page, - 'page[size]': 6, - sort: 'created_at', - order: sort, - }, - }); + const fetchData = useCallback( + async (page: number, teamId?: number | string, sort?: Sort) => { + setError(''); + try { + const response = await authApi.get('posts', { + headers: { + source: 'mobile', + }, + params: { + 'filter[team_id]': teamId, + 'page[number]': page, + 'page[size]': 6, + sort: 'created_at', + order: sort, + }, + }); - const data = response?.data; - if (data) { - const deserializedData = deserialize<Post>(data); - if (!deserializedData || !Array.isArray(deserializedData)) return; + const data = response?.data; + if (data) { + const deserializedData = deserialize<Post>(data); + if (!deserializedData || !Array.isArray(deserializedData)) return; - if (page === 1) { - const uniqueList = Array.from(new Set(deserializedData.map((item) => item.id))) - .map((id) => deserializedData.find((item) => item.id === id)) - .filter((item) => item !== undefined); + if (page === 1) { + const uniqueList = Array.from(new Set(deserializedData.map((item) => item.id))) + .map((id) => deserializedData.find((item) => item.id === id)) + .filter((item) => item !== undefined); - setDataList(uniqueList); - } else { - setDataList((prevData) => { - const updatedList = [...prevData, ...deserializedData]; + setDataList(uniqueList); + } else { + setDataList((prevData) => { + const updatedList = [...prevData, ...deserializedData]; - const uniqueList = Array.from(new Set(updatedList.map((item) => item.id))) - .map((id) => updatedList.find((item) => item.id === id)) - .filter((item) => item !== undefined); + const uniqueList = Array.from(new Set(updatedList.map((item) => item.id))) + .map((id) => updatedList.find((item) => item.id === id)) + .filter((item) => item !== undefined); - return uniqueList; - }); - } + return uniqueList; + }); + } - // console.log("data.links>>>", data.links); - setHasMore(data.links?.self !== data.links?.last); + // console.log("data.links>>>", data.links); + setHasMore(data.links?.self !== data.links?.last); + } + } catch (err) { + console.log('error>>>>>>', err); + setError(t('errorCodes:generic')); + } finally { + setLoadingMore(false); } - } catch (err) { - console.log('error>>>>>>', err); - setError(t('errorCodes:generic')); - } finally { - setLoadingMore(false); - } - }; + }, + [t], + ); const loadMoreData = () => { if (!loadingMore && hasMore && !error) { setLoadingMore(true); const nextPage = currentPage + 1; - console.log('loadMoreData>>>> ', nextPage); setCurrentPage(nextPage); - fetchData(nextPage, all ? undefined : (user?.team as Team)?.id); + fetchData(nextPage, all ? undefined : (user?.team as Team)?.id, sortFilter); } }; - const firstLoad = () => { + const firstLoad = useCallback(() => { // setAll(true); setDataList([]); setHasMore(true); setCurrentPage(1); - fetchData(1, undefined); - }; + fetchData(1, undefined, sortFilter); + }, [sortFilter, fetchData]); useEffect(() => { firstLoad(); @@ -104,7 +106,10 @@ const MyCity = () => { const selectedSort = e?.target.value as Sort; setSortFilter(selectedSort); setLoading(true); - await fetchData(currentPage, undefined, selectedSort); + setHasMore(true); + setDataList([]); + setCurrentPage(1); + await fetchData(1, undefined, selectedSort); setLoading(false); }; @@ -123,7 +128,7 @@ const MyCity = () => { labelId="label-attribute-search" value={sortFilter} onChange={handleSelectOptionChange} - label="Search" + label={t('layout.filter.order')} > <MenuItem value="desc">{t('layout.filter.latest')}</MenuItem> <MenuItem value="asc">{t('layout.filter.oldest')}</MenuItem> diff --git a/src/pages/my-community/MyCommunityPage.tsx b/src/pages/my-community/MyCommunityPage.tsx index a91bce5..b0c951a 100644 --- a/src/pages/my-community/MyCommunityPage.tsx +++ b/src/pages/my-community/MyCommunityPage.tsx @@ -10,20 +10,22 @@ import useUser from '@/hooks/useUser'; import { Post, Team } from '@/schemas/entities'; import Loader from '@/themed/loader/Loader'; import Title from '@/themed/title/Title'; +import Text from '@/themed/text/Text'; type Sort = 'asc' | 'desc'; const LookerStudioEmbed = () => { return ( - <iframe - title="Looker Studio Report" - width="100%" - height="1000" - src="https://lookerstudio.google.com/embed/reporting/52ffa4b7-c386-4c77-b5c8-ad765c9f15cc/page/2hjKE" - scrolling="no" - className="border-0 p-0 h-min-max overscroll-none overflow-hidden" - sandbox="allow-storage-access-by-user-activation allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" - /> + <div> + <iframe + title="Looker Studio Report" + width="100%" + height="700" + src="https://lookerstudio.google.com/embed/reporting/51c9e940-d10f-458f-8cbb-8a40804404a1/page/P6GUE" + className="border-0 p-0 h-min-max overscroll-none overflow-hidden" + sandbox="allow-storage-access-by-user-activation allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" + /> + </div> ); }; @@ -112,12 +114,16 @@ const MyCommunity = () => { return ( <> - <Title type="page2" label={t('community')} /> + <Title type="page2" label={t('community.title')} /> <Divider /> <Box className="flex pt-6 gap-6"> <Box className="bg-gray-300 h-full w-full"> <SitesReport /> + <Title label={t('community.riskChart.title')} /> + <Text>{t('community.riskChart.description')}</Text> <LookerStudioEmbed /> + <Divider className="mt-12 mb-8" /> + <Title label={t('community.postAndComments')} /> <InfiniteScroll loader={<Loader />} hasMore={hasMore} diff --git a/src/pages/reports/HeatMapPage.tsx b/src/pages/reports/HeatMapPage.tsx new file mode 100644 index 0000000..9241bb9 --- /dev/null +++ b/src/pages/reports/HeatMapPage.tsx @@ -0,0 +1,39 @@ +import { Box, Divider } from '@mui/material'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import Title from '@/themed/title/Title'; + +const LookerStudioEmbed = () => { + return ( + <div className="w-full flex"> + <iframe + title="Looker Studio Report" + width="85%" + height="1180" + src="https://lookerstudio.google.com/embed/reporting/afe08079-b56d-4a3c-bc34-810b41a0b901/page/FcHUE" + className="border-0 p-0 h-min-max overscroll-none overflow-hidden" + sandbox="allow-storage-access-by-user-activation allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" + /> + </div> + ); +}; + +const HeatMap = () => { + const { t } = useTranslation(['feed', 'errorCodes']); + + useEffect(() => {}, []); + + return ( + <> + <Title type="page2" label={t('reports.heatMap')} /> + <Divider /> + <Box className="flex gap-6"> + <Box className="bg-gray-300 h-full w-full"> + <LookerStudioEmbed /> + </Box> + </Box> + </> + ); +}; + +export default HeatMap; diff --git a/src/pages/reports/SitesPage.tsx b/src/pages/reports/SitesPage.tsx new file mode 100644 index 0000000..16b8de2 --- /dev/null +++ b/src/pages/reports/SitesPage.tsx @@ -0,0 +1,39 @@ +import { Box, Divider } from '@mui/material'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import Title from '@/themed/title/Title'; + +const LookerStudioEmbed = () => { + return ( + <div className="w-full flex"> + <iframe + title="Looker Studio Report" + width="85%" + height="1180" + src="https://lookerstudio.google.com/embed/reporting/ca92353e-36a4-425e-821d-bdda3f450d4d/page/hhHUE" + className="border-0 p-0 h-min-max overscroll-none overflow-hidden" + sandbox="allow-storage-access-by-user-activation allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" + /> + </div> + ); +}; + +const Site = () => { + const { t } = useTranslation(['feed', 'errorCodes']); + + useEffect(() => {}, []); + + return ( + <> + <Title type="page2" label={t('reports.houseResume')} /> + <Divider /> + <Box className="flex gap-6"> + <Box className="bg-gray-300 h-full w-full"> + <LookerStudioEmbed /> + </Box> + </Box> + </> + ); +}; + +export default Site; diff --git a/src/pages/reports/VisitPage.tsx b/src/pages/reports/VisitPage.tsx new file mode 100644 index 0000000..7632f9b --- /dev/null +++ b/src/pages/reports/VisitPage.tsx @@ -0,0 +1,39 @@ +import { Box, Divider } from '@mui/material'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import Title from '@/themed/title/Title'; + +const LookerStudioEmbed = () => { + return ( + <div className="w-full flex"> + <iframe + title="Looker Studio Report" + width="85%" + height="2050" + src="https://lookerstudio.google.com/embed/reporting/14f24d0d-8542-405a-b6e4-9f758d73ff3f/page/NqHUE" + className="border-0 p-0 h-min-max overscroll-none overflow-hidden" + sandbox="allow-storage-access-by-user-activation allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" + /> + </div> + ); +}; + +const Visit = () => { + const { t } = useTranslation(['feed', 'errorCodes']); + + useEffect(() => {}, []); + + return ( + <> + <Title type="page2" label={t('reports.visits')} /> + <Divider /> + <Box className="flex gap-6"> + <Box className="bg-gray-300 h-full w-full"> + <LookerStudioEmbed /> + </Box> + </Box> + </> + ); +}; + +export default Visit; diff --git a/src/pages/visits/VisitsPage.tsx b/src/pages/visits/VisitsPage.tsx new file mode 100644 index 0000000..739aab4 --- /dev/null +++ b/src/pages/visits/VisitsPage.tsx @@ -0,0 +1,71 @@ +import { useTranslation } from 'react-i18next'; +import FilteredDataTable from '../../components/list/FilteredDataTable'; +import { Visit } from '../../schemas/entities'; +import { HeadCell } from '../../themed/table/DataTable'; + +const headCells: HeadCell<Visit>[] = [ + { + id: 'id', + label: 'id', + sortable: true, + }, + { + id: 'visitedAt', + label: 'visitedAt', + type: 'date', + sortable: true, + }, + { + id: 'city', + label: 'city', + filterable: true, + sortable: false, + }, + { + id: 'wedge', + label: 'wedge', + filterable: true, + sortable: true, + }, + { + id: 'team', + label: 'brigade', + filterable: true, + sortable: true, + }, + { + id: 'brigadist', + label: 'brigadist', + filterable: true, + sortable: true, + }, + { + id: 'house', + label: 'site', + filterable: true, + sortable: true, + }, + { + id: 'visitStatus', + label: 'visitStatus', + filterable: true, + sortable: true, + }, +]; + +const VisitDataTable = FilteredDataTable<Visit>; + +export default function VisitsList() { + const { t } = useTranslation('translation'); + + return ( + <VisitDataTable + endpoint="visits" + defaultFilter="brigadist" + headCells={headCells} + title={t('menu.visits')} + subtitle={t('menu.descriptions.visits')} + pageSize={15} + /> + ); +} diff --git a/src/routes/AppRouter.tsx b/src/routes/AppRouter.tsx index bfc4594..5993078 100644 --- a/src/routes/AppRouter.tsx +++ b/src/routes/AppRouter.tsx @@ -1,6 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Suspense } from 'react'; -import { RouterProvider, createBrowserRouter } from 'react-router-dom'; +import { Outlet, RouterProvider, createBrowserRouter } from 'react-router-dom'; import BaseLayout from '../layout/BaseLayout'; import MuiTheme from '../mui-theme'; @@ -28,6 +28,10 @@ import CreateSuccessPage from '../pages/auth/CreateSuccess'; import LoadUser from '../pages/loader/LoadUser'; import Loader from '../themed/loader/Loader'; import ProtectedRoute from './ProtectedRoute'; +import SitesPage from '@/pages/reports/SitesPage'; +import HeatMapPage from '@/pages/reports/HeatMapPage'; +import VisitPage from '@/pages/reports/VisitPage'; +import VisitsList from '@/pages/visits/VisitsPage'; // Create a React Query client const queryClient = new QueryClient({ @@ -53,7 +57,7 @@ const router = createBrowserRouter([ element: ( <ProtectedRoute> <PageLayout> - <MyCity />, + <MyCity /> </PageLayout> </ProtectedRoute> ), @@ -63,12 +67,49 @@ const router = createBrowserRouter([ element: ( <ProtectedRoute> <PageLayout> - <MyCommunity />, + <MyCommunity /> </PageLayout> </ProtectedRoute> ), errorElement: <RouterErrorPage />, }, + { + path: 'visits', + element: ( + <ProtectedRoute> + <PageLayout> + <VisitsList /> + </PageLayout> + </ProtectedRoute> + ), + errorElement: <RouterErrorPage />, + }, + { + path: 'reports', + element: ( + <ProtectedRoute> + <PageLayout> + <Outlet /> + </PageLayout> + </ProtectedRoute> + ), + errorElement: <RouterErrorPage />, + children: [ + { + path: 'sites', + element: <SitesPage />, + }, + { + path: 'heat-map', + element: <HeatMapPage />, + }, + { + path: 'visits', + element: <VisitPage />, + errorElement: <RouterErrorPage />, + }, + ], + }, { path: '/admin', element: <BaseAdminPage />, diff --git a/src/schemas/create.ts b/src/schemas/create.ts index 1174183..3328aeb 100644 --- a/src/schemas/create.ts +++ b/src/schemas/create.ts @@ -49,7 +49,6 @@ export const createTeamSchema = () => { return object({ name: requiredNameString, organizationId: string(), - leaderId: string(), sectorId: string(), wedgeId: string(), memberIds: array(object({ label: string(), value: string() })).min(1), @@ -62,7 +61,6 @@ export type CreateTeamInputType = TypeOf<typeof createTeamSchemaForType>; export interface CreateTeam { name: string; organizationId: string; - leaderId: string; sectorId: string; wedgeId: string; memberIds: string[]; diff --git a/src/schemas/entities.ts b/src/schemas/entities.ts index bc61ddd..d1852fe 100644 --- a/src/schemas/entities.ts +++ b/src/schemas/entities.ts @@ -18,6 +18,17 @@ export interface BaseWithStatus extends BaseEntity { export interface Organization extends BaseWithStatus {} +export interface Visit extends BaseEntity { + visitedAt: string; + city: string; + sector: string; + wedge: string; + house: number; + visitStatus: string | null; + brigadist: string; + team: string; +} + export interface SpecialPlace extends BaseWithStatus {} export interface Permission extends BaseEntity { @@ -49,7 +60,7 @@ export interface HouseBlock extends BaseEntity { export interface Post { id: number; - createdAt: number; + createdAt: string; userAccountId: number; canDeleteByUser: boolean; createdBy: string; diff --git a/src/themed/progress-bar/ProgressBar.tsx b/src/themed/progress-bar/ProgressBar.tsx index e89b7f5..700522a 100644 --- a/src/themed/progress-bar/ProgressBar.tsx +++ b/src/themed/progress-bar/ProgressBar.tsx @@ -1,19 +1,29 @@ -import { Box } from '@mui/material'; +import { Box, Tooltip } from '@mui/material'; import React from 'react'; import Text from '../text/Text'; +import Help from '@/assets/icons/help.svg'; export interface ProgressBarProps { label: string; progress: number; + value: number; color: string; + tooltip?: string; } -export const ProgressBar: React.FC<ProgressBarProps> = ({ label, progress, color }) => { +export const ProgressBar: React.FC<ProgressBarProps> = ({ label, value, progress, color, tooltip }) => { return ( <Box className="flex-col items-start mb-4"> <Box className="flex flex-row justify-between"> - <Text className="text-gray-800 font-medium mr-4 mb-2">{label}</Text> - <Text className="text-gray-800 font-medium ml-4">{progress}</Text> + <Box className="flex"> + <Text className="text-gray-800 font-medium mr-2 mb-0">{label}</Text> + {tooltip && ( + <Tooltip title={tooltip} placement="top"> + <img className="self-start mt-1" src={Help} alt="help" width="18" /> + </Tooltip> + )} + </Box> + <Text className="text-gray-800 font-medium ml-4">{value}</Text> </Box> <Box className="flex-row items-center"> <Box className="relative w-11/12 rounded-full h-2" style={{ width: '100%' }}> diff --git a/src/themed/table/DataTable.tsx b/src/themed/table/DataTable.tsx index 3736a8a..b7b4909 100644 --- a/src/themed/table/DataTable.tsx +++ b/src/themed/table/DataTable.tsx @@ -13,7 +13,7 @@ import { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { twMerge } from 'tailwind-merge'; -import { PAGE_SIZES } from '@/constants'; +import { PAGE_SIZES, PageSizes } from '@/constants'; import { formatDateFromString } from '@/util'; import useLangContext from '../../hooks/useLangContext'; @@ -166,6 +166,7 @@ export interface DataTableProps<T> { pagination?: HandlePagination; actions?: (row: T, isLoading?: boolean) => JSX.Element; isLoading?: boolean; + pageSize?: PageSizes; } export function DataTable<T>({ @@ -176,6 +177,7 @@ export function DataTable<T>({ pagination, actions, isLoading, + pageSize = PAGE_SIZES[0], }: DataTableProps<T>) { const { t } = useTranslation('translation'); @@ -185,7 +187,7 @@ export function DataTable<T>({ const [order, setOrder] = React.useState<Order>('asc'); const [orderBy, setOrderBy] = React.useState<Extract<keyof T, string> | undefined>(); const [page, setPage] = React.useState(0); - const [rowsPerPage, setRowsPerPage] = React.useState(PAGE_SIZES[0]); + const [rowsPerPage, setRowsPerPage] = React.useState<PageSizes>(pageSize); // eslint-disable-next-line @typescript-eslint/no-explicit-any const renderValue = (row: T, headCell: HeadCell<T>): string | React.ReactNode => { @@ -239,7 +241,7 @@ export function DataTable<T>({ }; const onChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { - const newRowsPerPage = parseInt(`${event.target.value}`, 10); + const newRowsPerPage = parseInt(`${event.target.value}`, 10) as PageSizes; setRowsPerPage(newRowsPerPage); setPage(0); pagination?.handleChangePage(0, newRowsPerPage); diff --git a/src/themed/text/Text.tsx b/src/themed/text/Text.tsx index ae10f14..cf99323 100644 --- a/src/themed/text/Text.tsx +++ b/src/themed/text/Text.tsx @@ -10,7 +10,7 @@ export type TextProps = { }; export function Text({ children, type, className }: TextProps & PropsWithChildren) { - let textClasses = 'text-lg mb-4'; + let textClasses = 'text-md mb-4'; if (type === 'menuItem') { textClasses = 'mb-0 text-sm'; } diff --git a/src/util/index.ts b/src/util/index.ts index dad49b4..a5f824e 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -24,18 +24,21 @@ export function getProperty(obj: any, propertyString: string): any { } const dateFormatOptions: Intl.DateTimeFormatOptions = { - month: 'short', // Display month in short format - day: 'numeric', // Display day of the month - year: 'numeric', // Display full year + // month: 'short', // Display month in short format + // day: 'numeric', // Display day of the month + // year: 'numeric', // Display full year + year: 'numeric', + month: '2-digit', + day: '2-digit', }; -export const formatDateFromString = (locale: string, date: string | null | undefined) => { +export const formatDateFromString = (_locale: string, date: string | null | undefined) => { if (!date) { return '-'; } const dateObj = new Date(date); - return dateObj.toLocaleDateString(locale, dateFormatOptions); + return dateObj.toLocaleDateString('zh-Hans-CN', dateFormatOptions); }; export function a11yProps(index: number) {