mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat(portal): wire the certificates and endorsement screens to the API
Both listed hardcoded samples -- a CoC application permanently awaiting examination, an endorsement from Greece. They now show the seafarer's own certificates and applications. Status badges are keyed by the workflow's own values rather than display strings, so a status the map has not seen falls back to grey instead of rendering colourless, and the label is derived from the status itself rather than stored beside it where the two can disagree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,43 +30,80 @@ import {
|
|||||||
IconShieldCheck,
|
IconShieldCheck,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { authStorage } from '@ema-platform/auth';
|
import { authStorage } from '@ema-platform/auth';
|
||||||
|
import { useApiQuery } from '@ema-platform/api';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Mock data
|
// Mock data
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const MOCK_COC_APPS = [
|
/** What `/certificates/my` returns: what is held, and what is still in flight. */
|
||||||
{
|
interface CertificatesOverview {
|
||||||
id: 'COC-APP-2025-001',
|
certificates: {
|
||||||
type: 'CoC — STCW II/1 Officer in Charge of Navigational Watch',
|
id: string;
|
||||||
submitted: '2025-03-10',
|
type: string;
|
||||||
examDate: '2025-04-15',
|
issued: string;
|
||||||
examVenue: 'EMA HQ — Addis Ababa',
|
expiry: string;
|
||||||
status: 'Examination Scheduled',
|
status: string;
|
||||||
statusColor: 'indigo',
|
}[];
|
||||||
statusNote: 'TRB inspected and approved by EMA officer. Attend your scheduled examination.',
|
applications: {
|
||||||
},
|
id: string;
|
||||||
{
|
applicationId: string;
|
||||||
id: 'COC-APP-2025-005',
|
type: string;
|
||||||
type: 'CoC — STCW II/5 Able Seafarer Deck (AB)',
|
submitted: string;
|
||||||
submitted: '2025-05-01',
|
status: string;
|
||||||
examDate: null,
|
}[];
|
||||||
examVenue: null,
|
}
|
||||||
status: 'TRB Inspection',
|
|
||||||
statusColor: 'yellow',
|
|
||||||
statusNote: 'Your TRB is being physically inspected by an EMA officer. You may be contacted to bring the original document.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const MOCK_CERTIFICATES = [
|
/**
|
||||||
{
|
* Badge colour per workflow status.
|
||||||
id: 'COC-2023-0042',
|
*
|
||||||
type: 'CoC — STCW II/1',
|
* Keyed by the values the API reports rather than display strings, so an
|
||||||
issued: '2023-06-20',
|
* unmapped status falls back to grey instead of rendering colourless.
|
||||||
expiry: '2028-06-20',
|
*/
|
||||||
status: 'Valid',
|
const STATUS_COLOR: Record<string, string> = {
|
||||||
statusColor: 'teal',
|
DRAFT: 'gray',
|
||||||
},
|
SUBMITTED: 'blue',
|
||||||
];
|
UNDER_REVIEW: 'yellow',
|
||||||
|
UNDER_EVALUATION: 'yellow',
|
||||||
|
RESUBMIT_REQUIRED: 'orange',
|
||||||
|
INSPECTION_PENDING: 'grape',
|
||||||
|
INSPECTION_COMPLETED: 'grape',
|
||||||
|
ELIGIBILITY_APPROVED: 'teal',
|
||||||
|
EXAM_PAYMENT_PENDING: 'orange',
|
||||||
|
EXAM_PAID: 'blue',
|
||||||
|
EXAM_SCHEDULED: 'indigo',
|
||||||
|
EXAM_PASSED: 'teal',
|
||||||
|
EXAM_FAILED: 'red',
|
||||||
|
APPROVED: 'teal',
|
||||||
|
REJECTED: 'red',
|
||||||
|
PAYMENT_PENDING: 'orange',
|
||||||
|
PAID: 'blue',
|
||||||
|
PAYMENT_CONFIRMED: 'blue',
|
||||||
|
CERTIFICATE_ISSUED: 'teal',
|
||||||
|
COMPLETED: 'teal',
|
||||||
|
ACTIVE: 'teal',
|
||||||
|
EXPIRED: 'red',
|
||||||
|
SUSPENDED: 'orange',
|
||||||
|
CANCELLED: 'gray',
|
||||||
|
SUPERSEDED: 'gray',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Turns `EXAM_PAYMENT_PENDING` into something a person reads. */
|
||||||
|
function humanStatus(status: string): string {
|
||||||
|
return status
|
||||||
|
.toLowerCase()
|
||||||
|
.split('_')
|
||||||
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value: string | null | undefined): string {
|
||||||
|
if (!value) return '—';
|
||||||
|
return new Date(value).toLocaleDateString('en-GB', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const API_BASE =
|
const API_BASE =
|
||||||
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
|
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
|
||||||
@@ -99,6 +136,13 @@ export function CertificatesPage() {
|
|||||||
const [previewTitle, setPreviewTitle] = useState('');
|
const [previewTitle, setPreviewTitle] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const { data } = useApiQuery<CertificatesOverview>({
|
||||||
|
url: '/certificates/my',
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
const certificates = data?.certificates ?? [];
|
||||||
|
const applications = data?.applications ?? [];
|
||||||
|
|
||||||
const openPreview = async (profileId: string, title: string) => {
|
const openPreview = async (profileId: string, title: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -179,7 +223,7 @@ export function CertificatesPage() {
|
|||||||
{/* Active applications */}
|
{/* Active applications */}
|
||||||
<Paper withBorder radius="lg" p="xl">
|
<Paper withBorder radius="lg" p="xl">
|
||||||
<Text fw={700} mb="md">My Applications</Text>
|
<Text fw={700} mb="md">My Applications</Text>
|
||||||
{MOCK_COC_APPS.length === 0 ? (
|
{applications.length === 0 ? (
|
||||||
<Alert variant="light" color="gray" icon={<IconInfoCircle size={15} />}>
|
<Alert variant="light" color="gray" icon={<IconInfoCircle size={15} />}>
|
||||||
No active CoC/CoP applications. Click "Apply for CoC / CoP" to start.
|
No active CoC/CoP applications. Click "Apply for CoC / CoP" to start.
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -193,21 +237,34 @@ export function CertificatesPage() {
|
|||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{MOCK_COC_APPS.map((app) => (
|
{applications.map((app) => (
|
||||||
<Table.Tr key={app.id}>
|
<Table.Tr key={app.id}>
|
||||||
<Table.Td><Text fz="xs" fw={600} c="blue.7">{app.id}</Text></Table.Td>
|
<Table.Td><Text fz="xs" fw={600} c="blue.7">{app.id}</Text></Table.Td>
|
||||||
<Table.Td><Text fz="xs" fw={500} maw={220} style={{ lineHeight: 1.3 }}>{app.type}</Text></Table.Td>
|
<Table.Td><Text fz="xs" fw={500} maw={220} style={{ lineHeight: 1.3 }}>{app.type}</Text></Table.Td>
|
||||||
<Table.Td><Text fz="xs">{app.submitted}</Text></Table.Td>
|
<Table.Td><Text fz="xs">{formatDate(app.submitted)}</Text></Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{app.examDate
|
<Text fz="xs" c="dimmed" maw={200} lh={1.3}>
|
||||||
? <><Text fz="xs" fw={500}>{app.examDate}</Text><Text fz="xs" c="dimmed">{app.examVenue}</Text></>
|
{humanStatus(app.status)}
|
||||||
: <Text fz="xs" c="dimmed" maw={200} lh={1.3}>{app.statusNote}</Text>}
|
</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Badge color={app.statusColor} variant="light" size="sm">{app.status}</Badge>
|
<Badge
|
||||||
|
color={STATUS_COLOR[app.status] ?? 'gray'}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{humanStatus(app.status)}
|
||||||
|
</Badge>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text fz="xs" c="blue" style={{ cursor: 'pointer' }}>Details</Text>
|
<Text
|
||||||
|
fz="xs"
|
||||||
|
c="blue"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => navigate(`/applications/${app.applicationId}`)}
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
@@ -219,13 +276,13 @@ export function CertificatesPage() {
|
|||||||
{/* Issued certificates */}
|
{/* Issued certificates */}
|
||||||
<Paper withBorder radius="lg" p="xl">
|
<Paper withBorder radius="lg" p="xl">
|
||||||
<Text fw={700} mb="md">My Certificates</Text>
|
<Text fw={700} mb="md">My Certificates</Text>
|
||||||
{MOCK_CERTIFICATES.length === 0 ? (
|
{certificates.length === 0 ? (
|
||||||
<Alert variant="light" color="gray" icon={<IconInfoCircle size={15} />}>
|
<Alert variant="light" color="gray" icon={<IconInfoCircle size={15} />}>
|
||||||
No certificates issued yet.
|
No certificates issued yet.
|
||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||||
{MOCK_CERTIFICATES.map((cert) => (
|
{certificates.map((cert) => (
|
||||||
<Card key={cert.id} withBorder radius="md" p="md">
|
<Card key={cert.id} withBorder radius="md" p="md">
|
||||||
<Group justify="space-between" mb="sm">
|
<Group justify="space-between" mb="sm">
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
@@ -235,7 +292,7 @@ export function CertificatesPage() {
|
|||||||
<Text fz="xs" c="dimmed">{cert.id}</Text>
|
<Text fz="xs" c="dimmed">{cert.id}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
<Badge color={cert.statusColor} variant="light">{cert.status}</Badge>
|
<Badge color={STATUS_COLOR[cert.status] ?? "gray"} variant="light">{cert.status}</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
<Divider mb="sm" />
|
<Divider mb="sm" />
|
||||||
<SimpleGrid cols={2} spacing="xs">
|
<SimpleGrid cols={2} spacing="xs">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useApiQuery } from '@ema-platform/api';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
@@ -39,32 +40,64 @@ import {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Mock data — existing endorsement applications
|
// Mock data — existing endorsement applications
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const MOCK_ENDORSEMENTS = [
|
/** What `/endorsements/my` returns. */
|
||||||
{
|
interface EndorsementsOverview {
|
||||||
id: 'END-APP-2025-001',
|
issued: {
|
||||||
cocType: 'Officer in Charge of a Navigational Watch (STCW II/1)',
|
id: string;
|
||||||
foreignCocNo: 'PHL-COC-2022-0045',
|
endorsementNo: string;
|
||||||
issuingCountry: 'Philippines',
|
cocType: string;
|
||||||
submitted: '2025-04-05',
|
foreignCocNo: string;
|
||||||
status: 'Document Verification',
|
issuingCountry: string;
|
||||||
statusColor: 'blue',
|
issued: string;
|
||||||
statusNote: 'EMA is verifying your documents. You will be notified when verification is complete.',
|
expiry: string;
|
||||||
},
|
status: string;
|
||||||
];
|
}[];
|
||||||
|
applications: {
|
||||||
|
id: string;
|
||||||
|
applicationId: string;
|
||||||
|
cocType: string;
|
||||||
|
foreignCocNo: string;
|
||||||
|
issuingCountry: string;
|
||||||
|
submitted: string;
|
||||||
|
status: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
const MOCK_ISSUED = [
|
// Keyed by the workflow's own status values so an unmapped one falls back to
|
||||||
{
|
// grey rather than rendering colourless.
|
||||||
id: 'EMA-END-2024-012',
|
const STATUS_COLOR: Record<string, string> = {
|
||||||
cocType: 'Chief Mate — STCW II/2',
|
DRAFT: 'gray',
|
||||||
foreignCocNo: 'GRC-COC-2019-0033',
|
SUBMITTED: 'blue',
|
||||||
issuingCountry: 'Greece',
|
UNDER_REVIEW: 'yellow',
|
||||||
endorsementNo: 'EMA-END-2024-012',
|
UNDER_EVALUATION: 'yellow',
|
||||||
issued: '2024-08-10',
|
RESUBMIT_REQUIRED: 'orange',
|
||||||
expiry: '2029-06-15',
|
APPROVED: 'teal',
|
||||||
status: 'Valid',
|
REJECTED: 'red',
|
||||||
statusColor: 'teal',
|
PAYMENT_PENDING: 'orange',
|
||||||
},
|
PAYMENT_CONFIRMED: 'blue',
|
||||||
];
|
CERTIFICATE_ISSUED: 'teal',
|
||||||
|
COMPLETED: 'teal',
|
||||||
|
ACTIVE: 'teal',
|
||||||
|
EXPIRED: 'red',
|
||||||
|
SUSPENDED: 'orange',
|
||||||
|
};
|
||||||
|
|
||||||
|
function humanStatus(status: string): string {
|
||||||
|
return status
|
||||||
|
.toLowerCase()
|
||||||
|
.split('_')
|
||||||
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value: string | null | undefined): string {
|
||||||
|
if (!value) return '—';
|
||||||
|
return new Date(value).toLocaleDateString('en-GB', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// blank PDF
|
// blank PDF
|
||||||
const BLANK_PDF = 'data:application/pdf;base64,JVBERi0xLjQKJcfsj6IKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDIgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagp4cmVmCjAgNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA1OCAwMDAwMCBuIAowMDAwMDAwMTE1IDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA0L1Jvb3QgMSAwIFI+PgpzdGFydHhyZWYKMjE3CiUlRU9G';
|
const BLANK_PDF = 'data:application/pdf;base64,JVBERi0xLjQKJcfsj6IKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDIgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagp4cmVmCjAgNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA1OCAwMDAwMCBuIAowMDAwMDAwMTE1IDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA0L1Jvb3QgMSAwIFI+PgpzdGFydHhyZWYKMjE3CiUlRU9G';
|
||||||
@@ -298,6 +331,13 @@ export function EndorsementPage() {
|
|||||||
const [applying, setApplying] = useState(false);
|
const [applying, setApplying] = useState(false);
|
||||||
const [previewId, setPreviewId] = useState<string | null>(null);
|
const [previewId, setPreviewId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { data } = useApiQuery<EndorsementsOverview>({
|
||||||
|
url: '/endorsements/my',
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
const endorsementApps = data?.applications ?? [];
|
||||||
|
const issuedEndorsements = data?.issued ?? [];
|
||||||
|
|
||||||
if (applying) {
|
if (applying) {
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
@@ -353,29 +393,29 @@ export function EndorsementPage() {
|
|||||||
{/* Active applications */}
|
{/* Active applications */}
|
||||||
<Paper withBorder radius="lg" p="xl">
|
<Paper withBorder radius="lg" p="xl">
|
||||||
<Text fw={700} mb="md">My Endorsement Applications</Text>
|
<Text fw={700} mb="md">My Endorsement Applications</Text>
|
||||||
{MOCK_ENDORSEMENTS.length === 0 ? (
|
{endorsementApps.length === 0 ? (
|
||||||
<Alert variant="light" color="gray" icon={<IconInfoCircle size={15} />}>
|
<Alert variant="light" color="gray" icon={<IconInfoCircle size={15} />}>
|
||||||
No active endorsement applications.
|
No active endorsement applications.
|
||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{MOCK_ENDORSEMENTS.map((app) => (
|
{endorsementApps.map((app) => (
|
||||||
<Paper key={app.id} withBorder radius="md" p="md">
|
<Paper key={app.id} withBorder radius="md" p="md">
|
||||||
<Group justify="space-between" wrap="wrap" gap="sm">
|
<Group justify="space-between" wrap="wrap" gap="sm">
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<ThemeIcon size={36} radius="md" color="blue" variant="light"><IconRubberStamp size={18} /></ThemeIcon>
|
<ThemeIcon size={36} radius="md" color="blue" variant="light"><IconRubberStamp size={18} /></ThemeIcon>
|
||||||
<div>
|
<div>
|
||||||
<Text fz="sm" fw={700}>{app.cocType}</Text>
|
<Text fz="sm" fw={700}>{app.cocType}</Text>
|
||||||
<Text fz="xs" c="dimmed">CoC No: {app.foreignCocNo} · {app.issuingCountry} · Submitted {app.submitted}</Text>
|
<Text fz="xs" c="dimmed">CoC No: {app.foreignCocNo} · {app.issuingCountry} · Submitted {formatDate(app.submitted)}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Badge color={app.statusColor} variant="light">{app.status}</Badge>
|
<Badge color={STATUS_COLOR[app.status] ?? "gray"} variant="light">{humanStatus(app.status)}</Badge>
|
||||||
<Text fz="xs" c="blue.7" fw={600}>{app.id}</Text>
|
<Text fz="xs" c="blue.7" fw={600}>{app.id}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<Alert variant="light" color={app.statusColor} icon={<IconInfoCircle size={13} />} p="xs" mt="sm">
|
<Alert variant="light" color={STATUS_COLOR[app.status] ?? "gray"} icon={<IconInfoCircle size={13} />} p="xs" mt="sm">
|
||||||
<Text fz="xs">{app.statusNote}</Text>
|
<Text fz="xs">{humanStatus(app.status)}</Text>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
@@ -386,13 +426,13 @@ export function EndorsementPage() {
|
|||||||
{/* Issued endorsements */}
|
{/* Issued endorsements */}
|
||||||
<Paper withBorder radius="lg" p="xl">
|
<Paper withBorder radius="lg" p="xl">
|
||||||
<Text fw={700} mb="md">My Endorsements</Text>
|
<Text fw={700} mb="md">My Endorsements</Text>
|
||||||
{MOCK_ISSUED.length === 0 ? (
|
{issuedEndorsements.length === 0 ? (
|
||||||
<Alert variant="light" color="gray" icon={<IconInfoCircle size={15} />}>
|
<Alert variant="light" color="gray" icon={<IconInfoCircle size={15} />}>
|
||||||
No endorsements issued yet.
|
No endorsements issued yet.
|
||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||||
{MOCK_ISSUED.map((end) => (
|
{issuedEndorsements.map((end) => (
|
||||||
<Card key={end.id} withBorder radius="md" p="md">
|
<Card key={end.id} withBorder radius="md" p="md">
|
||||||
<Group justify="space-between" mb="sm">
|
<Group justify="space-between" mb="sm">
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
@@ -402,7 +442,7 @@ export function EndorsementPage() {
|
|||||||
<Text fz="xs" c="dimmed">{end.endorsementNo}</Text>
|
<Text fz="xs" c="dimmed">{end.endorsementNo}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
<Badge color={end.statusColor} variant="light">{end.status}</Badge>
|
<Badge color={STATUS_COLOR[end.status] ?? "gray"} variant="light">{humanStatus(end.status)}</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
<Divider mb="sm" />
|
<Divider mb="sm" />
|
||||||
<SimpleGrid cols={2} spacing="xs" mb="sm">
|
<SimpleGrid cols={2} spacing="xs" mb="sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user