mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Implement reject step functionality for contract approval process and enhance contract editing experience
This commit is contained in:
@@ -250,6 +250,44 @@ export class ContractTransitionService {
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject one approval step (line staff / director / CEO). The rejecting
|
||||
* approver must supply a reason. A rejection is terminal: the whole contract
|
||||
* moves to REJECTED and the customer must create a new one — there is no
|
||||
* resubmit of the same contract. The reason is recorded both on the step and
|
||||
* as a REJECTION review note so it is visible to the customer and the rest of
|
||||
* the approval chain.
|
||||
*/
|
||||
async rejectStep(
|
||||
contractId: string,
|
||||
stepId: string,
|
||||
actorId: string,
|
||||
reason: string,
|
||||
): Promise<Contract> {
|
||||
const contract = await this.contractsService.findById(contractId);
|
||||
assertContractStatus(contract, ['PENDING_APPROVAL', 'APPROVED_PENDING_SIGNATURE']);
|
||||
|
||||
const step = await this.contractsRepository.findApprovalStepById(contractId, stepId);
|
||||
if (!step) throw new BadRequestException('Approval step not found');
|
||||
|
||||
await this.contractsRepository.completeApprovalStep(step.id, actorId, 'REJECTED', reason);
|
||||
|
||||
await this.contractsRepository.createReviewNote(
|
||||
contractId,
|
||||
reason,
|
||||
'REJECTION',
|
||||
actorId,
|
||||
'STAFF',
|
||||
);
|
||||
|
||||
await this.contractsRepository.update(contractId, {
|
||||
status: 'REJECTED',
|
||||
} as never);
|
||||
const updated = await this.contractsService.findById(contractId);
|
||||
this.notifier.rejected(updated, reason);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/** Approve one approval step in sequence; → APPROVED when all complete. */
|
||||
async approveStep(
|
||||
contractId: string,
|
||||
|
||||
@@ -60,6 +60,7 @@ import { AcceptContractDto } from './dto/accept-contract.dto';
|
||||
import {
|
||||
ApproveStepDto,
|
||||
RejectContractDto,
|
||||
RejectStepDto,
|
||||
RequestChangesDto,
|
||||
} from './dto/approve-step.dto';
|
||||
import { SignContractDto } from './dto/sign-contract.dto';
|
||||
@@ -390,6 +391,27 @@ export class ContractsController {
|
||||
);
|
||||
}
|
||||
|
||||
@Post(':id/approval-steps/:stepId/reject')
|
||||
@BookingStaff([
|
||||
FREIGHT_PERMS.contracts.approveLineStaff,
|
||||
FREIGHT_PERMS.contracts.approveDirector,
|
||||
FREIGHT_PERMS.contracts.approveCeo,
|
||||
])
|
||||
@ApiOperation({ summary: 'Reject one approval step (terminal → REJECTED)' })
|
||||
rejectStep(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Param('stepId', ParseUUIDPipe) stepId: string,
|
||||
@Body() dto: RejectStepDto,
|
||||
@CurrentUser() user: AuthUserPayload,
|
||||
) {
|
||||
return this.transitionService.rejectStep(
|
||||
id,
|
||||
stepId,
|
||||
resolveAuthUserId(user),
|
||||
dto.reason,
|
||||
);
|
||||
}
|
||||
|
||||
@Post(':id/contract/generate')
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.generateContract)
|
||||
@ApiOperation({ summary: 'Generate contract document → CONTRACT_READY' })
|
||||
|
||||
@@ -117,6 +117,18 @@ export function deriveContractCustomerAction(
|
||||
};
|
||||
}
|
||||
|
||||
// Saved-but-not-submitted contract — send the customer back into the wizard to
|
||||
// finish editing and submit it for review.
|
||||
if (contract.status === "DRAFT" || contract.status === "RENEWAL_DRAFT") {
|
||||
return {
|
||||
type: "navigate",
|
||||
label: "Continue draft",
|
||||
to: `/contracts/${id}/edit`,
|
||||
primary: true,
|
||||
icon: PencilLine,
|
||||
};
|
||||
}
|
||||
|
||||
const payable = findPayableBookingForContract(id, bookings);
|
||||
if (payable) {
|
||||
return {
|
||||
|
||||
@@ -73,10 +73,6 @@ import {
|
||||
MUTED,
|
||||
} from "./contract-ui";
|
||||
|
||||
// Statuses where a customer may create a shipment booking themselves. Reached
|
||||
// only after self-clearance is approved by Operations (Path A) or, for DOMESTIC,
|
||||
// directly at counter-sign.
|
||||
const PATH_A_BOOKABLE = ["FULLY_EXECUTED", "CONTRACT_ACTIVE"];
|
||||
// Statuses where the customer uploads clearance documents on the contract. Used
|
||||
// by both paths: Path B (customs, GL-reviewed) and Path A self-clearance
|
||||
// (non-customs IMPORT/EXPORT, Operations-reviewed).
|
||||
@@ -263,10 +259,14 @@ export default function ContractDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Staff returned the contract for changes — send the customer to the full edit
|
||||
// wizard (edit any term + replace documents → resubmit) rather than the
|
||||
// read-only detail.
|
||||
if (contract.status === "CHANGES_REQUESTED") {
|
||||
// Not yet submitted (customer saved a draft) or staff returned the contract for
|
||||
// changes — send the customer to the full edit wizard (edit any term + replace
|
||||
// documents → submit) rather than the read-only detail.
|
||||
if (
|
||||
contract.status === "DRAFT" ||
|
||||
contract.status === "RENEWAL_DRAFT" ||
|
||||
contract.status === "CHANGES_REQUESTED"
|
||||
) {
|
||||
return <Navigate to={`/contracts/${contract.id}/edit`} replace />;
|
||||
}
|
||||
|
||||
@@ -305,9 +305,13 @@ export default function ContractDetailPage() {
|
||||
const clearanceFinalized =
|
||||
contract.clearanceStatus === "CLEARANCE_READY_FOR_BOOKING" ||
|
||||
contract.status === "CLEARANCE_READY_FOR_BOOKING";
|
||||
const canBookShipment =
|
||||
!customsPath && PATH_A_BOOKABLE.includes(contract.status);
|
||||
const bookingAction = getContractBookingAction(contract, contractBookings);
|
||||
// Whether the customer may open a new self-service booking. Derived from the
|
||||
// shared booking-action helper so it honours the ONE_TIME single-slot rule:
|
||||
// once a non-terminal booking exists on a ONE_TIME contract there is no free
|
||||
// slot, so the action is "none" and no booking button is shown.
|
||||
const canBookShipment =
|
||||
bookingAction.kind === "book" || bookingAction.kind === "rebook";
|
||||
const canRequestShipment = bookingAction.kind === "request";
|
||||
// Customs + clearance finalized: GL is preparing the booking — surface a
|
||||
// status notice instead of any action.
|
||||
|
||||
@@ -81,9 +81,10 @@ type PriceModalMode = "submit" | "draft";
|
||||
|
||||
/**
|
||||
* The contract wizard, used both to create a new contract and — in `edit` mode —
|
||||
* to edit & resubmit a contract staff returned with CHANGES_REQUESTED. Edit mode
|
||||
* hydrates the form from the saved contract, lets the customer change any term
|
||||
* and replace documents, then runs the same update → price → submit flow.
|
||||
* to continue an unsubmitted DRAFT or edit & resubmit a contract staff returned
|
||||
* with CHANGES_REQUESTED. Edit mode hydrates the form from the saved contract,
|
||||
* lets the customer change any term and replace documents, then runs the same
|
||||
* update → price → submit flow.
|
||||
*/
|
||||
export default function NewContractPage({
|
||||
mode = "create",
|
||||
|
||||
Reference in New Issue
Block a user