feat: Refactor seafarer document application flow

- Remove SeamanBookApplicationPage from router and redirect to Seaman Book page.
- Add new API endpoints for managing seafarer documents, including listing, reviewing, and issuing documents.
- Introduce new SeafarerDocumentQueuePage and SeafarerDocumentReviewPage components for document management.
- Update licensing types to accommodate optional applicationId and documentId in ApplicationPayment.
- Remove unused claimSeafarerRegistration mutation and related constants.
- Update seafarer registration status labels and types to remove 'UNDER_REVIEW'.
- Create new constants and types for seafarer documents, including status labels and colors.
- Implement document payment initiation and confirmation functionalities.
- Enhance UI components for better user experience in document management.
This commit is contained in:
Nati
2026-08-20 08:39:17 +00:00
parent 5a802b5dfc
commit 8eb4c38216
27 changed files with 1081 additions and 1325 deletions

View File

@@ -5,6 +5,7 @@ export * from './lib/features/licensing';
export * from './lib/features/location';
export * from './lib/features/seafarer';
export * from './lib/features/seafarer-registration';
export * from './lib/features/seafarer-document';
export * from './lib/features/vessel';
export { baseQueryWithReauth, configureTokenRefresh } from './lib/base-api/base-query-with-reauth';
export { openAuthedDocument, downloadAuthedFile } from './lib/base-api/download';

View File

