Skip to content

Commit 4b18a97

Browse files
committed
Texting + multiselect filter fix
1 parent 456faba commit 4b18a97

File tree

6 files changed

+344
-37
lines changed

6 files changed

+344
-37
lines changed

apps/property-tree/src/components/leases/LeasesPage.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,10 @@ const LeasesPage = () => {
250250
selectedProperties.length > 0 ? selectedProperties : undefined,
251251
districtNames:
252252
selectedDistricts.length > 0 ? selectedDistricts : undefined,
253+
buildingManager:
254+
selectedBuildingManagers.length > 0
255+
? selectedBuildingManagers
256+
: undefined,
253257
startDateFrom: startDateFrom || undefined,
254258
startDateTo: startDateTo || undefined,
255259
endDateFrom: endDateFrom || undefined,
@@ -322,6 +326,10 @@ const LeasesPage = () => {
322326
property: selectedProperties.length > 0 ? selectedProperties : undefined,
323327
districtNames:
324328
selectedDistricts.length > 0 ? selectedDistricts : undefined,
329+
buildingManager:
330+
selectedBuildingManagers.length > 0
331+
? selectedBuildingManagers
332+
: undefined,
325333
startDateFrom: startDateFrom || undefined,
326334
startDateTo: startDateTo || undefined,
327335
endDateFrom: endDateFrom || undefined,
@@ -333,6 +341,7 @@ const LeasesPage = () => {
333341
selectedStatuses,
334342
selectedProperties,
335343
selectedDistricts,
344+
selectedBuildingManagers,
336345
startDateFrom,
337346
startDateTo,
338347
endDateFrom,
@@ -751,13 +760,15 @@ const LeasesPage = () => {
751760
open={showSmsModal}
752761
onOpenChange={setShowSmsModal}
753762
recipients={smsRecipients}
763+
totalSelectedItems={selectedCount}
754764
onSend={handleSendSms}
755765
/>
756766

757767
<BulkEmailModal
758768
open={showEmailModal}
759769
onOpenChange={setShowEmailModal}
760770
recipients={emailRecipients}
771+
totalSelectedItems={selectedCount}
761772
onSend={handleSendEmail}
762773
/>
763774
</div>

apps/property-tree/src/components/ui/BulkEmailModal.tsx

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useMemo, useCallback } from 'react'
2-
import { Mail, User, AlertTriangle } from 'lucide-react'
2+
import { Mail, User, AlertTriangle, ChevronDown, Info } from 'lucide-react'
33
import {
44
Dialog,
55
DialogContent,
@@ -13,6 +13,7 @@ import { Textarea } from '@/components/ui/Textarea'
1313
import { Input } from '@/components/ui/Input'
1414
import { Badge } from '@/components/ui/v2/Badge'
1515
import { Label } from '@/components/ui/Label'
16+
import { cn } from '@/lib/utils'
1617

1718
export interface EmailRecipient {
1819
id: string
@@ -24,6 +25,7 @@ interface BulkEmailModalProps {
2425
open: boolean
2526
onOpenChange: (open: boolean) => void
2627
recipients: EmailRecipient[]
28+
totalSelectedItems?: number
2729
onSend?: (
2830
subject: string,
2931
body: string,
@@ -35,11 +37,13 @@ export function BulkEmailModal({
3537
open,
3638
onOpenChange,
3739
recipients,
40+
totalSelectedItems,
3841
onSend,
3942
}: BulkEmailModalProps) {
4043
const [subject, setSubject] = useState('')
4144
const [body, setBody] = useState('')
4245
const [isSending, setIsSending] = useState(false)
46+
const [showAllInvalid, setShowAllInvalid] = useState(false)
4347

4448
const { validRecipients, invalidRecipients } = useMemo(() => {
4549
const valid = recipients.filter((r) => r.email)
@@ -75,8 +79,10 @@ export function BulkEmailModal({
7579
Skicka mejl
7680
</DialogTitle>
7781
<DialogDescription>
78-
Skicka mejl till {validRecipients.length} av {recipients.length}{' '}
79-
valda kunder
82+
{totalSelectedItems != null &&
83+
totalSelectedItems !== recipients.length
84+
? `${totalSelectedItems} valda hyreskontrakt \u2192 ${recipients.length} unika kontakter`
85+
: `Skicka mejl till ${validRecipients.length} av ${recipients.length} valda kunder`}
8086
</DialogDescription>
8187
</DialogHeader>
8288

@@ -99,20 +105,48 @@ export function BulkEmailModal({
99105
</div>
100106
</div>
101107

102-
{invalidRecipients.length > 0 && (
103-
<div className="flex items-start gap-2 p-3 rounded-md bg-yellow-50 border border-yellow-200 text-yellow-800">
104-
<AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" />
105-
<div className="text-sm">
106-
<span className="font-medium">
107-
{invalidRecipients.length} mottagare saknar e-postadress:
108-
</span>{' '}
109-
{invalidRecipients
110-
.slice(0, 3)
111-
.map((r) => r.name)
112-
.join(', ')}
113-
{invalidRecipients.length > 3 &&
114-
` och ${invalidRecipients.length - 3} till`}
115-
</div>
108+
{((totalSelectedItems != null &&
109+
totalSelectedItems > recipients.length) ||
110+
invalidRecipients.length > 0) && (
111+
<div className="space-y-2">
112+
{totalSelectedItems != null &&
113+
totalSelectedItems > recipients.length && (
114+
<div className="flex items-start gap-2 p-3 rounded-md bg-blue-50 border border-blue-200 text-blue-800">
115+
<Info className="h-4 w-4 mt-0.5 shrink-0" />
116+
<span className="text-sm">
117+
{totalSelectedItems - recipients.length} kontakter
118+
förekommer på flera kontrakt och visas bara en gång
119+
</span>
120+
</div>
121+
)}
122+
123+
{invalidRecipients.length > 0 && (
124+
<div className="p-3 rounded-md bg-yellow-50 border border-yellow-200 text-yellow-800">
125+
<button
126+
type="button"
127+
className="flex items-start gap-2 w-full text-left"
128+
onClick={() => setShowAllInvalid((prev) => !prev)}
129+
>
130+
<AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" />
131+
<span className="text-sm font-medium flex-1">
132+
{invalidRecipients.length} mottagare saknar e-postadress
133+
</span>
134+
<ChevronDown
135+
className={cn(
136+
'h-4 w-4 mt-0.5 shrink-0 transition-transform',
137+
showAllInvalid && 'rotate-180'
138+
)}
139+
/>
140+
</button>
141+
{showAllInvalid && (
142+
<div className="mt-2 ml-6 text-sm space-y-1 max-h-32 overflow-y-auto">
143+
{invalidRecipients.map((r) => (
144+
<div key={r.id}>{r.name}</div>
145+
))}
146+
</div>
147+
)}
148+
</div>
149+
)}
116150
</div>
117151
)}
118152

apps/property-tree/src/components/ui/BulkSmsModal.tsx

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { 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'
39
import {
410
Dialog,
511
DialogContent,
@@ -17,11 +23,8 @@ const MAX_SMS_LENGTH = 1600
1723
const COST_WARNING_THRESHOLD = 100
1824
const 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 /^(07\d{8}|\+467\d{8}|00467\d{8}|467\d{8})$/.test(cleaned)
26+
function hasPhoneNumber(phone: string | null): boolean {
27+
return phone !== null && phone.trim().length > 0
2528
}
2629

2730
export 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

4044
export 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 &times; {SMS_COST_SEK} kr)
148206
</div>
149207
</div>
150208
)}

core/src/adapters/leasing-adapter/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,27 @@ const deleteListingTextContent = async (
889889
}
890890
}
891891

892+
const getContactsByFilters = async (
893+
queryParams: Record<string, string | string[] | undefined>
894+
): Promise<{ content: leasing.v1.ContactInfo[] }> => {
895+
const params = new URLSearchParams()
896+
897+
Object.entries(queryParams).forEach(([key, value]) => {
898+
if (value === undefined) return
899+
if (Array.isArray(value)) {
900+
value.forEach((v) => params.append(key, v))
901+
} else {
902+
params.append(key, value)
903+
}
904+
})
905+
906+
const response = await axios.get(
907+
`${tenantsLeasesServiceUrl}/leases/contacts-by-filters?${params.toString()}`
908+
)
909+
910+
return response.data
911+
}
912+
892913
interface ExportLeasesResult {
893914
data: Buffer
894915
contentType: string
@@ -932,6 +953,7 @@ export {
932953
addApplicantToWaitingList,
933954
createLease,
934955
exportLeasesToExcel,
956+
getContactsByFilters,
935957
getApplicantByContactCodeAndListingId,
936958
getApplicantsAndListingByContactCode,
937959
getApplicantsByContactCode,

core/src/services/lease-service/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,27 @@ export const routes = (router: KoaRouter) => {
442442
}
443443
})
444444

445+
router.get('/leases/contacts-by-filters', async (ctx) => {
446+
const metadata = generateRouteMetadata(ctx)
447+
448+
try {
449+
const result = await leasingAdapter.getContactsByFilters(ctx.query)
450+
451+
ctx.status = 200
452+
ctx.body = result
453+
} catch (error: unknown) {
454+
logger.error({ error, metadata }, 'Error fetching contacts by filters')
455+
ctx.status = 500
456+
ctx.body = {
457+
error:
458+
error instanceof Error
459+
? error.message
460+
: 'Unknown error occurred fetching contacts',
461+
...metadata,
462+
}
463+
}
464+
})
465+
445466
/**
446467
* @swagger
447468
* /leases/by-rental-property-id/{rentalPropertyId}:

0 commit comments

Comments
 (0)