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'); 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,13 +34,18 @@ export default async function DownloadPortal() {

Download Portal

, + }, + { + title: COUNTRY_REPORTS_TITLE, content: , }, { - title: TITLE, + title: EXPORT_COUNTRY_DATA_TITLE, content: , }, ]} 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}
(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..81dbf065 --- /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.downloadYearInReviewReport(selectedReport, showSnackBar); + } + }} + /> +
+ ); +} diff --git a/src/components/Pdf/PdfViewer.tsx b/src/components/Pdf/PdfViewer.tsx index a0354eb9..3ecdd92e 100644 --- a/src/components/Pdf/PdfViewer.tsx +++ b/src/components/Pdf/PdfViewer.tsx @@ -129,22 +129,24 @@ export default function PdfViewer({ {/* PDF Viewer */}
- - {Array.from(new Array(totalPages), (_, index) => ( - - ))} - + {file && ( + + {Array.from(new Array(totalPages), (_, index) => ( + + ))} + + )}
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/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/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/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'; diff --git a/src/operations/download-portal/DownloadPortalOperations.tsx b/src/operations/download-portal/DownloadPortalOperations.tsx index e92724cc..45b1ff3d 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.downloadYearInReviewReport(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,49 @@ 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.downloadPdfFile(country.url.summary, `${country.country.name}.pdf`, showSnackBar); + } + + static async downloadYearInReviewReport( + yearInReview: YearInReview, + showSnackBar: (snackbarProps: SnackbarProps) => void + ): Promise { + await this.downloadPdfFile(yearInReview.url, `${yearInReview.label}.pdf`, showSnackBar); + } + + private static async downloadPdfFile( + 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); + 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 {