Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/features/orders/components/ApprovalBanner.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { Button } from '../../../components/Button'
import { getErrorMessage } from '../../../utils/apiErrorMessage.js'
import { getOrdersErrorMessage } from '../ordersErrorMessage.js'
import { formatUnitsString } from '../../../utils/format.js'
import { useApproveOrder } from '../hooks/useApproveOrder.js'
import './ApprovalBanner.css'
Expand All @@ -26,7 +26,7 @@ export function ApprovalBanner({ order, onApproved }) {
</p>
{status === 'error' && (
<p className="tu-approval-banner__error" role="alert">
{getErrorMessage(error)}
{getOrdersErrorMessage(error)}
</p>
)}
<div className="tu-approval-banner__actions">
Expand Down
28 changes: 25 additions & 3 deletions frontend/src/features/orders/components/OrderForm.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
/* One column below 768px; Security+Side and Quantity+Reference price pair up into a 2-column
grid at 768px and above, matching FeesPage.css/FundingPage.css's established breakpoint
convention in this codebase (a literal px value, since --breakpoint-md can't be read inside a
media query). The preview, error, and submit button each span the full row regardless of
column count -- pairing a number with its own row of controls reads worse than a clean divider
between input fields and the outcome/action below them. */
.tu-order-form {
display: flex;
flex-direction: column;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--space-4);
max-width: 360px;
}

