From c8f62b6e9511ecf0b49ddfe8c8899470f8276f7d Mon Sep 17 00:00:00 2001 From: Bohdan Garchu Date: Thu, 19 Dec 2024 21:56:54 +0100 Subject: [PATCH 1/4] feat: year in review reports --- src/app/download-portal/page.tsx | 13 ++- .../DownloadPortal/CountryReports.tsx | 12 +- .../DownloadPortal/YearInReviewReports.tsx | 52 +++++++++ src/domain/props/YearInReviewReportsProps.ts | 5 + .../DownloadPortalOperations.tsx | 110 ++++++++++++++++-- 5 files changed, 176 insertions(+), 16 deletions(-) create mode 100644 src/components/DownloadPortal/YearInReviewReports.tsx create mode 100644 src/domain/props/YearInReviewReportsProps.ts diff --git a/src/app/download-portal/page.tsx b/src/app/download-portal/page.tsx index 6d50a324..05b08ad1 100644 --- a/src/app/download-portal/page.tsx +++ b/src/app/download-portal/page.tsx @@ -1,6 +1,7 @@ import AccordionContainer from '@/components/Accordions/AccordionContainer'; import DownloadCountryAccordion from '@/components/DownloadCountryAccordions/DownloadCountryAccordions'; import CountryReports from '@/components/DownloadPortal/CountryReports'; +import YearInReviewReports from '@/components/DownloadPortal/YearInReviewReports'; import container from '@/container'; import { TITLE } from '@/domain/entities/download/Country'; import { GlobalDataRepository } from '@/domain/repositories/GlobalDataRepository'; @@ -9,7 +10,12 @@ export default async function DownloadPortal() { const globalRepo = container.resolve('GlobalDataRepository'); const countryMapDataPromise = globalRepo.getMapDataForCountries(); const countryCodesPromise = globalRepo.getCountryCodes(); - const [countryCodesData, countryMapData] = await Promise.all([countryCodesPromise, countryMapDataPromise]); + const yearInReviewsPromise = globalRepo.getYearInReviews(); + const [countryCodesData, countryMapData, yearInReviews] = await Promise.all([ + countryCodesPromise, + countryMapDataPromise, + yearInReviewsPromise, + ]); const countries = countryMapData?.features.map((feature) => ({ @@ -24,7 +30,12 @@ export default async function DownloadPortal() {

Download Portal

, + }, { title: 'Country Reports', content: , diff --git a/src/components/DownloadPortal/CountryReports.tsx b/src/components/DownloadPortal/CountryReports.tsx index 4ebda77c..0ebf493f 100644 --- a/src/components/DownloadPortal/CountryReports.tsx +++ b/src/components/DownloadPortal/CountryReports.tsx @@ -6,6 +6,7 @@ import PdfPreview from '@/components/Pdf/PdfPreview'; import SearchBar from '@/components/Search/SearchBar'; import CustomTable from '@/components/Table/CustomTable'; import { useChatbot } from '@/domain/contexts/ChatbotContext'; +import { useSnackbar } from '@/domain/contexts/SnackbarContext'; import { CountryCodesData } from '@/domain/entities/country/CountryCodesData'; import CountryReportsProps from '@/domain/props/CountryReportsProps'; import { PdfFile } from '@/domain/props/PdfViewerProps'; @@ -18,8 +19,8 @@ export default function CountryReports({ countryCodesData }: CountryReportsProps const [error, setError] = useState(null); const [selectedCountry, setSelectedCountry] = useState(null); - const chatBot = useChatbot(); - const { initiateChatAboutReport } = chatBot; + const { initiateChatAboutReport } = useChatbot(); + const { showSnackBar } = useSnackbar(); const toggleModal = () => setModalOpen((prev) => !prev); @@ -34,13 +35,14 @@ export default function CountryReports({ countryCodesData }: CountryReportsProps
@@ -51,7 +53,7 @@ export default function CountryReports({ countryCodesData }: CountryReportsProps error={error} onDownloadPdf={() => { if (selectedCountry) { - DownloadPortalOperations.downloadPdf(selectedCountry); + DownloadPortalOperations.downloadCountryReport(selectedCountry, showSnackBar); } }} /> diff --git a/src/components/DownloadPortal/YearInReviewReports.tsx b/src/components/DownloadPortal/YearInReviewReports.tsx new file mode 100644 index 00000000..05734986 --- /dev/null +++ b/src/components/DownloadPortal/YearInReviewReports.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useState } from 'react'; + +import { useChatbot } from '@/domain/contexts/ChatbotContext'; +import { useSnackbar } from '@/domain/contexts/SnackbarContext'; +import { YearInReview } from '@/domain/entities/YearInReview'; +import { PdfFile } from '@/domain/props/PdfViewerProps'; +import YearInReviewReportsProps from '@/domain/props/YearInReviewReportsProps'; +import { DownloadPortalOperations } from '@/operations/download-portal/DownloadPortalOperations'; + +import PdfPreview from '../Pdf/PdfPreview'; +import CustomTable from '../Table/CustomTable'; + +export default function YearInReviewReports({ yearInReviewReports }: YearInReviewReportsProps) { + const [isModalOpen, setModalOpen] = useState(false); + const [pdfFile, setPdfFile] = useState(null); + const [error, setError] = useState(null); + const [selectedReport, setSelectedReport] = useState(null); + const { initiateChatAboutReport } = useChatbot(); + const toggleModal = () => setModalOpen((prev) => !prev); + const { showSnackBar } = useSnackbar(); + + return ( +
+ + { + if (selectedReport) { + DownloadPortalOperations.downloadYearInReview(selectedReport, showSnackBar); + } + }} + /> +
+ ); +} diff --git a/src/domain/props/YearInReviewReportsProps.ts b/src/domain/props/YearInReviewReportsProps.ts new file mode 100644 index 00000000..ba7336fa --- /dev/null +++ b/src/domain/props/YearInReviewReportsProps.ts @@ -0,0 +1,5 @@ +import { YearInReview } from '../entities/YearInReview'; + +export default interface YearInReviewReportsProps { + yearInReviewReports: YearInReview[]; +} diff --git a/src/operations/download-portal/DownloadPortalOperations.tsx b/src/operations/download-portal/DownloadPortalOperations.tsx index e92724cc..68649e98 100644 --- a/src/operations/download-portal/DownloadPortalOperations.tsx +++ b/src/operations/download-portal/DownloadPortalOperations.tsx @@ -4,7 +4,11 @@ import { Bot } from 'lucide-react'; import { CountryCodesData } from '@/domain/entities/country/CountryCodesData'; import { ICountryData } from '@/domain/entities/download/Country'; +import { SNACKBAR_SHORT_DURATION } from '@/domain/entities/snackbar/Snackbar'; +import { YearInReview } from '@/domain/entities/YearInReview'; +import { SnackbarPosition, SnackbarStatus } from '@/domain/enums/Snackbar'; import { CustomTableColumns } from '@/domain/props/CustomTableProps'; +import { SnackbarProps } from '@/domain/props/SnackbarProps'; export class DownloadPortalOperations { static getColumns(): CustomTableColumns { @@ -16,13 +20,14 @@ export class DownloadPortalOperations { ] as CustomTableColumns; } - static formatTableData( + static formatCountryTableData( data: CountryCodesData[], setSelectedCountry: (countryData: CountryCodesData) => void, initiateChatAboutReport: (country: string, report: string) => Promise, setPdfFile: (file: Blob | null) => void, setError: (error: string | null) => void, - toggleModal: () => void + toggleModal: () => void, + showSnackBar: (snackbarProps: SnackbarProps) => void ) { return data.map((item) => ({ keyColumn: item.country.name, @@ -41,7 +46,7 @@ export class DownloadPortalOperations {
DownloadPortalOperations.downloadPdf(item)} + onClick={() => DownloadPortalOperations.downloadCountryReport(item, showSnackBar)} className="cursor-pointer" />
@@ -58,6 +63,53 @@ export class DownloadPortalOperations { })); } + static formatYearInReviewTableData( + data: YearInReview[], + setSelectedReport: (report: YearInReview) => void, + initiateChatAboutReport: (country: string, report: string) => Promise, + setPdfFile: (file: Blob | null) => void, + setError: (error: string | null) => void, + toggleModal: () => void, + showSnackBar: (snackbarProps: SnackbarProps) => void + ) { + return data.map((item) => ({ + keyColumn: item.label, + preview: ( +
+ { + setSelectedReport(item); + DownloadPortalOperations.handlePreview(item.url, setPdfFile, setError); + toggleModal(); + }} + className="cursor-pointer" + /> +
+ ), + download: ( +
+ DownloadPortalOperations.downloadYearInReview(item, showSnackBar)} + className="cursor-pointer" + /> +
+ ), + chat: ( +
+ { + initiateChatAboutReport(item.label, item.url); + }} + className="cursor-pointer" + /> +
+ ), + })); + } + static downloadJsonFile(data: ICountryData[], country: string): void { const a = document.createElement('a'); const file = new Blob([JSON.stringify(data)], { type: 'application/json' }); @@ -77,13 +129,51 @@ export class DownloadPortalOperations { return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } - static downloadPdf(country: CountryCodesData) { - const link = document.createElement('a'); - link.href = country.url.summary; - link.download = `${country.country.name}.pdf`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + static async downloadCountryReport( + country: CountryCodesData, + showSnackBar: (snackbarProps: SnackbarProps) => void + ): Promise { + await this.downloadFile(country.url.summary, `${country.country.name}.pdf`, showSnackBar); + } + + static async downloadYearInReview( + yearInReview: YearInReview, + showSnackBar: (snackbarProps: SnackbarProps) => void + ): Promise { + await this.downloadFile(yearInReview.url, `${yearInReview.label}.pdf`, showSnackBar); + } + + private static async downloadFile( + fileUrl: string, + fileName: string, + showSnackBar?: (snackbarProps: SnackbarProps) => void + ): Promise { + try { + const response = await fetch(fileUrl); + if (!response.ok) throw new Error('Failed to download file'); + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Cleanup URL object + window.URL.revokeObjectURL(url); + } catch { + if (showSnackBar) { + showSnackBar({ + message: 'Error downloading file', + status: SnackbarStatus.Error, + position: SnackbarPosition.BottomRight, + duration: SNACKBAR_SHORT_DURATION, + }); + } + } } static async fetchPdfAsByteStream(url: string): Promise { From 47a1c25f1747489a0ba4d4beb536b63dfbde5454 Mon Sep 17 00:00:00 2001 From: Bohdan Garchu Date: Fri, 20 Dec 2024 12:38:06 +0100 Subject: [PATCH 2/4] fix: minor refactoring --- .../DownloadPortal/YearInReviewReports.tsx | 2 +- src/domain/contexts/ChatbotContext.tsx | 14 +++++++------- .../download-portal/DownloadPortalOperations.tsx | 12 +++++------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/DownloadPortal/YearInReviewReports.tsx b/src/components/DownloadPortal/YearInReviewReports.tsx index 05734986..81dbf065 100644 --- a/src/components/DownloadPortal/YearInReviewReports.tsx +++ b/src/components/DownloadPortal/YearInReviewReports.tsx @@ -43,7 +43,7 @@ export default function YearInReviewReports({ yearInReviewReports }: YearInRevie error={error} onDownloadPdf={() => { if (selectedReport) { - DownloadPortalOperations.downloadYearInReview(selectedReport, showSnackBar); + DownloadPortalOperations.downloadYearInReviewReport(selectedReport, showSnackBar); } }} /> diff --git a/src/domain/contexts/ChatbotContext.tsx b/src/domain/contexts/ChatbotContext.tsx index 082d327b..6032de5c 100644 --- a/src/domain/contexts/ChatbotContext.tsx +++ b/src/domain/contexts/ChatbotContext.tsx @@ -80,13 +80,13 @@ export function ChatbotProvider({ children }: { children: ReactNode }) { }; /** - * Initiates a chat with a report based on the provided country name and report content. - * If a chat for that specific already exists, it opens that chat; otherwise, it creates a new one. - * @param countryName - The name of the country related to the report. + * Initiates a chat with a report based on the provided report name and report content. + * If a chat for that specific report already exists, it opens that chat; otherwise, it creates a new one. + * @param reportName - The name of the report. * @param reportURL - The URL of the report to be processed. */ - const initiateChatAboutReport = async (countryName: string, reportURL: string) => { - const reportChatIndex = chats.findIndex((chat) => chat.title === `Report ${countryName}`); + const initiateChatAboutReport = async (reportName: string, reportURL: string) => { + const reportChatIndex = chats.findIndex((chat) => chat.title === `Report ${reportName}`); const reportChatExists = reportChatIndex !== -1; if (reportChatExists) { @@ -105,14 +105,14 @@ export function ChatbotProvider({ children }: { children: ReactNode }) { const assistantMessage = { id: crypto.randomUUID(), content: reportText - ? `Hey, how can I help you with this report about ${countryName}?` + ? `Hey, how can I help you with this report about ${reportName}?` : `Hey, unfortunately I'm currently unable to answer questions about this report. You can try it later or chat with me about other things!`, role: SenderRole.ASSISTANT, }; const newChat: IChat = { id: chats.length + 1, - title: `Report ${countryName}`, + title: `Report ${reportName}`, context: reportText, isReportStarter: true, messages: [assistantMessage], diff --git a/src/operations/download-portal/DownloadPortalOperations.tsx b/src/operations/download-portal/DownloadPortalOperations.tsx index 68649e98..45b1ff3d 100644 --- a/src/operations/download-portal/DownloadPortalOperations.tsx +++ b/src/operations/download-portal/DownloadPortalOperations.tsx @@ -91,7 +91,7 @@ export class DownloadPortalOperations {
DownloadPortalOperations.downloadYearInReview(item, showSnackBar)} + onClick={() => DownloadPortalOperations.downloadYearInReviewReport(item, showSnackBar)} className="cursor-pointer" />
@@ -133,17 +133,17 @@ export class DownloadPortalOperations { country: CountryCodesData, showSnackBar: (snackbarProps: SnackbarProps) => void ): Promise { - await this.downloadFile(country.url.summary, `${country.country.name}.pdf`, showSnackBar); + await this.downloadPdfFile(country.url.summary, `${country.country.name}.pdf`, showSnackBar); } - static async downloadYearInReview( + static async downloadYearInReviewReport( yearInReview: YearInReview, showSnackBar: (snackbarProps: SnackbarProps) => void ): Promise { - await this.downloadFile(yearInReview.url, `${yearInReview.label}.pdf`, showSnackBar); + await this.downloadPdfFile(yearInReview.url, `${yearInReview.label}.pdf`, showSnackBar); } - private static async downloadFile( + private static async downloadPdfFile( fileUrl: string, fileName: string, showSnackBar?: (snackbarProps: SnackbarProps) => void @@ -161,8 +161,6 @@ export class DownloadPortalOperations { document.body.appendChild(link); link.click(); document.body.removeChild(link); - - // Cleanup URL object window.URL.revokeObjectURL(url); } catch { if (showSnackBar) { From e3ce2b3b08f395e0bec51915a25e84715dcc596a Mon Sep 17 00:00:00 2001 From: Bohdan Garchu Date: Fri, 20 Dec 2024 12:59:28 +0100 Subject: [PATCH 3/4] fix: refactor constants --- src/app/download-portal/loading.tsx | 13 ++++++++++--- src/app/download-portal/page.tsx | 12 ++++++++---- src/domain/entities/download/Country.ts | 14 -------------- .../download-portal/DownloadPortalConstants.ts | 17 +++++++++++++++++ 4 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 src/operations/download-portal/DownloadPortalConstants.ts diff --git a/src/app/download-portal/loading.tsx b/src/app/download-portal/loading.tsx index c2a6192a..a414b9eb 100644 --- a/src/app/download-portal/loading.tsx +++ b/src/app/download-portal/loading.tsx @@ -1,5 +1,9 @@ import AccordionContainer from '@/components/Accordions/AccordionContainer'; -import { TITLE } from '@/domain/entities/download/Country'; +import { + COUNTRY_REPORTS_TITLE, + EXPORT_COUNTRY_DATA_TITLE, + YEAR_IN_REVIEW_REPORTS_TITLE, +} from '@/operations/download-portal/DownloadPortalConstants'; export default function Loading() { const loading = true; @@ -10,10 +14,13 @@ export default function Loading() { ('GlobalDataRepository'); @@ -33,15 +37,15 @@ export default async function DownloadPortal() { multipleSelectionMode items={[ { - title: 'WFP Real Time Monitoring: Year in Review Reports', + title: YEAR_IN_REVIEW_REPORTS_TITLE, content: , }, { - title: 'Country Reports', + title: COUNTRY_REPORTS_TITLE, content: , }, { - title: TITLE, + title: EXPORT_COUNTRY_DATA_TITLE, content: , }, ]} diff --git a/src/domain/entities/download/Country.ts b/src/domain/entities/download/Country.ts index b6824868..e5745a46 100644 --- a/src/domain/entities/download/Country.ts +++ b/src/domain/entities/download/Country.ts @@ -29,17 +29,3 @@ export interface ICountryData { dataType: string; metrics: IMetrics; } - -export const DESCRIPTION = 'Select a country and a date range to download its food security data.'; - -export const TITLE = 'Export Country Food Security Data'; - -export const COUNTRY_ERROR_MSG = 'Country is required'; - -export const DATE_RANGE_ERROR_MSG = 'Date range is required'; - -export const DATE_RANGE_TOO_LONG_ERROR_MSG = 'Date range must be less than 500 days'; - -export const DOWNLOAD_SUCCESS_MSG = 'Download successful!'; - -export const DOWNLOAD_ERROR_MSG = 'Download failed!'; diff --git a/src/operations/download-portal/DownloadPortalConstants.ts b/src/operations/download-portal/DownloadPortalConstants.ts new file mode 100644 index 00000000..48c7946d --- /dev/null +++ b/src/operations/download-portal/DownloadPortalConstants.ts @@ -0,0 +1,17 @@ +export const EXPORT_COUNTRY_DESCRIPTION = 'Select a country and a date range to download its food security data.'; + +export const EXPORT_COUNTRY_DATA_TITLE = 'Export Country Food Security Data'; + +export const COUNTRY_ERROR_MSG = 'Country is required'; + +export const DATE_RANGE_ERROR_MSG = 'Date range is required'; + +export const DATE_RANGE_TOO_LONG_ERROR_MSG = 'Date range must be less than 500 days'; + +export const DOWNLOAD_SUCCESS_MSG = 'Download successful!'; + +export const DOWNLOAD_ERROR_MSG = 'Download failed!'; + +export const COUNTRY_REPORTS_TITLE = 'Country Reports'; + +export const YEAR_IN_REVIEW_REPORTS_TITLE = 'WFP Real Time Monitoring: Year in Review Reports'; From 6b2b45db5938b27501d10d51decd5c2522f2780a Mon Sep 17 00:00:00 2001 From: Bohdan Garchu Date: Fri, 20 Dec 2024 13:16:24 +0100 Subject: [PATCH 4/4] fix: imports --- .../DownloadCountryAccordions.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/DownloadCountryAccordions/DownloadCountryAccordions.tsx b/src/components/DownloadCountryAccordions/DownloadCountryAccordions.tsx index 659ff7c5..8d7561c1 100644 --- a/src/components/DownloadCountryAccordions/DownloadCountryAccordions.tsx +++ b/src/components/DownloadCountryAccordions/DownloadCountryAccordions.tsx @@ -7,19 +7,19 @@ import React, { useState } from 'react'; import container from '@/container'; import { DOWNLOAD_DATA } from '@/domain/constant/subscribe/Subscribe'; import { useSnackbar } from '@/domain/contexts/SnackbarContext'; +import { SNACKBAR_SHORT_DURATION } from '@/domain/entities/snackbar/Snackbar'; +import { SnackbarPosition, SnackbarStatus } from '@/domain/enums/Snackbar'; +import { SubmitStatus } from '@/domain/enums/SubscribeTopic'; +import DownloadCountryAccordionProps from '@/domain/props/DownloadCountryAccordionProps'; +import DownloadRepository from '@/domain/repositories/DownloadRepository'; import { COUNTRY_ERROR_MSG, DATE_RANGE_ERROR_MSG, DATE_RANGE_TOO_LONG_ERROR_MSG, - DESCRIPTION, DOWNLOAD_ERROR_MSG, DOWNLOAD_SUCCESS_MSG, -} from '@/domain/entities/download/Country'; -import { SNACKBAR_SHORT_DURATION } from '@/domain/entities/snackbar/Snackbar'; -import { SnackbarPosition, SnackbarStatus } from '@/domain/enums/Snackbar'; -import { SubmitStatus } from '@/domain/enums/SubscribeTopic'; -import DownloadCountryAccordionProps from '@/domain/props/DownloadCountryAccordionProps'; -import DownloadRepository from '@/domain/repositories/DownloadRepository'; + EXPORT_COUNTRY_DESCRIPTION, +} from '@/operations/download-portal/DownloadPortalConstants'; import { DownloadPortalOperations } from '@/operations/download-portal/DownloadPortalOperations'; import { SubmitButton } from '../SubmitButton/SubmitButton'; @@ -94,7 +94,7 @@ export default function DownloadCountryAccordion({ countries }: DownloadCountryA return (
-
{DESCRIPTION}
+
{EXPORT_COUNTRY_DESCRIPTION}