mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
Merge branch 'WorkflowChange' of https://github.com/Tria-plc/emaui into feature/exam-attempt-domain
This commit is contained in:
@@ -198,20 +198,21 @@ export type VerificationKind = 'medical' | 'sea-service';
|
||||
*/
|
||||
export function MedicalVerificationPage({ kind = 'medical' }: { kind?: VerificationKind }) {
|
||||
const { t } = useTranslation();
|
||||
const isMedical = kind === 'medical';
|
||||
const [filter, setFilter] = useState<RecordQueueFilter>('SUBMITTED');
|
||||
const {
|
||||
data: pendingMedical,
|
||||
isLoading: loadingMedical,
|
||||
isFetching: fetchingMedical,
|
||||
refetch: refetchMedical,
|
||||
} = useGetPendingMedicalQuery(filter);
|
||||
} = useGetPendingMedicalQuery(filter, { skip: !isMedical });
|
||||
|
||||
const {
|
||||
data: pendingSeaService,
|
||||
isLoading: loadingSeaService,
|
||||
isFetching: fetchingSeaService,
|
||||
refetch: refetchSeaService,
|
||||
} = useGetPendingSeaServiceQuery(filter);
|
||||
} = useGetPendingSeaServiceQuery(filter, { skip: isMedical });
|
||||
|
||||
const [verifyMedical, { isLoading: rulingMedical }] =
|
||||
useVerifyMedicalCertificateMutation();
|
||||
@@ -372,8 +373,6 @@ export function MedicalVerificationPage({ kind = 'medical' }: { kind?: Verificat
|
||||
[rulingSeaService, rule, verifySeaService, showDate, t],
|
||||
);
|
||||
|
||||
const isMedical = kind === 'medical';
|
||||
|
||||
return (
|
||||
<Container size="xl" py="md">
|
||||
<Title order={3} mb={4}>
|
||||
@@ -393,6 +392,8 @@ export function MedicalVerificationPage({ kind = 'medical' }: { kind?: Verificat
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{statusFilter}
|
||||
|
||||
{isMedical ? (
|
||||
<AdvancedTable
|
||||
columns={medicalTableColumns}
|
||||
@@ -408,7 +409,7 @@ export function MedicalVerificationPage({ kind = 'medical' }: { kind?: Verificat
|
||||
onPageSizeChange={handleMedicalPageSizeChange}
|
||||
refresh={refetchMedical}
|
||||
isLoading={loadingMedical || fetchingMedical}
|
||||
emptyText={t('recordVerification.emptyText', 'Nothing awaiting verification.')}
|
||||
emptyText={emptyText}
|
||||
/>
|
||||
) : (
|
||||
<AdvancedTable
|
||||
@@ -425,7 +426,7 @@ export function MedicalVerificationPage({ kind = 'medical' }: { kind?: Verificat
|
||||
onPageSizeChange={handleSeaServicePageSizeChange}
|
||||
refresh={refetchSeaService}
|
||||
isLoading={loadingSeaService || fetchingSeaService}
|
||||
emptyText={t('recordVerification.emptyText', 'Nothing awaiting verification.')}
|
||||
emptyText={emptyText}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
IconLock,
|
||||
IconMail,
|
||||
IconMoon,
|
||||
IconPhone,
|
||||
IconSettings,
|
||||
IconShieldLock,
|
||||
IconSun,
|
||||
@@ -42,7 +41,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { notify, PageHeader, useErrorHandler, passwordSchema as strongPasswordSchema, PasswordRequirements, phoneNumber } from '@ema-platform/ui';
|
||||
import { notify, PageHeader, useErrorHandler, passwordSchema as strongPasswordSchema, PasswordRequirements, phoneNumber, PhoneInput } from '@ema-platform/ui';
|
||||
import { useApiMutation } from '@ema-platform/api';
|
||||
import { ActiveSessions, setUser } from '@ema-platform/auth';
|
||||
import type { AuthUser } from '@ema-platform/auth';
|
||||
@@ -136,6 +135,9 @@ export function ProfilePage() {
|
||||
register: registerProfile,
|
||||
handleSubmit: handleProfileSubmit,
|
||||
reset: resetProfile,
|
||||
watch: watchProfile,
|
||||
setValue: setValueProfile,
|
||||
trigger: triggerProfile,
|
||||
formState: { errors: profileErrors },
|
||||
} = useForm<ProfileValues>({
|
||||
resolver: zodResolver(profileSchema),
|
||||
@@ -372,11 +374,12 @@ export function ProfilePage() {
|
||||
error={profileErrors.email?.message}
|
||||
{...registerProfile('email')}
|
||||
/>
|
||||
<TextInput
|
||||
<PhoneInput
|
||||
label={t('profile.fields.phone')}
|
||||
leftSection={<IconPhone size={18} />}
|
||||
value={watchProfile('phoneNumber') || ''}
|
||||
onChange={(val) => setValueProfile('phoneNumber', val, { shouldValidate: !!profileErrors.phoneNumber })}
|
||||
onBlur={() => triggerProfile('phoneNumber')}
|
||||
error={profileErrors.phoneNumber?.message}
|
||||
{...registerProfile('phoneNumber')}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { AppShell, Drawer } from '@mantine/core';
|
||||
import { AppShell, Box, Drawer, Group, Text } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -26,6 +26,14 @@ const BADGE_POLL_MS = 60_000;
|
||||
|
||||
const HEADER_HEIGHT = 116;
|
||||
|
||||
/**
|
||||
* Horizontal inset of the header chrome. `AppHeader` adds its own `px="lg"`
|
||||
* inside this, so the nav strip below needs the sum to line up with the
|
||||
* controls above it — it used to start 20px to their left.
|
||||
*/
|
||||
const CHROME_PAD_X = 32;
|
||||
const NAV_STRIP_PAD_X = CHROME_PAD_X + 20;
|
||||
|
||||
/**
|
||||
* A desk left unlocked with a license-review or medical-record screen open is
|
||||
* the actual threat model here, not a slow token. 15 minutes of no mouse,
|
||||
@@ -144,7 +152,9 @@ export function BackofficeLayout() {
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: isSidebar ? 74 : HEADER_HEIGHT }}
|
||||
// The top layout drops its nav strip on small screens — the drawer is
|
||||
// the nav there — so the header shrinks back to a single row with it.
|
||||
header={{ height: isSidebar ? 74 : { base: 74, sm: HEADER_HEIGHT } }}
|
||||
navbar={
|
||||
isSidebar
|
||||
? {
|
||||
@@ -162,13 +172,28 @@ export function BackofficeLayout() {
|
||||
<AppShell.Header
|
||||
style={{
|
||||
background: "var(--mantine-color-body)",
|
||||
borderBottom: "1px solid var(--mantine-color-gray-2)",
|
||||
borderBottom: "1px solid var(--mantine-color-default-border)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div style={{ height: 74, flexShrink: 0, padding: "0 32px" }}>
|
||||
<div
|
||||
style={{ height: 74, flexShrink: 0, padding: `0 ${CHROME_PAD_X}px` }}
|
||||
>
|
||||
<AppHeader
|
||||
brand={
|
||||
isSidebar ? undefined : (
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<BrandMark size={28} />
|
||||
<Text fw={700} size="sm" lh={1.1} visibleFrom="xs">
|
||||
{t('app.name')}
|
||||
</Text>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
// Nothing to toggle on a desktop top bar; on mobile it opens the
|
||||
// drawer below.
|
||||
burgerHiddenFrom={isSidebar ? undefined : 'sm'}
|
||||
onToggleNav={toggleNav}
|
||||
onToggleSidebar={isSidebar ? handleToggleCollapse : toggleNav}
|
||||
navOpened={opened}
|
||||
@@ -182,13 +207,14 @@ export function BackofficeLayout() {
|
||||
</div>
|
||||
|
||||
{!isSidebar && (
|
||||
<div
|
||||
<Box
|
||||
visibleFrom="sm"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 32px',
|
||||
padding: `0 ${NAV_STRIP_PAD_X}px`,
|
||||
height: 42,
|
||||
borderTop: '1px solid var(--mantine-color-gray-1)',
|
||||
borderTop: '1px solid var(--mantine-color-default-border)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
@@ -199,7 +225,7 @@ export function BackofficeLayout() {
|
||||
activePath={location.pathname}
|
||||
onNavigate={go}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
</AppShell.Header>
|
||||
|
||||
@@ -210,7 +236,7 @@ export function BackofficeLayout() {
|
||||
overflow: "hidden",
|
||||
transition: "width 200ms ease",
|
||||
background: "var(--mantine-color-body)",
|
||||
borderRight: "1px solid var(--mantine-color-gray-2)",
|
||||
borderRight: "1px solid var(--mantine-color-default-border)",
|
||||
}}
|
||||
>
|
||||
<AppSidebar
|
||||
@@ -238,30 +264,28 @@ export function BackofficeLayout() {
|
||||
{/* Mobile nav: a proper Drawer (sized, backdrop, closes on outside
|
||||
click) instead of AppShell's full-width mobile navbar. Mirrors the
|
||||
landing page's mobile menu. */}
|
||||
{isSidebar && (
|
||||
<Drawer
|
||||
opened={opened}
|
||||
onClose={closeNav}
|
||||
hiddenFrom="sm"
|
||||
size="75%"
|
||||
padding={0}
|
||||
withCloseButton={false}
|
||||
>
|
||||
<AppSidebar
|
||||
navItems={sections}
|
||||
collapsed={false}
|
||||
activePath={location.pathname}
|
||||
onToggleCollapse={handleToggleCollapse}
|
||||
onNavigate={(item) => {
|
||||
go(item);
|
||||
closeNav();
|
||||
}}
|
||||
brandName={t('app.name')}
|
||||
brandSubtitle={t('app.authority')}
|
||||
brandLogo={<BrandMark size={32} />}
|
||||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
<Drawer
|
||||
opened={opened}
|
||||
onClose={closeNav}
|
||||
hiddenFrom="sm"
|
||||
size="75%"
|
||||
padding={0}
|
||||
withCloseButton={false}
|
||||
>
|
||||
<AppSidebar
|
||||
navItems={sections}
|
||||
collapsed={false}
|
||||
activePath={location.pathname}
|
||||
onToggleCollapse={handleToggleCollapse}
|
||||
onNavigate={(item) => {
|
||||
go(item);
|
||||
closeNav();
|
||||
}}
|
||||
brandName={t('app.name')}
|
||||
brandSubtitle={t('app.authority')}
|
||||
brandLogo={<BrandMark size={32} />}
|
||||
/>
|
||||
</Drawer>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user