@media (min-width: 768px) {
.tu-order-form {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

.tu-order-form__preview,
.tu-order-form__error,
.tu-order-form__submit {
grid-column: 1 / -1;
}

.tu-order-form__preview {
Expand Down Expand Up @@ -37,3 +54,8 @@
color: var(--color-error);
font-size: var(--font-size-caption);
}

.tu-order-form__empty {
grid-column: 1 / -1;
color: var(--color-text-secondary);
}
49 changes: 31 additions & 18 deletions frontend/src/features/orders/components/OrderForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { Select } from '../../../components/Select'
import { Skeleton } from '../../../components/Skeleton'
import { useToast } from '../../../components/Toast'
import { useSecurities } from '../../portfolio/hooks/useSecurities.js'
import { getErrorMessage } from '../../../utils/apiErrorMessage.js'
import { formatMoney } from '../../../utils/format.js'
import { getOrdersErrorMessage } from '../ordersErrorMessage.js'
import { usePlaceOrder } from '../hooks/usePlaceOrder.js'
import { parseQuantity } from '../parseQuantity.js'
import './OrderForm.css'
Expand All @@ -24,14 +24,19 @@ export function OrderForm() {
const [side, setSide] = useState('buy')
const [quantity, setQuantity] = useState('')
const [referencePrice, setReferencePrice] = useState('')
const [validationMessage, setValidationMessage] = useState(null)
const [quantityError, setQuantityError] = useState(null)
const [referencePriceError, setReferencePriceError] = useState(null)

if (securitiesStatus === 'idle' || securitiesStatus === 'loading') {
return <Skeleton height="200px" width="360px" />
}

if (securitiesStatus === 'error') {
return <ErrorState description={getErrorMessage(securitiesError)} onRetry={refetchSecurities} />
return <ErrorState description={getOrdersErrorMessage(securitiesError)} onRetry={refetchSecurities} />
}

if (securities.length === 0) {
return <p className="tu-order-form__empty">No tradable securities are available right now.</p>
}

const selectedSecurity = securities.find((security) => security.security_id === securityId) || securities[0]
Expand All @@ -50,26 +55,26 @@ export function OrderForm() {

const handleSubmit = (event) => {
event.preventDefault()
setValidationMessage(null)
setQuantityError(null)
setReferencePriceError(null)

const parsedQuantity = parseQuantity(quantity)
if (!parsedQuantity) {
setValidationMessage('Enter a valid quantity greater than zero.')
setQuantityError('Enter a valid quantity greater than zero.')
return
}
if (!selectedSecurity) {
setValidationMessage('Choose a security.')
return
}
if (!referencePrice || Number.isNaN(Number(referencePrice)) || Number(referencePrice) <= 0) {
setValidationMessage('Enter a valid reference price greater than zero.')

const trimmedReferencePrice = referencePrice.trim()
if (!trimmedReferencePrice || Number.isNaN(Number(trimmedReferencePrice)) || Number(trimmedReferencePrice) <= 0) {
setReferencePriceError('Enter a valid reference price greater than zero.')
return
}

placeOrder({
securityId: selectedSecurity.security_id,
side,
quantity: parsedQuantity,
referencePrice,
referencePrice: trimmedReferencePrice,
})
.then((order) => {
showToast({ message: 'Order placed.', tone: 'success' })
Expand All @@ -78,18 +83,24 @@ export function OrderForm() {
.catch(() => {})
}

const displayError = validationMessage || (status === 'error' ? getErrorMessage(error) : null)
const submitError = status === 'error' ? getOrdersErrorMessage(error) : null

return (
<form className="tu-order-form" onSubmit={handleSubmit}>
<Select label="Security" name="security" value={selectedSecurity?.security_id} onChange={(event) => setSecurityId(event.target.value)}>
<Select
label="Security"
name="security"
required
value={selectedSecurity?.security_id}
onChange={(event) => setSecurityId(event.target.value)}
>
{securities.map((security) => (
<option key={security.security_id} value={security.security_id}>
{security.symbol}
</option>
))}
</Select>
<Select label="Side" name="side" value={side} onChange={(event) => setSide(event.target.value)}>
<Select label="Side" name="side" required value={side} onChange={(event) => setSide(event.target.value)}>
<option value="buy">Buy</option>
<option value="sell">Sell</option>
</Select>
Expand All @@ -100,6 +111,7 @@ export function OrderForm() {
value={quantity}
onChange={(event) => setQuantity(event.target.value)}
placeholder="0"
error={quantityError}
required
/>
<Input
Expand All @@ -109,6 +121,7 @@ export function OrderForm() {
value={referencePrice}
onChange={(event) => setReferencePrice(event.target.value)}
placeholder="0.00"
error={referencePriceError}
required
/>
{notional && (
Expand All @@ -117,12 +130,12 @@ export function OrderForm() {
<span className="tu-order-form__preview-value">{formatMoney(notional)}</span>
</div>
)}
{displayError && (
{submitError && (
<p className="tu-order-form__error" role="alert">
{displayError}
{submitError}
</p>
)}
<Button type="submit" loading={isSubmitting} disabled={isSubmitting}>
<Button type="submit" className="tu-order-form__submit" loading={isSubmitting} disabled={isSubmitting}>
Place order
</Button>
</form>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/features/orders/components/OrderList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { EmptyState } from '../../../components/EmptyState'
import { ErrorState } from '../../../components/ErrorState'
import { Skeleton } from '../../../components/Skeleton'
import { Table } from '../../../components/Table'
import { getErrorMessage } from '../../../utils/apiErrorMessage.js'
import { useOrders } from '../hooks/useOrders.js'
import { getOrdersErrorMessage } from '../ordersErrorMessage.js'
import { OrderRow } from './OrderRow.jsx'

export function OrderList() {
Expand All @@ -23,7 +23,7 @@ export function OrderList() {
}

if (status === 'error') {
return <ErrorState description={getErrorMessage(error)} onRetry={refetch} />
return <ErrorState description={getOrdersErrorMessage(error)} onRetry={refetch} />
}

if (orders.length === 0) {
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/features/orders/ordersErrorMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getErrorMessage } from '../../utils/apiErrorMessage.js'

// order_service.py / orders.py error codes -- every real rejection reason a POST /orders or
// POST /orders/<id>/approve can return, so a real cause is never swallowed into the generic
// fallback (which is what "some error occurred" looked like before this map existed).
const SPECIFIC_MESSAGES = {
customer_not_eligible: "Your account isn't eligible to place orders yet.",
kyc_verification_pending: 'Your identity verification is still pending — you can place orders once it completes.',
kyc_verification_rejected: 'Your identity verification was not approved. Contact support to resolve this.',
account_approval_pending: 'Your account is still pending approval — you can place orders once it completes.',
account_approval_rejected: 'Your account was not approved. Contact support to resolve this.',
insufficient_investable_cash: "You don't have enough investable cash for this order.",
}

export function getOrdersErrorMessage(error) {
if (!error) return null
if (SPECIFIC_MESSAGES[error.code]) return SPECIFIC_MESSAGES[error.code]
return getErrorMessage(error)
}
7 changes: 7 additions & 0 deletions frontend/src/pages/OrderNewPage.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* The order form is four short fields, not a reading-length document -- the page's own
--reading-max-width (640px) gives it real room for a 2-column field grid (OrderForm.css)
without stretching to the full --content-max-width, which would leave the fields looking lost
in a mostly-empty row on a wide viewport. */
.tu-page.tu-order-new-page {
max-width: var(--reading-max-width);
}
8 changes: 6 additions & 2 deletions frontend/src/pages/OrderNewPage.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Card } from '../components/Card'
import { OrderForm } from '../features/orders/components/OrderForm.jsx'
import './OrderNewPage.css'
import './PageLayout.css'

export function OrderNewPage() {
return (
<div className="tu-page">
<div className="tu-page tu-order-new-page">
<h1 className="tu-page__title">Place an order</h1>
<OrderForm />
<Card>
<OrderForm />
</Card>
</div>
)
}
Loading