diff --git a/apps/backoffice/index.html b/apps/backoffice/index.html index 8d1d8072c..851a44f30 100644 --- a/apps/backoffice/index.html +++ b/apps/backoffice/index.html @@ -6,9 +6,122 @@ EMA Backoffice + +
+
+
+
+ + + + + + + + + + + + + + + EMA +
+
ETHIOPIAN MARITIME AUTHORITY
+
የኢትዮጵያ ማሪታይም ባለስልጣን
+
Loading Maritime Backoffice…
+
+
diff --git a/apps/backoffice/src/app/features/certificate-designer/hooks/useTemplatePreview.ts b/apps/backoffice/src/app/features/certificate-designer/hooks/useTemplatePreview.ts index 761a89e15..c2714f856 100644 --- a/apps/backoffice/src/app/features/certificate-designer/hooks/useTemplatePreview.ts +++ b/apps/backoffice/src/app/features/certificate-designer/hooks/useTemplatePreview.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { notifications } from '@mantine/notifications'; import { useTranslation } from 'react-i18next'; import { extractErrorMessage } from '@ema-platform/api'; @@ -13,12 +13,14 @@ interface PreviewArgs { /** * Renders the editor's current contents, not the saved row, so unsaved edits - * are what you see. Opened as a blob so it never leaves a file behind. + * are what you see. Opened as a blob into `PdfPreviewModal` rather than a new + * tab, so the designer never loses their place. */ export function useTemplatePreview() { const { t } = useTranslation(); + const [previewUrl, setPreviewUrl] = useState(null); - return useCallback( + const open = useCallback( async ({ hbsSource, licenseTypeId, landscape }: PreviewArgs) => { try { // The preview returns a PDF stream, not JSON, so it bypasses RTK Query @@ -38,10 +40,7 @@ export function useTemplatePreview() { }), }); if (!response.ok) throw new Error(await response.text()); - const url = URL.createObjectURL(await response.blob()); - window.open(url, '_blank', 'noopener'); - // Give the new tab time to read it before revoking. - setTimeout(() => URL.revokeObjectURL(url), 60_000); + setPreviewUrl(URL.createObjectURL(await response.blob())); } catch (err) { notifications.show({ color: 'red', @@ -52,4 +51,13 @@ export function useTemplatePreview() { }, [t], ); + + const close = useCallback(() => { + setPreviewUrl((current) => { + if (current) URL.revokeObjectURL(current); + return null; + }); + }, []); + + return { previewUrl, open, close }; } diff --git a/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx b/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx index 7f49af593..d30aa5d2f 100644 --- a/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx +++ b/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx @@ -31,7 +31,7 @@ import { useUpdateLicenseValidityMutation, useUpdateLicenseTemplateMutation, } from '@ema-platform/api'; -import { EmptyState, ErrorState, PageHeader } from '@ema-platform/ui'; +import { EmptyState, ErrorState, PageHeader, PdfPreviewModal } from '@ema-platform/ui'; import { usePermissions, LICENSE_PERMISSIONS as PERMISSIONS } from '@ema-platform/auth'; import { BlockPropertiesPanel } from '../components/BlockPropertiesPanel'; import { DesignerToolbar } from '../components/DesignerToolbar'; @@ -86,7 +86,7 @@ export function CertificateDesignerPage() { const draft = useTemplateDraft(templates); const run = useDesignerActions(); - const openPreview = useTemplatePreview(); + const { previewUrl, open: openPreview, close: closePreview } = useTemplatePreview(); const [newOpen, setNewOpen] = useState(false); const [newName, setNewName] = useState(''); @@ -377,6 +377,13 @@ export function CertificateDesignerPage() { }, t('designer.created', 'Draft created')) } /> + + ); } diff --git a/apps/backoffice/src/app/features/configuration/pages/ConfigurationPage/index.tsx b/apps/backoffice/src/app/features/configuration/pages/ConfigurationPage/index.tsx index 13dfada32..3e38cc111 100644 --- a/apps/backoffice/src/app/features/configuration/pages/ConfigurationPage/index.tsx +++ b/apps/backoffice/src/app/features/configuration/pages/ConfigurationPage/index.tsx @@ -31,6 +31,7 @@ import { AdvancedTable, useServerTable, ModalFooter, + PageLoader, } from "@ema-platform/ui"; import { LocationPage } from "../../../location/pages/LocationPage"; import { CertificationPage } from "../../../certification/pages/CertificationPage"; @@ -294,11 +295,7 @@ function ProfessionTab() { ]; if (isLoading) { - return ( -
- -
- ); + return ; } if (isError) { diff --git a/apps/backoffice/src/app/features/dashboard/pages/DashboardPage/index.tsx b/apps/backoffice/src/app/features/dashboard/pages/DashboardPage/index.tsx index 443b66eae..927ca47a3 100644 --- a/apps/backoffice/src/app/features/dashboard/pages/DashboardPage/index.tsx +++ b/apps/backoffice/src/app/features/dashboard/pages/DashboardPage/index.tsx @@ -11,7 +11,7 @@ import { } from '@mantine/core'; import { IconChevronRight } from '@tabler/icons-react'; import { useGetAssignedToMeQuery, useGetQueueQuery } from '@ema-platform/api'; -import { AdvancedTable, useServerTable } from '@ema-platform/ui'; +import { AdvancedTable, PageLoader, useServerTable } from '@ema-platform/ui'; import { dashboardQueueColumns } from './columns'; /** @@ -28,11 +28,7 @@ export function DashboardPage() { const table = useServerTable(); if (queue.isLoading || mine.isLoading) { - return ( -
- -
- ); + return ; } const unclaimed = queue.data?.items ?? []; diff --git a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx index e49efe907..6c09dd93b 100644 --- a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx +++ b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx @@ -54,6 +54,7 @@ import { QuestionAssigner } from '../components/QuestionAssigner'; import { RecordResultModal } from '../../result/components/RecordResultModal'; import { ExamCandidatesPanel } from '../components/ExamCandidatesPanel'; import { ExamIncidentsPanel } from '../components/ExamIncidentsPanel'; +import { PageLoader } from '@ema-platform/ui'; import type { ExamStatus, QuestionBrief } from '../types/exam'; const STATUS_COLOR: Record = { @@ -129,11 +130,7 @@ export function ExamDetailPage() { }, [allQuestions, exam?.certificationId, exam?.form]); if (isLoading) - return ( -
- -
- ); + return ; if (isError || !exam) { return ( diff --git a/apps/backoffice/src/app/features/license-review/components/DecisionBar.tsx b/apps/backoffice/src/app/features/license-review/components/DecisionBar.tsx index 0403cfaad..e224fc028 100644 --- a/apps/backoffice/src/app/features/license-review/components/DecisionBar.tsx +++ b/apps/backoffice/src/app/features/license-review/components/DecisionBar.tsx @@ -76,9 +76,13 @@ export function DecisionBar({ role="region" aria-label={t('review.decisionBar', 'Decision bar')} > - + {/* Wraps rather than overflows: at narrow widths the nowrap row pushed + the workflow buttons past the viewport edge, so Assign, Escalate and + Hold were simply not there. Wrapping drops them onto a second line + instead of off the screen. */} + {/* Left: where the application stands, and who has it. */} - + {t(`queue.statusValues.${status}`, STATUS_LABELS[status])} @@ -124,7 +128,7 @@ export function DecisionBar({ {/* Right: the decision. */} - + {primary.map((action) => ( onAction(action)} > {t(action.labelKey)} @@ -207,7 +218,13 @@ function ActionButton({ action, busy, size, onAction }: ActionButtonProps) { if (action.enabled) return button; return ( - + {button} ); @@ -234,7 +251,13 @@ function MenuAction({ ); if (action.enabled) return item; return ( - +
{item}
); diff --git a/apps/backoffice/src/app/features/license-review/components/DocumentsTab.tsx b/apps/backoffice/src/app/features/license-review/components/DocumentsTab.tsx index 979ab44a8..7d55d4bce 100644 --- a/apps/backoffice/src/app/features/license-review/components/DocumentsTab.tsx +++ b/apps/backoffice/src/app/features/license-review/components/DocumentsTab.tsx @@ -34,6 +34,7 @@ import { type DocumentRequirement, } from "@ema-platform/api"; import { notifications } from "@mantine/notifications"; +import { PdfPreviewModal } from "@ema-platform/ui"; interface DocumentsTabProps { applicationId: string; @@ -441,8 +442,20 @@ export function DocumentsTab({ ); })} + setPreview(null)} + url={previewFile?.url ?? ""} + title={ + preview + ? localized(requirementByKey.get(preview.documentKey)?.name) || + preview.documentKey + : "" + } + /> + setPreview(null)} position="right" size="xl" @@ -458,16 +471,7 @@ export function DocumentsTab({ returnFocus > {previewFile?.url ? ( - isPdf ? ( -