@@ -3,6 +3,7 @@ import { toast } from "react-hot-toast";
33import { AdvancedBaseTableHandle } from "../baseTable/AdvancedBaseTable" ;
44import { AllocationTableItem } from "../allocationTable/types" ;
55import Chip from "./Chip" ;
6+ import { useState } from "react" ;
67
78export interface PartnerDistributionSummary {
89 id : number ;
@@ -92,6 +93,10 @@ function LineItemChip({
9293 isInteractionMode ?: boolean ;
9394 readOnly ?: boolean ;
9495} ) {
96+ const [ isSplitting , setIsSplitting ] = useState ( false ) ;
97+ const [ isSplitLoading , setIsSplitLoading ] = useState ( false ) ;
98+ const [ numSplits , setNumSplits ] = useState ( "" ) ;
99+ const [ splitQuantities , setSplitQuantities ] = useState < string [ ] > ( [ ] ) ;
95100 const { apiClient } = useApiClient ( ) ;
96101
97102 function updateLineItemInTable ( updatedItem : Partial < typeof item > ) {
@@ -103,6 +108,44 @@ function LineItemChip({
103108 } ) ) ;
104109 }
105110
111+ async function splitItem ( ) {
112+ if ( isSplitLoading ) return ;
113+ setIsSplitLoading ( true ) ;
114+ try {
115+ const quantities = splitQuantities . map ( Number ) ;
116+ const response = await apiClient . post < {
117+ original : { id : number ; quantity : number } ;
118+ newItem : ( typeof item ) [ ] ;
119+ } > (
120+ `/api/generalItems/${ generalItemId } /lineItems/${ item . id } ` ,
121+ { body : JSON . stringify ( { quantities } ) }
122+ ) ;
123+
124+ updateItem ( generalItemId , ( prev ) => ( {
125+ ...prev ,
126+ items : prev . items
127+ . map ( ( it ) =>
128+ it . id === item . id
129+ ? { ...it , quantity : response . original . quantity }
130+ : it
131+ )
132+ . concat ( response . newItem . map ( ( newItem ) => ( {
133+ ...newItem ,
134+ allocation : null ,
135+ } ) ) )
136+ } ) ) ;
137+
138+ toast . success ( "Line item split successfully!" ) ;
139+ setIsSplitting ( false ) ;
140+ setNumSplits ( "" ) ;
141+ setSplitQuantities ( [ ] ) ;
142+ } catch ( error ) {
143+ toast . error ( ( error as Error ) . message || "Failed to split line item." ) ;
144+ } finally {
145+ setIsSplitLoading ( false ) ;
146+ }
147+ }
148+
106149 async function unallocateItem ( ) {
107150 if ( isInteractionMode ) {
108151 updateLineItemInTable ( {
@@ -275,6 +318,96 @@ function LineItemChip({
275318 </ button >
276319 ) ) }
277320 </ div >
321+ { ! item . allocation && ! isInteractionMode && item . quantity > 1 && (
322+ < div className = "border-t border-gray-primary/20 mt-2 pt-2" >
323+ { ! isSplitting ? (
324+ < button
325+ onClick = { ( ) => setIsSplitting ( true ) }
326+ className = "w-full text-center px-2 py-1 hover:bg-blue-primary/20 border border-blue-primary rounded transition-all duration-200"
327+ >
328+ Split by Quantity
329+ </ button >
330+ ) : (
331+ < div className = "flex flex-col gap-2" >
332+ < p className = "text-gray-primary font-bold mb-1" > Split Item (Total: { item . quantity } )</ p >
333+ < p className = "text-sm text-gray-primary font-normal" >
334+ Number of Splits
335+ </ p >
336+ < input
337+ type = "number"
338+ step = { 1 }
339+ min = { 2 }
340+ max = { 7 }
341+ value = { numSplits }
342+ onChange = { ( e ) => {
343+ const val = e . target . value ;
344+ setNumSplits ( val ) ;
345+ const n = Number ( val ) ;
346+ if ( Number . isInteger ( n ) && n >= 2 && n <= 7 ) {
347+ setSplitQuantities ( Array ( n ) . fill ( "" ) ) ;
348+ } else {
349+ setSplitQuantities ( [ ] ) ;
350+ }
351+ } }
352+ placeholder = "2 - 7"
353+ className = "border border-gray-primary/20 rounded px-2 py-1 font-normal"
354+ />
355+ { splitQuantities . length > 0 && (
356+ < div className = "flex flex-col gap-1 mt-1" >
357+ { splitQuantities . map ( ( qty , i ) => (
358+ < div key = { i } className = "flex items-center gap-2" >
359+ < label className = "text-sm text-gray-primary font-normal whitespace-nowrap" >
360+ Line Item { i + 1 } :
361+ </ label >
362+ < input
363+ type = "number"
364+ step = { 1 }
365+ min = { 1 }
366+ value = { qty }
367+ onChange = { ( e ) => {
368+ const updated = [ ...splitQuantities ] ;
369+ updated [ i ] = e . target . value ;
370+ setSplitQuantities ( updated ) ;
371+ } }
372+ placeholder = "Qty"
373+ className = "border border-gray-primary/20 rounded px-2 py-1 font-normal w-full"
374+ />
375+ </ div >
376+ ) ) }
377+ < p className = "flex justify-end text-s font-semibold text-gray-primary my-2" >
378+ Total: { splitQuantities . reduce ( ( sum , q ) => sum + ( Number ( q ) || 0 ) , 0 ) } / { item . quantity }
379+ </ p >
380+ </ div >
381+ ) }
382+ < div className = "w-full flex justify-end gap-2" >
383+ < button
384+ onClick = { ( ) => {
385+ setIsSplitting ( false ) ;
386+ setNumSplits ( "" ) ;
387+ setSplitQuantities ( [ ] ) ;
388+ } }
389+ className = "rounded border border-red-primary text-red-primary px-3 py-1 hover:bg-red-primary/10 transition-all duration-200"
390+ >
391+ Cancel
392+ </ button >
393+ < button
394+ onClick = { splitItem }
395+ disabled = {
396+ isSplitLoading ||
397+ splitQuantities . length < 2 ||
398+ splitQuantities . some ( ( q ) => ! q || ! Number . isInteger ( Number ( q ) ) || Number ( q ) <= 0 ) ||
399+ splitQuantities . reduce ( ( sum , q ) => sum + ( Number ( q ) || 0 ) , 0 ) !== item . quantity
400+ }
401+ className = "rounded bg-blue-primary text-white px-3 py-1 disabled:opacity-50"
402+ >
403+ Split
404+ </ button >
405+ </ div >
406+ </ div >
407+ ) }
408+
409+ </ div >
410+ ) }
278411 </ div >
279412 ) }
280413 />
0 commit comments