2026-03-14 23:03:17 -05:00
import type { InventoryItemOptionDto } from "@mrp/shared/dist/inventory/types.js" ;
import type { SalesCustomerOptionDto , SalesDocumentDetailDto , SalesDocumentInput , SalesLineInput } from "@mrp/shared/dist/sales/types.js" ;
import { useEffect , useState } from "react" ;
import { Link , useNavigate , useParams } from "react-router-dom" ;
2026-03-15 19:40:35 -05:00
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog" ;
2026-03-14 23:03:17 -05:00
import { useAuth } from "../../auth/AuthProvider" ;
import { api , ApiError } from "../../lib/api" ;
import { inventoryUnitOptions } from "../inventory/config" ;
import { emptySalesDocumentInput , salesConfigs , salesStatusOptions , type SalesDocumentEntity } from "./config" ;
export function SalesFormPage ( { entity , mode } : { entity : SalesDocumentEntity ; mode : "create" | "edit" } ) {
const { token } = useAuth ( ) ;
const navigate = useNavigate ( ) ;
const { quoteId , orderId } = useParams ( ) ;
const documentId = entity === "quote" ? quoteId : orderId ;
const config = salesConfigs [ entity ] ;
const [ form , setForm ] = useState < SalesDocumentInput > ( emptySalesDocumentInput ) ;
const [ status , setStatus ] = useState ( mode === "create" ? ` Create a new ${ config . singularLabel . toLowerCase ( ) } . ` : ` Loading ${ config . singularLabel . toLowerCase ( ) } ... ` ) ;
const [ customers , setCustomers ] = useState < SalesCustomerOptionDto [ ] > ( [ ] ) ;
2026-03-14 23:07:43 -05:00
const [ customerSearchTerm , setCustomerSearchTerm ] = useState ( "" ) ;
const [ customerPickerOpen , setCustomerPickerOpen ] = useState ( false ) ;
2026-03-14 23:03:17 -05:00
const [ itemOptions , setItemOptions ] = useState < InventoryItemOptionDto [ ] > ( [ ] ) ;
const [ lineSearchTerms , setLineSearchTerms ] = useState < string [ ] > ( [ ] ) ;
const [ activeLinePicker , setActiveLinePicker ] = useState < number | null > ( null ) ;
const [ isSaving , setIsSaving ] = useState ( false ) ;
2026-03-15 19:40:35 -05:00
const [ pendingLineRemovalIndex , setPendingLineRemovalIndex ] = useState < number | null > ( null ) ;
2026-03-14 23:03:17 -05:00
2026-03-14 23:39:51 -05:00
const subtotal = form . lines . reduce ( ( sum , line ) = > sum + line . quantity * line . unitPrice , 0 ) ;
const discountAmount = subtotal * ( form . discountPercent / 100 ) ;
const taxableSubtotal = subtotal - discountAmount ;
const taxAmount = taxableSubtotal * ( form . taxPercent / 100 ) ;
const total = taxableSubtotal + taxAmount + form . freightAmount ;
2026-03-14 23:03:17 -05:00
useEffect ( ( ) = > {
if ( ! token ) {
return ;
}
api . getSalesCustomers ( token ) . then ( setCustomers ) . catch ( ( ) = > setCustomers ( [ ] ) ) ;
api . getInventoryItemOptions ( token ) . then ( setItemOptions ) . catch ( ( ) = > setItemOptions ( [ ] ) ) ;
} , [ token ] ) ;
useEffect ( ( ) = > {
if ( ! token || mode !== "edit" || ! documentId ) {
return ;
}
const loader = entity === "quote" ? api . getQuote ( token , documentId ) : api . getSalesOrder ( token , documentId ) ;
loader
. then ( ( document ) = > {
setForm ( {
customerId : document.customerId ,
status : document.status ,
issueDate : document.issueDate ,
expiresAt : entity === "quote" ? document . expiresAt : null ,
2026-03-14 23:39:51 -05:00
discountPercent : document.discountPercent ,
taxPercent : document.taxPercent ,
freightAmount : document.freightAmount ,
2026-03-14 23:03:17 -05:00
notes : document.notes ,
2026-03-15 11:44:14 -05:00
revisionReason : "" ,
2026-03-14 23:03:17 -05:00
lines : document.lines.map ( ( line ) = > ( {
itemId : line.itemId ,
description : line.description ,
quantity : line.quantity ,
unitOfMeasure : line.unitOfMeasure ,
unitPrice : line.unitPrice ,
position : line.position ,
} ) ) ,
} ) ;
2026-03-14 23:07:43 -05:00
setCustomerSearchTerm ( document . customerName ) ;
2026-03-14 23:03:17 -05:00
setLineSearchTerms ( document . lines . map ( ( line : SalesDocumentDetailDto [ "lines" ] [ number ] ) = > line . itemSku ) ) ;
setStatus ( ` ${ config . singularLabel } loaded. ` ) ;
} )
. catch ( ( error : unknown ) = > {
const message = error instanceof ApiError ? error . message : ` Unable to load ${ config . singularLabel . toLowerCase ( ) } . ` ;
setStatus ( message ) ;
} ) ;
} , [ config . singularLabel , documentId , entity , mode , token ] ) ;
function updateField < Key extends keyof SalesDocumentInput > ( key : Key , value : SalesDocumentInput [ Key ] ) {
setForm ( ( current : SalesDocumentInput ) = > ( { . . . current , [ key ] : value } ) ) ;
}
2026-03-14 23:07:43 -05:00
function getSelectedCustomerName ( customerId : string ) {
return customers . find ( ( customer ) = > customer . id === customerId ) ? . name ? ? "" ;
}
2026-03-14 23:39:51 -05:00
function getSelectedCustomer ( customerId : string ) {
return customers . find ( ( customer ) = > customer . id === customerId ) ? ? null ;
}
2026-03-14 23:03:17 -05:00
function updateLine ( index : number , nextLine : SalesLineInput ) {
setForm ( ( current : SalesDocumentInput ) = > ( {
. . . current ,
lines : current.lines.map ( ( line : SalesLineInput , lineIndex : number ) = > ( lineIndex === index ? nextLine : line ) ) ,
} ) ) ;
}
function updateLineSearchTerm ( index : number , value : string ) {
setLineSearchTerms ( ( current : string [ ] ) = > {
const next = [ . . . current ] ;
next [ index ] = value ;
return next ;
} ) ;
}
function addLine() {
setForm ( ( current : SalesDocumentInput ) = > ( {
. . . current ,
lines : [
. . . current . lines ,
{
itemId : "" ,
description : "" ,
quantity : 1 ,
unitOfMeasure : "EA" ,
unitPrice : 0 ,
position : current.lines.length === 0 ? 10 : Math.max ( . . . current . lines . map ( ( line : SalesLineInput ) = > line . position ) ) + 10 ,
} ,
] ,
} ) ) ;
setLineSearchTerms ( ( current : string [ ] ) = > [ . . . current , "" ] ) ;
}
function removeLine ( index : number ) {
setForm ( ( current : SalesDocumentInput ) = > ( {
. . . current ,
lines : current.lines.filter ( ( _line : SalesLineInput , lineIndex : number ) = > lineIndex !== index ) ,
} ) ) ;
setLineSearchTerms ( ( current : string [ ] ) = > current . filter ( ( _term : string , termIndex : number ) = > termIndex !== index ) ) ;
}
2026-03-15 19:40:35 -05:00
const pendingLineRemoval =
pendingLineRemovalIndex != null
? {
index : pendingLineRemovalIndex ,
line : form.lines [ pendingLineRemovalIndex ] ,
sku : lineSearchTerms [ pendingLineRemovalIndex ] ? ? "" ,
}
: null ;
2026-03-14 23:03:17 -05:00
async function handleSubmit ( event : React.FormEvent < HTMLFormElement > ) {
event . preventDefault ( ) ;
if ( ! token ) {
return ;
}
setIsSaving ( true ) ;
setStatus ( ` Saving ${ config . singularLabel . toLowerCase ( ) } ... ` ) ;
try {
const saved =
entity === "quote"
? mode === "create"
? await api . createQuote ( token , form )
: await api . updateQuote ( token , documentId ? ? "" , form )
: mode === "create"
? await api . createSalesOrder ( token , { . . . form , expiresAt : null } )
: await api . updateSalesOrder ( token , documentId ? ? "" , { . . . form , expiresAt : null } ) ;
navigate ( ` ${ config . routeBase } / ${ saved . id } ` ) ;
} catch ( error : unknown ) {
const message = error instanceof ApiError ? error . message : ` Unable to save ${ config . singularLabel . toLowerCase ( ) } . ` ;
setStatus ( message ) ;
setIsSaving ( false ) ;
}
}
return (
< form className = "space-y-6" onSubmit = { handleSubmit } >
< section className = "rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
< div className = "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between" >
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > { config . detailEyebrow } Editor < / p >
< h3 className = "mt-2 text-xl font-bold text-text" > { mode === "create" ? ` New ${ config . singularLabel } ` : ` Edit ${ config . singularLabel } ` } < / h3 >
< / div >
< Link to = { mode === "create" ? config . routeBase : ` ${ config . routeBase } / ${ documentId } ` } className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" >
Cancel
< / Link >
< / div >
< / section >
< section className = "space-y-4 rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
< div className = "grid gap-3 xl:grid-cols-4" >
< label className = "block xl:col-span-2" >
< span className = "mb-2 block text-sm font-semibold text-text" > Customer < / span >
2026-03-14 23:07:43 -05:00
< div className = "relative" >
< input
value = { customerSearchTerm }
onChange = { ( event ) = > {
setCustomerSearchTerm ( event . target . value ) ;
updateField ( "customerId" , "" ) ;
setCustomerPickerOpen ( true ) ;
} }
onFocus = { ( ) = > setCustomerPickerOpen ( true ) }
onBlur = { ( ) = > {
window . setTimeout ( ( ) = > {
setCustomerPickerOpen ( false ) ;
if ( form . customerId ) {
setCustomerSearchTerm ( getSelectedCustomerName ( form . customerId ) ) ;
}
} , 120 ) ;
} }
placeholder = "Search customer"
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/ >
{ customerPickerOpen ? (
< div className = "absolute z-20 mt-2 max-h-64 w-full overflow-y-auto rounded-2xl border border-line/70 bg-surface shadow-panel" >
{ customers
. filter ( ( customer ) = > {
const query = customerSearchTerm . trim ( ) . toLowerCase ( ) ;
if ( ! query ) {
return true ;
}
return (
customer . name . toLowerCase ( ) . includes ( query ) ||
customer . email . toLowerCase ( ) . includes ( query )
) ;
} )
. slice ( 0 , 12 )
. map ( ( customer ) = > (
< button
key = { customer . id }
type = "button"
onMouseDown = { ( event ) = > {
event . preventDefault ( ) ;
updateField ( "customerId" , customer . id ) ;
2026-03-14 23:39:51 -05:00
updateField ( "discountPercent" , customer . resellerDiscountPercent ) ;
2026-03-14 23:07:43 -05:00
setCustomerSearchTerm ( customer . name ) ;
setCustomerPickerOpen ( false ) ;
} }
className = "block w-full border-b border-line/50 px-4 py-2 text-left text-sm transition last:border-b-0 hover:bg-page/70"
>
< div className = "font-semibold text-text" > { customer . name } < / div >
< div className = "mt-1 text-xs text-muted" > { customer . email } < / div >
< / button >
) ) }
{ customers . filter ( ( customer ) = > {
const query = customerSearchTerm . trim ( ) . toLowerCase ( ) ;
if ( ! query ) {
return true ;
}
return customer . name . toLowerCase ( ) . includes ( query ) || customer . email . toLowerCase ( ) . includes ( query ) ;
} ) . length === 0 ? (
< div className = "px-2 py-2 text-sm text-muted" > No matching customers found . < / div >
) : null }
< / div >
) : null }
< / div >
< div className = "mt-2 min-h-5 text-xs text-muted" >
{ form . customerId ? getSelectedCustomerName ( form . customerId ) : "No customer selected" }
< / div >
2026-03-14 23:39:51 -05:00
{ form . customerId ? (
< div className = "mt-1 text-xs text-muted" >
Default reseller discount : { getSelectedCustomer ( form . customerId ) ? . resellerDiscountPercent . toFixed ( 2 ) ? ? "0.00" } %
< / div >
) : null }
2026-03-14 23:03:17 -05:00
< / label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Status < / span >
< select
value = { form . status }
onChange = { ( event ) = > updateField ( "status" , event . target . value as SalesDocumentInput [ "status" ] ) }
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
>
{ salesStatusOptions . map ( ( option ) = > (
< option key = { option . value } value = { option . value } >
{ option . label }
< / option >
) ) }
< / select >
< / label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Issue date < / span >
< input
type = "date"
value = { form . issueDate . slice ( 0 , 10 ) }
onChange = { ( event ) = > updateField ( "issueDate" , new Date ( event . target . value ) . toISOString ( ) ) }
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/ >
< / label >
< / div >
{ entity === "quote" ? (
< label className = "block xl:max-w-sm" >
< span className = "mb-2 block text-sm font-semibold text-text" > Expiration date < / span >
< input
type = "date"
value = { form . expiresAt ? form . expiresAt . slice ( 0 , 10 ) : "" }
onChange = { ( event ) = > updateField ( "expiresAt" , event . target . value ? new Date ( event . target . value ) . toISOString ( ) : null ) }
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/ >
< / label >
) : null }
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Notes < / span >
< textarea
value = { form . notes }
onChange = { ( event ) = > updateField ( "notes" , event . target . value ) }
rows = { 3 }
className = "w-full rounded-3xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/ >
< / label >
2026-03-15 11:44:14 -05:00
{ mode === "edit" ? (
< label className = "block xl:max-w-xl" >
< span className = "mb-2 block text-sm font-semibold text-text" > Revision Reason < / span >
< input
value = { form . revisionReason ? ? "" }
onChange = { ( event ) = > updateField ( "revisionReason" , event . target . value ) }
placeholder = "What changed in this revision?"
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/ >
< / label >
) : null }
2026-03-14 23:39:51 -05:00
< div className = "grid gap-3 xl:grid-cols-3" >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Discount % < / span >
< input
type = "number"
min = { 0 }
max = { 100 }
step = { 0.01 }
value = { form . discountPercent }
onChange = { ( event ) = > updateField ( "discountPercent" , Number ( event . target . value ) || 0 ) }
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/ >
< / label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Tax % < / span >
< input
type = "number"
min = { 0 }
max = { 100 }
step = { 0.01 }
value = { form . taxPercent }
onChange = { ( event ) = > updateField ( "taxPercent" , Number ( event . target . value ) || 0 ) }
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/ >
< / label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Freight < / span >
< input
type = "number"
min = { 0 }
step = { 0.01 }
value = { form . freightAmount }
onChange = { ( event ) = > updateField ( "freightAmount" , Number ( event . target . value ) || 0 ) }
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/ >
< / label >
< / div >
2026-03-14 23:03:17 -05:00
< / section >
< section className = "rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
< div className = "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between" >
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Line Items < / p >
< h4 className = "mt-2 text-lg font-bold text-text" > Commercial lines < / h4 >
< / div >
< button type = "button" onClick = { addLine } className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" >
Add line
< / button >
< / div >
{ form . lines . length === 0 ? (
< div className = "mt-5 rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" >
No line items added yet .
< / div >
) : (
< div className = "mt-5 space-y-4" >
{ form . lines . map ( ( line : SalesLineInput , index : number ) = > (
< div key = { index } className = "rounded-3xl border border-line/70 bg-page/60 p-3" >
2026-03-14 23:39:51 -05:00
< div className = "grid gap-3 xl:grid-cols-[1.15fr_1.25fr_0.5fr_0.55fr_0.7fr_0.75fr_auto]" >
2026-03-14 23:03:17 -05:00
< label className = "block" >
< span className = "mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted" > SKU < / span >
< div className = "relative" >
< input
value = { lineSearchTerms [ index ] ? ? "" }
onChange = { ( event ) = > {
updateLineSearchTerm ( index , event . target . value ) ;
updateLine ( index , { . . . line , itemId : "" } ) ;
setActiveLinePicker ( index ) ;
} }
onFocus = { ( ) = > setActiveLinePicker ( index ) }
onBlur = { ( ) = > window . setTimeout ( ( ) = > setActiveLinePicker ( ( current ) = > ( current === index ? null : current ) ) , 120 ) }
placeholder = "Search by SKU"
className = "w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
/ >
{ activeLinePicker === index ? (
< div className = "absolute z-20 mt-2 max-h-64 w-full overflow-y-auto rounded-2xl border border-line/70 bg-surface shadow-panel" >
{ itemOptions
. filter ( ( option ) = > option . sku . toLowerCase ( ) . includes ( ( lineSearchTerms [ index ] ? ? "" ) . trim ( ) . toLowerCase ( ) ) )
. slice ( 0 , 12 )
. map ( ( option ) = > (
< button
key = { option . id }
type = "button"
onMouseDown = { ( event ) = > {
event . preventDefault ( ) ;
updateLine ( index , {
. . . line ,
itemId : option.id ,
description : line.description || option . name ,
2026-03-14 23:23:43 -05:00
unitPrice : line.unitPrice > 0 ? line . unitPrice : ( option . defaultPrice ? ? 0 ) ,
2026-03-14 23:03:17 -05:00
} ) ;
updateLineSearchTerm ( index , option . sku ) ;
setActiveLinePicker ( null ) ;
} }
className = "block w-full border-b border-line/50 px-4 py-2 text-left text-sm font-semibold text-text transition last:border-b-0 hover:bg-page/70"
>
{ option . sku }
< / button >
) ) }
< / div >
) : null }
< / div >
< / label >
< label className = "block" >
< span className = "mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Description < / span >
< input value = { line . description } onChange = { ( event ) = > updateLine ( index , { . . . line , description : event.target.value } ) } className = "w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" / >
< / label >
< label className = "block" >
< span className = "mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Qty < / span >
< input type = "number" min = { 1 } step = { 1 } value = { line . quantity } onChange = { ( event ) = > updateLine ( index , { . . . line , quantity : Number.parseInt ( event . target . value , 10 ) || 0 } ) } className = "w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" / >
< / label >
< label className = "block" >
< span className = "mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted" > UOM < / span >
< select value = { line . unitOfMeasure } onChange = { ( event ) = > updateLine ( index , { . . . line , unitOfMeasure : event.target.value as SalesLineInput [ "unitOfMeasure" ] } ) } className = "w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" >
{ inventoryUnitOptions . map ( ( option ) = > (
< option key = { option . value } value = { option . value } >
{ option . label }
< / option >
) ) }
< / select >
< / label >
< label className = "block" >
< span className = "mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Unit Price < / span >
< input type = "number" min = { 0 } step = { 0.01 } value = { line . unitPrice } onChange = { ( event ) = > updateLine ( index , { . . . line , unitPrice : Number ( event . target . value ) } ) } className = "w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" / >
< / label >
2026-03-14 23:39:51 -05:00
< div className = "flex items-end" >
< div className = "w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-sm text-text" >
$ { ( line . quantity * line . unitPrice ) . toFixed ( 2 ) }
< / div >
< / div >
2026-03-14 23:03:17 -05:00
< div className = "flex items-end" >
2026-03-15 19:40:35 -05:00
< button type = "button" onClick = { ( ) = > setPendingLineRemovalIndex ( index ) } className = "rounded-2xl border border-rose-400/40 px-2 py-2 text-sm font-semibold text-rose-700 dark:text-rose-300" >
2026-03-14 23:03:17 -05:00
Remove
< / button >
< / div >
< / div >
< / div >
) ) }
< / div >
) }
2026-03-14 23:39:51 -05:00
< div className = "mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-4" >
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm" >
< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Subtotal < / div >
< div className = "mt-1 font-semibold text-text" > $ { subtotal . toFixed ( 2 ) } < / div >
< / div >
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm" >
< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Discount < / div >
< div className = "mt-1 font-semibold text-text" > - $ { discountAmount . toFixed ( 2 ) } < / div >
< / div >
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm" >
< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Tax + Freight < / div >
< div className = "mt-1 font-semibold text-text" > $ { ( taxAmount + form . freightAmount ) . toFixed ( 2 ) } < / div >
< / div >
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm" >
< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Total < / div >
< div className = "mt-1 font-semibold text-text" > $ { total . toFixed ( 2 ) } < / div >
< / div >
< / div >
2026-03-14 23:03:17 -05:00
< div className = "mt-6 flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between" >
< span className = "min-w-0 text-sm text-muted" > { status } < / span >
< button type = "submit" disabled = { isSaving } className = "rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60" >
{ isSaving ? "Saving..." : mode === "create" ? ` Create ${ config . singularLabel . toLowerCase ( ) } ` : "Save changes" }
< / button >
< / div >
< / section >
2026-03-15 19:40:35 -05:00
< ConfirmActionDialog
open = { pendingLineRemoval != null }
title = { ` Remove ${ config . singularLabel . toLowerCase ( ) } line ` }
description = {
pendingLineRemoval
? ` Remove ${ pendingLineRemoval . sku || pendingLineRemoval . line ? . description || "this line" } from the ${ config . singularLabel . toLowerCase ( ) } . `
: "Remove this line."
}
impact = "The line will be dropped from the document draft immediately and totals will recalculate."
recovery = "Add the line back manually before saving if this removal was a mistake."
confirmLabel = "Remove line"
isConfirming = { false }
onClose = { ( ) = > setPendingLineRemovalIndex ( null ) }
onConfirm = { ( ) = > {
if ( pendingLineRemoval ) {
removeLine ( pendingLineRemoval . index ) ;
}
setPendingLineRemovalIndex ( null ) ;
} }
/ >
2026-03-14 23:03:17 -05:00
< / form >
) ;
}