Skip to content

Commit 471702b

Browse files
authored
Feature/mim 1319 ny status pa befintlig nyckel ersattning bestalld inkommen (#390)
* replacement key handling * prettier * bugfix race condition where on creating a new extra key it didnt have time to register the status before adding it to the list in the frontend
1 parent 7efb438 commit 471702b

File tree

13 files changed

+408
-40
lines changed

13 files changed

+408
-40
lines changed

apps/keys-portal/src/components/loan/AddKeyForm.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ type Props = {
4343
keys: Key[]
4444
selectedKeyIds?: string[]
4545
rentalObjectCode: string
46-
onKeyCreated: (key: Key) => void
46+
onComplete: () => void
4747
onCancel: () => void
4848
}
4949

5050
export function AddKeyForm({
5151
keys,
5252
selectedKeyIds = [],
5353
rentalObjectCode,
54-
onKeyCreated,
54+
onComplete,
5555
onCancel,
5656
}: Props) {
5757
const { toast } = useToast()
@@ -341,14 +341,11 @@ export function AddKeyForm({
341341

342342
// Create all keys and collect their IDs
343343
const createdKeyIds: string[] = []
344-
let createdCount = 0
345344
for (const keyPayload of keysToCreate) {
346345
const created = await keyService.createKey({
347346
...keyPayload,
348347
})
349348
createdKeyIds.push(created.id)
350-
onKeyCreated(created)
351-
createdCount++
352349
}
353350

354351
// Create ORDER event for all created keys
@@ -363,12 +360,13 @@ export function AddKeyForm({
363360

364361
toast({
365362
title: 'Nycklar skapade',
366-
description: `${createdCount} ${createdCount === 1 ? 'nyckel skapad' : 'nycklar skapade'}`,
363+
description: `${createdKeyIds.length} ${createdKeyIds.length === 1 ? 'nyckel skapad' : 'nycklar skapade'}`,
367364
})
368365

369366
setRows([])
370367
setKeySystemSearch('')
371368
setSelectedKeySystem(null)
369+
onComplete()
372370
} catch (e: any) {
373371
toast({
374372
title: 'Kunde inte skapa nycklar',

apps/keys-portal/src/components/loan/KeyActionButtons.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { useState } from 'react'
22
import { Button } from '@/components/ui/button'
3-
import { Plus, Copy, Trash2 } from 'lucide-react'
3+
import { Plus, Copy, RefreshCw, Trash2 } from 'lucide-react'
44

55
import type { KeyDetails, CardDetails } from '@/services/types'
66
import { getActiveLoan } from '@/utils/loanHelpers'
77
import { FlexMenu } from './dialogs/FlexMenu'
88
import { IncomingFlexMenu } from './dialogs/IncomingFlexMenu'
99
import { IncomingOrderMenu } from './dialogs/IncomingOrderMenu'
10+
import { ReplacementMenu } from './dialogs/ReplacementMenu'
11+
import { IncomingReplacementMenu } from './dialogs/IncomingReplacementMenu'
1012

1113
type Props = {
1214
selectedKeys: string[]
@@ -40,6 +42,9 @@ export function KeyActionButtons({
4042
const [flexMenuOpen, setFlexMenuOpen] = useState(false)
4143
const [incomingFlexMenuOpen, setIncomingFlexMenuOpen] = useState(false)
4244
const [incomingOrderMenuOpen, setIncomingOrderMenuOpen] = useState(false)
45+
const [replacementMenuOpen, setReplacementMenuOpen] = useState(false)
46+
const [incomingReplacementMenuOpen, setIncomingReplacementMenuOpen] =
47+
useState(false)
4348

4449
// Helper to check if a key's or card's loan matches current tenant
4550
const matchesCurrentTenant = (item: KeyDetails | CardDetails) => {
@@ -99,6 +104,22 @@ export function KeyActionButtons({
99104
)
100105
})
101106

107+
// Keys that have "beställd ersättning" status (latest event is REPLACEMENT type with ORDERED status)
108+
const incomingReplacementKeys = selectedKeysData.filter((k) => {
109+
const latestEvent = k.events?.[0] // Events are sorted by createdAt desc
110+
return (
111+
latestEvent &&
112+
latestEvent.type === 'REPLACEMENT' &&
113+
latestEvent.status === 'ORDERED'
114+
)
115+
})
116+
117+
// Keys eligible for replacement (no active/non-completed event)
118+
const replacementEligibleKeys = selectedKeysData.filter((k) => {
119+
const latestEvent = k.events?.[0]
120+
return !latestEvent || latestEvent.status === 'COMPLETED'
121+
})
122+
102123
// All available keys (excluding disposed keys)
103124
const allAvailableKeys = keysWithStatus.filter(
104125
(k) => !getActiveLoan(k) && leaseIsNotPast && !k.disposed
@@ -206,6 +227,18 @@ export function KeyActionButtons({
206227
Inkommen extranyckel ({incomingOrderKeys.length})
207228
</Button>
208229
)}
230+
{incomingReplacementKeys.length > 0 && (
231+
<Button
232+
size="sm"
233+
variant="outline"
234+
onClick={() => setIncomingReplacementMenuOpen(true)}
235+
disabled={isProcessing}
236+
className="flex items-center gap-1"
237+
>
238+
<RefreshCw className="h-3 w-3" />
239+
Inkommen ersättning ({incomingReplacementKeys.length})
240+
</Button>
241+
)}
209242
<Button
210243
size="sm"
211244
variant="outline"
@@ -216,6 +249,18 @@ export function KeyActionButtons({
216249
<Copy className="h-3 w-3" />
217250
Flex ({selectedKeys.length})
218251
</Button>
252+
{replacementEligibleKeys.length > 0 && (
253+
<Button
254+
size="sm"
255+
variant="outline"
256+
onClick={() => setReplacementMenuOpen(true)}
257+
disabled={isProcessing}
258+
className="flex items-center gap-1"
259+
>
260+
<RefreshCw className="h-3 w-3" />
261+
Ersättning ({replacementEligibleKeys.length})
262+
</Button>
263+
)}
219264
{onDispose && (
220265
<Button
221266
size="sm"
@@ -291,6 +336,20 @@ export function KeyActionButtons({
291336
selectedKeys={selectedKeysData}
292337
onSuccess={onRefresh}
293338
/>
339+
340+
<ReplacementMenu
341+
open={replacementMenuOpen}
342+
onOpenChange={setReplacementMenuOpen}
343+
selectedKeys={selectedKeysData}
344+
onSuccess={onRefresh}
345+
/>
346+
347+
<IncomingReplacementMenu
348+
open={incomingReplacementMenuOpen}
349+
onOpenChange={setIncomingReplacementMenuOpen}
350+
selectedKeys={selectedKeysData}
351+
onSuccess={onRefresh}
352+
/>
294353
</>
295354
)
296355
}

apps/keys-portal/src/components/loan/LeaseKeyStatusList.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,8 @@ export function LeaseKeyStatusList({
124124
setCards(fetchedCards)
125125
}
126126

127-
const handleKeyCreated = async () => {
127+
const handleKeysAdded = async () => {
128128
setShowAddKeyForm(false)
129-
// Refresh keys after a new key is created
130129
await refreshStatuses()
131130
}
132131

@@ -286,7 +285,7 @@ export function LeaseKeyStatusList({
286285
keys={keys}
287286
selectedKeyIds={keySelection.selectedIds}
288287
rentalObjectCode={lease.rentalPropertyId}
289-
onKeyCreated={handleKeyCreated}
288+
onComplete={handleKeysAdded}
290289
onCancel={() => setShowAddKeyForm(false)}
291290
/>
292291
)}
@@ -344,7 +343,7 @@ export function LeaseKeyStatusList({
344343
keys={keys}
345344
selectedKeyIds={keySelection.selectedIds}
346345
rentalObjectCode={lease.rentalPropertyId}
347-
onKeyCreated={handleKeyCreated}
346+
onComplete={handleKeysAdded}
348347
onCancel={() => setShowAddKeyForm(false)}
349348
/>
350349
)}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { useState } from 'react'
2+
import type { KeyDetails } from '@/services/types'
3+
import { KeyTypeLabels } from '@/services/types'
4+
import { keyEventService } from '@/services/api/keyEventService'
5+
import { useToast } from '@/hooks/use-toast'
6+
import { Button } from '@/components/ui/button'
7+
import {
8+
Dialog,
9+
DialogContent,
10+
DialogDescription,
11+
DialogFooter,
12+
DialogHeader,
13+
DialogTitle,
14+
} from '@/components/ui/dialog'
15+
import { Spinner } from '@/components/ui/spinner'
16+
17+
type Props = {
18+
open: boolean
19+
onOpenChange: (open: boolean) => void
20+
selectedKeys: KeyDetails[]
21+
onSuccess?: () => void
22+
}
23+
24+
export function IncomingReplacementMenu({
25+
open,
26+
onOpenChange,
27+
selectedKeys,
28+
onSuccess,
29+
}: Props) {
30+
const { toast } = useToast()
31+
const [isProcessing, setIsProcessing] = useState(false)
32+
33+
const handleAccept = async () => {
34+
setIsProcessing(true)
35+
try {
36+
// Get event IDs directly from the key objects (already filtered by KeyActionButtons)
37+
const eventIds = selectedKeys
38+
.map((key) => key.events?.[0]?.id)
39+
.filter((id): id is string => !!id)
40+
41+
await Promise.all(
42+
eventIds.map((eventId) =>
43+
keyEventService.updateStatus(eventId, 'RECEIVED')
44+
)
45+
)
46+
47+
toast({
48+
title: 'Ersättningsnycklar inkomna',
49+
description: `${selectedKeys.length} ${selectedKeys.length === 1 ? 'ersättningsnyckel har' : 'ersättningsnycklar har'} markerats som inkomna.`,
50+
})
51+
52+
onOpenChange(false)
53+
onSuccess?.()
54+
} catch (error) {
55+
const msg = error instanceof Error ? error.message : 'Okänt fel'
56+
toast({
57+
title: 'Kunde inte markera ersättningsnycklar som inkomna',
58+
description: msg,
59+
variant: 'destructive',
60+
})
61+
} finally {
62+
setIsProcessing(false)
63+
}
64+
}
65+
66+
return (
67+
<Dialog open={open} onOpenChange={onOpenChange}>
68+
<DialogContent className="max-w-xl max-h-[80vh] overflow-y-auto">
69+
<DialogHeader>
70+
<DialogTitle>Inkommen ersättning</DialogTitle>
71+
<DialogDescription>
72+
Markera följande ersättningsnycklar som inkomna:
73+
</DialogDescription>
74+
</DialogHeader>
75+
76+
<div className="space-y-2 max-h-[400px] overflow-y-auto pb-px">
77+
{selectedKeys.map((key) => (
78+
<div
79+
key={key.id}
80+
className="p-3 border rounded-lg bg-card flex items-center gap-3"
81+
>
82+
<div className="flex-1">
83+
<div className="font-medium text-sm">{key.keyName}</div>
84+
<div className="text-xs text-muted-foreground">
85+
{KeyTypeLabels[key.keyType]}
86+
{key.keySequenceNumber !== undefined &&
87+
` • Löpnr: ${key.keySequenceNumber}`}
88+
</div>
89+
</div>
90+
</div>
91+
))}
92+
</div>
93+
94+
<DialogFooter>
95+
<Button
96+
variant="outline"
97+
onClick={() => onOpenChange(false)}
98+
disabled={isProcessing}
99+
>
100+
Avbryt
101+
</Button>
102+
<Button
103+
onClick={handleAccept}
104+
disabled={isProcessing || selectedKeys.length === 0}
105+
>
106+
{isProcessing ? (
107+
<>
108+
<Spinner size="sm" className="mr-2" />
109+
Bearbetar...
110+
</>
111+
) : (
112+
`Markera som inkommen (${selectedKeys.length})`
113+
)}
114+
</Button>
115+
</DialogFooter>
116+
</DialogContent>
117+
</Dialog>
118+
)
119+
}

0 commit comments

Comments
 (0)