11import { useState , useMemo , useCallback } from 'react'
2- import { MessageSquare , User , AlertTriangle } from 'lucide-react'
2+ import {
3+ MessageSquare ,
4+ User ,
5+ AlertTriangle ,
6+ ChevronDown ,
7+ Info ,
8+ } from 'lucide-react'
39import {
410 Dialog ,
511 DialogContent ,
@@ -17,11 +23,8 @@ const MAX_SMS_LENGTH = 1600
1723const COST_WARNING_THRESHOLD = 100
1824const SMS_COST_SEK = 0.5
1925
20- function isValidSwedishMobile ( phone : string | null ) : boolean {
21- if ( ! phone ) return false
22- const cleaned = phone . replace ( / [ \s - ] / g, '' )
23- // Swedish mobile: 07XXXXXXXX, +467XXXXXXXX, 00467XXXXXXXX, or 467XXXXXXXX
24- return / ^ ( 0 7 \d { 8 } | \+ 4 6 7 \d { 8 } | 0 0 4 6 7 \d { 8 } | 4 6 7 \d { 8 } ) $ / . test ( cleaned )
26+ function hasPhoneNumber ( phone : string | null ) : boolean {
27+ return phone !== null && phone . trim ( ) . length > 0
2528}
2629
2730export interface SmsRecipient {
@@ -34,28 +37,35 @@ interface BulkSmsModalProps {
3437 open : boolean
3538 onOpenChange : ( open : boolean ) => void
3639 recipients : SmsRecipient [ ]
40+ totalSelectedItems ?: number
3741 onSend ?: ( message : string , recipients : SmsRecipient [ ] ) => Promise < void >
3842}
3943
4044export function BulkSmsModal ( {
4145 open,
4246 onOpenChange,
4347 recipients,
48+ totalSelectedItems,
4449 onSend,
4550} : BulkSmsModalProps ) {
4651 const [ message , setMessage ] = useState ( '' )
4752 const [ showCostConfirmation , setShowCostConfirmation ] = useState ( false )
53+ const [ showAllInvalid , setShowAllInvalid ] = useState ( false )
4854 const [ isSending , setIsSending ] = useState ( false )
4955
5056 const charactersLeft = MAX_SMS_LENGTH - message . length
5157
5258 const { validRecipients, invalidRecipients } = useMemo ( ( ) => {
53- const valid = recipients . filter ( ( r ) => isValidSwedishMobile ( r . phone ) )
54- const invalid = recipients . filter ( ( r ) => ! isValidSwedishMobile ( r . phone ) )
59+ const valid = recipients . filter ( ( r ) => hasPhoneNumber ( r . phone ) )
60+ const invalid = recipients . filter ( ( r ) => ! hasPhoneNumber ( r . phone ) )
5561 return { validRecipients : valid , invalidRecipients : invalid }
5662 } , [ recipients ] )
5763
5864 const estimatedCost = validRecipients . length * SMS_COST_SEK
65+ const duplicatesRemoved =
66+ totalSelectedItems != null && totalSelectedItems > recipients . length
67+ ? totalSelectedItems - recipients . length
68+ : 0
5969
6070 const doSend = useCallback ( async ( ) => {
6171 if ( ! onSend || isSending ) return
@@ -107,8 +117,10 @@ export function BulkSmsModal({
107117 Skicka SMS
108118 </ DialogTitle >
109119 < DialogDescription >
110- Skicka SMS till { validRecipients . length } av { recipients . length } { ' ' }
111- valda kunder
120+ { totalSelectedItems != null &&
121+ totalSelectedItems !== recipients . length
122+ ? `${ totalSelectedItems } valda hyreskontrakt \u2192 ${ recipients . length } unika kontakter`
123+ : `Skicka SMS till ${ validRecipients . length } av ${ recipients . length } valda kunder` }
112124 </ DialogDescription >
113125 </ DialogHeader >
114126
@@ -131,20 +143,66 @@ export function BulkSmsModal({
131143 </ div >
132144 </ div >
133145
134- { invalidRecipients . length > 0 && (
135- < div className = "flex items-start gap-2 p-3 rounded-md bg-yellow-50 border border-yellow-200 text-yellow-800" >
146+ { ( duplicatesRemoved > 0 || invalidRecipients . length > 0 ) && (
147+ < div className = "space-y-2" >
148+ { duplicatesRemoved > 0 && (
149+ < div className = "flex items-start gap-2 p-3 rounded-md bg-blue-50 border border-blue-200 text-blue-800" >
150+ < Info className = "h-4 w-4 mt-0.5 shrink-0" />
151+ < span className = "text-sm" >
152+ { duplicatesRemoved } kontakter förekommer på flera kontrakt
153+ och visas bara en gång
154+ </ span >
155+ </ div >
156+ ) }
157+
158+ { invalidRecipients . length > 0 && (
159+ < div className = "p-3 rounded-md bg-yellow-50 border border-yellow-200 text-yellow-800" >
160+ < button
161+ type = "button"
162+ className = "flex items-start gap-2 w-full text-left"
163+ onClick = { ( ) => setShowAllInvalid ( ( prev ) => ! prev ) }
164+ >
165+ < AlertTriangle className = "h-4 w-4 mt-0.5 shrink-0" />
166+ < span className = "text-sm font-medium flex-1" >
167+ { invalidRecipients . length } mottagare saknar telefonnummer
168+ </ span >
169+ < ChevronDown
170+ className = { cn (
171+ 'h-4 w-4 mt-0.5 shrink-0 transition-transform' ,
172+ showAllInvalid && 'rotate-180'
173+ ) }
174+ />
175+ </ button >
176+ { showAllInvalid && (
177+ < div className = "mt-2 ml-6 text-sm space-y-1 max-h-32 overflow-y-auto" >
178+ { invalidRecipients . map ( ( r ) => (
179+ < div key = { r . id } className = "flex justify-between gap-2" >
180+ < span > { r . name } </ span >
181+ < span className = "text-yellow-600 shrink-0" >
182+ { r . phone || 'Saknar nummer' }
183+ </ span >
184+ </ div >
185+ ) ) }
186+ </ div >
187+ ) }
188+ </ div >
189+ ) }
190+ </ div >
191+ ) }
192+
193+ { validRecipients . length > COST_WARNING_THRESHOLD && (
194+ < div className = "flex items-start gap-2 p-3 rounded-md bg-amber-50 border border-amber-200 text-amber-800" >
136195 < AlertTriangle className = "h-4 w-4 mt-0.5 shrink-0" />
137196 < div className = "text-sm" >
197+ Beräknad kostnad:{ ' ' }
138198 < span className = "font-medium" >
139- { invalidRecipients . length } mottagare saknar giltigt
140- mobilnummer:
199+ { estimatedCost . toLocaleString ( 'sv-SE' , {
200+ minimumFractionDigits : 0 ,
201+ maximumFractionDigits : 2 ,
202+ } ) } { ' ' }
203+ kr
141204 </ span > { ' ' }
142- { invalidRecipients
143- . slice ( 0 , 3 )
144- . map ( ( r ) => r . name )
145- . join ( ', ' ) }
146- { invalidRecipients . length > 3 &&
147- ` och ${ invalidRecipients . length - 3 } till` }
205+ ({ validRecipients . length } mottagare × { SMS_COST_SEK } kr)
148206 </ div >
149207 </ div >
150208 ) }
0 commit comments