@@ -613,7 +613,8 @@ export interface InitiatePaymentResult {
export interface ApplicationPayment {
id: string;
applicationId: string;
applicationId: string | null;
documentId?: string | null;
paymentIntentId: string;
amount: string;
currency: string;

View File

@@ -0,0 +1,3 @@
export * from './seafarer-document.types';
export * from './seafarer-document.constants';
export * from './seafarer-document-api';

View File

@@ -0,0 +1,131 @@
import { baseApi } from '../../base-api';
import type { ApplicationPayment, InitiatePaymentResult } from '../licensing/licensing.types';
import type {
SeafarerDocument,
SeafarerDocumentDetail,
SeafarerDocumentKind,
SeafarerDocumentRow,
SeafarerDocumentStatus,
} from './seafarer-document.types';
const TAG = 'SeafarerDocument' as const;
const LIST = { type: TAG, id: 'LIST' } as const;
const item = (id: string) => ({ type: TAG, id }) as const;
export interface SeafarerDocumentListFilter {
kind?: SeafarerDocumentKind;
status?: SeafarerDocumentStatus;
search?: string;
take?: number;
skip?: number;
}
/**
* Seaman Book and BTC — own endpoints, not licence applications. Fees settle
* through the same payment gateway, under `/seafarer-documents/:id/payments`.
*/
export const seafarerDocumentApi = baseApi
.enhanceEndpoints({ addTagTypes: [TAG] })
.injectEndpoints({
endpoints: (builder) => ({
// ------------------------------------------------------------ applicant
getMySeafarerDocuments: builder.query<
{ seamanBook: SeafarerDocument | null; btc: SeafarerDocument | null },
void
>({
query: () => ({ url: '/seafarer-documents/mine' }),
providesTags: () => [LIST],
}),
getMySeafarerDocumentDownload: builder.query<{ url: string }, string>({
query: (id) => ({ url: `/seafarer-documents/${id}/download` }),
}),
initiateDocumentPayment: builder.mutation<
InitiatePaymentResult,
{ id: string; provider?: string; platform?: 'web' | 'mobile'; payerAccount?: string }
>({
query: ({ id, ...body }) => ({
url: `/seafarer-documents/${id}/payments/initiate`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) => (error ? [] : [LIST, item(id)]),
}),
getDocumentPayment: builder.query<ApplicationPayment, string>({
query: (id) => ({ url: `/seafarer-documents/${id}/payments` }),
providesTags: (_r, _e, id) => [item(id)],
}),
bypassDocumentPayment: builder.mutation<{ status: SeafarerDocumentStatus }, string>({
query: (id) => ({ url: `/seafarer-documents/${id}/payments/bypass`, method: 'POST' }),
invalidatesTags: (_r, error, id) => (error ? [] : [LIST, item(id)]),
}),
// --------------------------------------------------------------- review
listSeafarerDocuments: builder.query<
{ total: number; items: SeafarerDocumentRow[] },
SeafarerDocumentListFilter
>({
query: (params) => ({ url: '/seafarer-document-review', params }),
providesTags: () => [LIST],
}),
getSeafarerDocumentReview: builder.query<SeafarerDocumentDetail, string>({
query: (id) => ({ url: `/seafarer-document-review/${id}` }),
providesTags: (_r, _e, id) => [item(id)],
}),
getSeafarerDocumentReviewDownload: builder.query<{ url: string }, string>({
query: (id) => ({ url: `/seafarer-document-review/${id}/download` }),
}),
confirmSeafarerDocumentPayment: builder.mutation<SeafarerDocument, string>({
query: (id) => ({ url: `/seafarer-document-review/${id}/confirm-payment`, method: 'POST' }),
invalidatesTags: (_r, error, id) => (error ? [] : [LIST, item(id)]),
}),
scheduleSeafarerDocument: builder.mutation<
SeafarerDocument,
{ id: string; scheduledDate: string }
>({
query: ({ id, ...body }) => ({
url: `/seafarer-document-review/${id}/schedule-issuance`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) => (error ? [] : [LIST, item(id)]),
}),
issueSeafarerDocument: builder.mutation<SeafarerDocument, string>({
query: (id) => ({ url: `/seafarer-document-review/${id}/issue`, method: 'POST' }),
invalidatesTags: (_r, error, id) => (error ? [] : [LIST, item(id)]),
}),
rejectSeafarerDocument: builder.mutation<SeafarerDocument, { id: string; reason: string }>({
query: ({ id, ...body }) => ({
url: `/seafarer-document-review/${id}/reject`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) => (error ? [] : [LIST, item(id)]),
}),
}),
overrideExisting: false,
});
export const {
useGetMySeafarerDocumentsQuery,
useLazyGetMySeafarerDocumentDownloadQuery,
useInitiateDocumentPaymentMutation,
useGetDocumentPaymentQuery,
useBypassDocumentPaymentMutation,
useListSeafarerDocumentsQuery,
useGetSeafarerDocumentReviewQuery,
useLazyGetSeafarerDocumentReviewDownloadQuery,
useConfirmSeafarerDocumentPaymentMutation,
useScheduleSeafarerDocumentMutation,
useIssueSeafarerDocumentMutation,
useRejectSeafarerDocumentMutation,
} = seafarerDocumentApi;

View File

@@ -0,0 +1,28 @@
import type { SeafarerDocumentKind, SeafarerDocumentStatus } from './seafarer-document.types';
export const SEAFARER_DOCUMENT_KIND_LABELS: Record<SeafarerDocumentKind, string> = {
SEAMAN_BOOK: 'Seaman Book',
BTC_BASIC_TRAINING: 'Basic Training Certificate',
};
export const SEAFARER_DOCUMENT_STATUS_LABELS: Record<SeafarerDocumentStatus, string> = {
AWAITING_REGISTRATION: 'Awaiting Registration',
PAYMENT_PENDING: 'Payment Pending',
PAID: 'Paid',
PAYMENT_CONFIRMED: 'Payment Confirmed',
SCHEDULED: 'Pickup Scheduled',
ISSUED: 'Issued',
REJECTED: 'Rejected',
CANCELLED: 'Cancelled',
};
export const SEAFARER_DOCUMENT_STATUS_COLORS: Record<SeafarerDocumentStatus, string> = {
AWAITING_REGISTRATION: 'gray',
PAYMENT_PENDING: 'orange',
PAID: 'blue',
PAYMENT_CONFIRMED: 'blue',
SCHEDULED: 'grape',
ISSUED: 'teal',
REJECTED: 'red',
CANCELLED: 'gray',
};

View File

@@ -0,0 +1,54 @@
import type { ApplicationPayment } from '../licensing/licensing.types';
export type SeafarerDocumentKind = 'SEAMAN_BOOK' | 'BTC_BASIC_TRAINING';
export type SeafarerDocumentStatus =
| 'AWAITING_REGISTRATION'
| 'PAYMENT_PENDING'
| 'PAID'
| 'PAYMENT_CONFIRMED'
| 'SCHEDULED'
| 'ISSUED'
| 'REJECTED'
| 'CANCELLED';
/** A Seaman Book or BTC request — opened by a seafarer registration. */
export interface SeafarerDocument {
id: string;
kind: SeafarerDocumentKind;
requestNumber: string;
applicantUserId: string;
profileId: string | null;
seafarerRegistrationId: string | null;
status: SeafarerDocumentStatus;
feeAmount: number | null;
feeCurrency: string;
submittedAt: string | null;
paidAt: string | null;
paymentReference: string | null;
scheduledIssuanceDate: string | null;
documentNumber: string | null;
issueDate: string | null;
expiryDate: string | null;
documentFileKey: string | null;
issuedAt: string | null;
rejectionReason: string | null;
createdAt: string;
}
export interface SeafarerDocumentApplicant {
name: string;
seafarerNumber: string | null;
registrationNumber: string | null;
registrationId: string | null;
}
export type SeafarerDocumentRow = SeafarerDocument & {
applicant: SeafarerDocumentApplicant | null;
};
export interface SeafarerDocumentDetail {
document: SeafarerDocument;
applicant: SeafarerDocumentApplicant | null;
payment: ApplicationPayment | null;
}

View File

@@ -66,11 +66,6 @@ export const seafarerRegistrationApi = baseApi
providesTags: (_r, _e, id) => [item(id)],
}),
claimSeafarerRegistration: builder.mutation<SeafarerRegistration, string>({
query: (id) => ({ url: `/seafarer-registration-review/${id}/claim`, method: 'POST' }),
invalidatesTags: (_r, error, id) => (error ? [] : [LIST, item(id)]),
}),
approveSeafarerRegistration: builder.mutation<
SeafarerRegistration,
{ id: string; remark?: string }
@@ -117,7 +112,6 @@ export const {
useSubmitSeafarerRegistrationMutation,
useListSeafarerRegistrationsQuery,
useGetSeafarerRegistrationReviewQuery,
useClaimSeafarerRegistrationMutation,
useApproveSeafarerRegistrationMutation,
useRejectSeafarerRegistrationMutation,
useRequestSeafarerRegistrationChangesMutation,

View File

@@ -102,7 +102,6 @@ export const SEAFARER_REGISTRATION_DOCUMENTS: {
export const SEAFARER_REGISTRATION_STATUS_LABELS: Record<SeafarerRegistrationStatus, string> = {
DRAFT: 'Draft',
SUBMITTED: 'Submitted',
UNDER_REVIEW: 'Under Review',
RESUBMIT_REQUIRED: 'Corrections Requested',
APPROVED: 'Approved',
REJECTED: 'Rejected',
@@ -111,7 +110,6 @@ export const SEAFARER_REGISTRATION_STATUS_LABELS: Record<SeafarerRegistrationSta
export const SEAFARER_REGISTRATION_STATUS_COLORS: Record<SeafarerRegistrationStatus, string> = {
DRAFT: 'gray',
SUBMITTED: 'blue',
UNDER_REVIEW: 'indigo',
RESUBMIT_REQUIRED: 'orange',
APPROVED: 'teal',
REJECTED: 'red',

View File

@@ -3,7 +3,6 @@ import type { SeafarerDepartment } from '../seafarer/seafarer.types';
export type SeafarerRegistrationStatus =
| 'DRAFT'
| 'SUBMITTED'
| 'UNDER_REVIEW'
| 'RESUBMIT_REQUIRED'
| 'APPROVED'
| 'REJECTED';
@@ -55,8 +54,6 @@ export interface SeafarerRegistration extends SeafarerRegistrationAnswers {
profileId: string | null;
status: SeafarerRegistrationStatus;
submittedAt: string | null;
assignedOfficerId: string | null;
claimedAt: string | null;
decidedAt: string | null;
decidedById: string | null;
/** What the officer asked to be fixed, or noted at approval. */