521 lines
25 KiB
TypeScript
521 lines
25 KiB
TypeScript
'use client'
|
||
|
||
import React, { useEffect } from 'react'
|
||
import { useRouter } from 'next/navigation'
|
||
import useAuthStore from '../../store/authStore'
|
||
import PageLayout from '../../components/PageLayout'
|
||
import BlueBlurryBackground from '../../components/background/blueblurry'
|
||
import { useMyAbonements } from '../hooks/getAbo'
|
||
import FinanceInvoices from '../components/financeInvoices'
|
||
import { useActiveCoffees } from '../../coffee-abonnements/hooks/getActiveCoffees'
|
||
import { changeSubscriptionStatus, editSubscriptionContent } from '../hooks/editAbo'
|
||
import ConfirmActionModal from '../../components/modals/ConfirmActionModal'
|
||
|
||
type UiLifecycleStatus = 'issued' | 'ongoing' | 'finished' | 'pause' | 'cancelled'
|
||
|
||
const normalizeSubscriptionStatus = (rawStatus?: string | null): UiLifecycleStatus => {
|
||
const status = (rawStatus || '').toLowerCase()
|
||
if (status === 'pause' || status === 'paused') return 'pause'
|
||
if (status === 'cancelled' || status === 'canceled') return 'cancelled'
|
||
if (status === 'finished' || status === 'expired') return 'finished'
|
||
if (status === 'issued') return 'issued'
|
||
return 'ongoing'
|
||
}
|
||
|
||
const statusBadgeClass = (status: UiLifecycleStatus) => {
|
||
if (status === 'ongoing') return 'bg-green-100 text-green-800'
|
||
if (status === 'pause') return 'bg-amber-100 text-amber-800'
|
||
if (status === 'cancelled') return 'bg-red-100 text-red-700'
|
||
if (status === 'finished') return 'bg-gray-200 text-gray-700'
|
||
return 'bg-blue-100 text-blue-800'
|
||
}
|
||
|
||
const displayStatus = (status: UiLifecycleStatus) =>
|
||
status === 'pause'
|
||
? 'Pause'
|
||
: status === 'cancelled'
|
||
? 'Cancelled'
|
||
: status === 'ongoing'
|
||
? 'Ongoing'
|
||
: status === 'finished'
|
||
? 'Finished'
|
||
: 'Issued'
|
||
|
||
const formatDate = (value?: string | null) => {
|
||
if (!value) return '—'
|
||
const date = new Date(value)
|
||
return Number.isNaN(date.getTime()) ? '—' : date.toLocaleDateString('de-DE')
|
||
}
|
||
|
||
const formatMoney = (value?: string | number | null, currency?: string | null) => {
|
||
if (value == null || value === '') return '—'
|
||
const amount = typeof value === 'string' ? Number(value) : value
|
||
if (!Number.isFinite(Number(amount))) return String(value)
|
||
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: currency || 'EUR' }).format(Number(amount))
|
||
}
|
||
|
||
export default function ProfileSubscriptionsPage() {
|
||
const router = useRouter()
|
||
const user = useAuthStore(state => state.user)
|
||
const isAuthReady = useAuthStore(state => state.isAuthReady)
|
||
const [hasHydrated, setHasHydrated] = React.useState(false)
|
||
const [selectedAboId, setSelectedAboId] = React.useState<string | number | null>(null)
|
||
const [refreshKey, setRefreshKey] = React.useState(0)
|
||
const [editingContent, setEditingContent] = React.useState(false)
|
||
const [draftItems, setDraftItems] = React.useState<Record<string, number>>({})
|
||
const [savingContent, setSavingContent] = React.useState(false)
|
||
const [contentError, setContentError] = React.useState<string | null>(null)
|
||
const [statusBusy, setStatusBusy] = React.useState(false)
|
||
const [statusError, setStatusError] = React.useState<string | null>(null)
|
||
const [statusConfirmTarget, setStatusConfirmTarget] = React.useState<'ongoing' | 'pause' | 'cancelled' | null>(null)
|
||
|
||
const { data: subscriptions, loading, error } = useMyAbonements(refreshKey)
|
||
const { coffees, loading: coffeesLoading, error: coffeesError } = useActiveCoffees()
|
||
|
||
useEffect(() => { setHasHydrated(true) }, [])
|
||
|
||
useEffect(() => {
|
||
if (!hasHydrated || !isAuthReady) return
|
||
if (!user) router.replace('/login')
|
||
}, [hasHydrated, isAuthReady, user, router])
|
||
|
||
useEffect(() => {
|
||
if (!subscriptions.length) {
|
||
setSelectedAboId(null)
|
||
return
|
||
}
|
||
|
||
setSelectedAboId(prev => {
|
||
if (prev != null && subscriptions.some((sub) => String(sub.id) === String(prev))) return prev
|
||
return subscriptions[0].id
|
||
})
|
||
}, [subscriptions])
|
||
|
||
const coffeeImageById = React.useMemo(() => {
|
||
const map: Record<string, string> = {}
|
||
coffees.forEach((coffee) => {
|
||
if (coffee.image) {
|
||
map[String(coffee.id)] = coffee.image
|
||
}
|
||
})
|
||
return map
|
||
}, [coffees])
|
||
|
||
if (!hasHydrated || !isAuthReady || !user) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
|
||
<p className="text-[#4A4A4A]">Loading...</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const selectedAbo = subscriptions.find((sub) => String(sub.id) === String(selectedAboId)) || null
|
||
const status = normalizeSubscriptionStatus(selectedAbo?.status)
|
||
const includedItems = selectedAbo?.pack_breakdown || selectedAbo?.items || []
|
||
const totalPacks = includedItems.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0)
|
||
const draftTotalPacks = Object.values(draftItems).reduce((sum, qty) => sum + (Number(qty) || 0), 0)
|
||
const canChangeContent = status === 'ongoing' || status === 'pause' || status === 'issued'
|
||
|
||
const startEditingContent = () => {
|
||
if (!selectedAbo) return
|
||
const nextDraft: Record<string, number> = {}
|
||
;(selectedAbo.pack_breakdown || selectedAbo.items || []).forEach((item) => {
|
||
const key = String(item.coffeeId ?? '')
|
||
if (!key) return
|
||
nextDraft[key] = (nextDraft[key] || 0) + (Number(item.quantity) || 0)
|
||
})
|
||
setDraftItems(nextDraft)
|
||
setEditingContent(true)
|
||
setContentError(null)
|
||
}
|
||
|
||
const updateDraftQty = (coffeeId: string, value: number) => {
|
||
setDraftItems((prev) => ({ ...prev, [coffeeId]: Math.max(0, Math.floor(Number(value) || 0)) }))
|
||
}
|
||
|
||
const cancelEditingContent = () => {
|
||
setEditingContent(false)
|
||
setDraftItems({})
|
||
setContentError(null)
|
||
}
|
||
|
||
const saveContentChanges = async () => {
|
||
if (!selectedAbo) return
|
||
setContentError(null)
|
||
const items = Object.entries(draftItems)
|
||
.filter(([, qty]) => Number(qty) > 0)
|
||
.map(([coffeeId, quantity]) => ({ coffeeId, quantity: Number(quantity) }))
|
||
|
||
if (!items.length) {
|
||
setContentError('Please select at least one coffee with quantity greater than 0.')
|
||
return
|
||
}
|
||
if (draftTotalPacks !== 6 && draftTotalPacks !== 12) {
|
||
setContentError('Total packs must be exactly 6 or 12.')
|
||
return
|
||
}
|
||
|
||
try {
|
||
setSavingContent(true)
|
||
await editSubscriptionContent(selectedAbo.id, items)
|
||
setEditingContent(false)
|
||
setRefreshKey((k) => k + 1)
|
||
} catch (e: any) {
|
||
setContentError(e?.message || 'Failed to update subscription content.')
|
||
} finally {
|
||
setSavingContent(false)
|
||
}
|
||
}
|
||
|
||
const onStatusAction = async (targetStatus: 'ongoing' | 'pause' | 'cancelled') => {
|
||
if (!selectedAbo) return
|
||
setStatusError(null)
|
||
try {
|
||
setStatusBusy(true)
|
||
await changeSubscriptionStatus(selectedAbo.id, targetStatus)
|
||
setEditingContent(false)
|
||
setRefreshKey((k) => k + 1)
|
||
} catch (e: any) {
|
||
setStatusError(e?.message || 'Failed to update subscription status.')
|
||
} finally {
|
||
setStatusBusy(false)
|
||
}
|
||
}
|
||
|
||
const openStatusConfirm = (targetStatus: 'ongoing' | 'pause' | 'cancelled') => {
|
||
setStatusConfirmTarget(targetStatus)
|
||
setStatusError(null)
|
||
}
|
||
|
||
const closeStatusConfirm = () => {
|
||
if (statusBusy) return
|
||
setStatusConfirmTarget(null)
|
||
}
|
||
|
||
const confirmStatusChange = async () => {
|
||
if (!statusConfirmTarget) return
|
||
await onStatusAction(statusConfirmTarget)
|
||
setStatusConfirmTarget(null)
|
||
}
|
||
|
||
const confirmTitle =
|
||
statusConfirmTarget === 'pause'
|
||
? 'Pause subscription?'
|
||
: statusConfirmTarget === 'ongoing'
|
||
? 'Resume subscription?'
|
||
: 'Cancel subscription?'
|
||
|
||
const confirmDescription =
|
||
statusConfirmTarget === 'pause'
|
||
? 'Your subscription will be paused. Billing and deliveries remain stopped until you resume it.'
|
||
: statusConfirmTarget === 'ongoing'
|
||
? 'Your subscription will become ongoing again for the next billing cycle.'
|
||
: 'Your subscription will be cancelled and cannot continue automatically. This action is intended to stop future cycles.'
|
||
|
||
const confirmButtonText =
|
||
statusConfirmTarget === 'pause'
|
||
? 'Yes, pause'
|
||
: statusConfirmTarget === 'ongoing'
|
||
? 'Yes, resume'
|
||
: 'Yes, cancel subscription'
|
||
|
||
return (
|
||
<PageLayout className="bg-transparent text-gray-900">
|
||
<BlueBlurryBackground>
|
||
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
|
||
<div className="max-w-4xl mx-auto">
|
||
<div className="rounded-3xl bg-white/60 backdrop-blur-md border border-white/50 shadow-xl p-4 sm:p-6 lg:p-8 space-y-6 sm:space-y-8">
|
||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-gray-900">My Subscriptions</h1>
|
||
<p className="text-gray-600 mt-2">Select any subscription to view details and included items.</p>
|
||
</div>
|
||
<button
|
||
onClick={() => router.push('/profile')}
|
||
className="rounded-md border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
||
>
|
||
Back to profile
|
||
</button>
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">
|
||
Loading subscriptions…
|
||
</div>
|
||
) : error ? (
|
||
<div className="rounded-md border border-red-200 bg-red-50/80 backdrop-blur-md p-4 text-sm text-red-700">
|
||
{error}
|
||
</div>
|
||
) : subscriptions.length === 0 ? (
|
||
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">
|
||
You don’t have any subscriptions yet.
|
||
</div>
|
||
) : (
|
||
<div className="space-y-6">
|
||
<section className="rounded-lg border border-white/60 bg-white/70 backdrop-blur-md p-4 sm:p-6 shadow-lg">
|
||
<div className="flex items-center justify-between gap-3 flex-wrap mb-3">
|
||
<h2 className="text-lg font-semibold text-gray-900">All subscriptions</h2>
|
||
<p className="text-xs text-gray-600">{subscriptions.length} total</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{subscriptions.map((subscription) => {
|
||
const isSelected = String(subscription.id) === String(selectedAboId)
|
||
const subscriptionStatus = normalizeSubscriptionStatus(subscription.status)
|
||
const packs = (subscription.pack_breakdown || subscription.items || []).reduce(
|
||
(sum, item) => sum + (Number(item.quantity) || 0),
|
||
0,
|
||
)
|
||
|
||
return (
|
||
<button
|
||
key={subscription.id}
|
||
onClick={() => setSelectedAboId(subscription.id)}
|
||
className={`text-left rounded-md border px-3 py-3 transition ${
|
||
isSelected
|
||
? 'border-[#8D6B1D] bg-white shadow-md'
|
||
: 'border-gray-200 bg-white/80 hover:bg-white'
|
||
}`}
|
||
>
|
||
<div className="flex items-start justify-between gap-2">
|
||
<div className="min-w-0">
|
||
<p className="text-sm font-semibold text-gray-900 truncate">
|
||
{subscription.name || `Subscription #${subscription.id}`}
|
||
</p>
|
||
</div>
|
||
<span
|
||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${statusBadgeClass(subscriptionStatus)}`}
|
||
>
|
||
{displayStatus(subscriptionStatus)}
|
||
</span>
|
||
</div>
|
||
<p className="text-xs text-gray-600 mt-1">ID: {subscription.id}</p>
|
||
<p className="text-xs text-gray-600">Started: {formatDate(subscription.startedAt || subscription.createdAt)}</p>
|
||
<p className="text-xs text-gray-600">Included packs: {packs}</p>
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
</section>
|
||
|
||
{selectedAbo && (
|
||
<>
|
||
<section className="rounded-lg border border-white/60 bg-white/70 backdrop-blur-md p-4 sm:p-6 shadow-lg">
|
||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||
<div>
|
||
<h2 className="text-lg font-semibold text-gray-900">Subscription details</h2>
|
||
<p className="text-sm text-gray-600 mt-1">{selectedAbo.name || 'Coffee Subscription'}</p>
|
||
</div>
|
||
<span
|
||
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${statusBadgeClass(status)}`}
|
||
>
|
||
{displayStatus(status)}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="mt-3 flex items-center gap-2 flex-wrap">
|
||
{(status === 'ongoing' || status === 'issued') && (
|
||
<button
|
||
onClick={() => openStatusConfirm('pause')}
|
||
disabled={statusBusy}
|
||
className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed"
|
||
>
|
||
{statusBusy ? 'Updating…' : 'Pause subscription'}
|
||
</button>
|
||
)}
|
||
{status === 'pause' && (
|
||
<button
|
||
onClick={() => openStatusConfirm('ongoing')}
|
||
disabled={statusBusy}
|
||
className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed"
|
||
>
|
||
{statusBusy ? 'Updating…' : 'Resume subscription'}
|
||
</button>
|
||
)}
|
||
{(status === 'ongoing' || status === 'pause' || status === 'issued') && (
|
||
<button
|
||
onClick={() => openStatusConfirm('cancelled')}
|
||
disabled={statusBusy}
|
||
className="rounded-md border border-red-200 px-3 py-1.5 text-xs text-red-700 hover:bg-red-50 disabled:opacity-60 disabled:cursor-not-allowed"
|
||
>
|
||
{statusBusy ? 'Updating…' : 'Cancel subscription'}
|
||
</button>
|
||
)}
|
||
{(status === 'finished' || status === 'cancelled') && (
|
||
<p className="text-xs text-gray-600">No further status changes are available for this subscription.</p>
|
||
)}
|
||
</div>
|
||
{statusError && (
|
||
<p className="mt-2 text-xs text-red-600">{statusError}</p>
|
||
)}
|
||
|
||
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
||
<div className="rounded-md bg-white/80 border border-gray-200 px-3 py-2">
|
||
<p className="text-gray-500">Subscription ID</p>
|
||
<p className="font-medium text-gray-900">{selectedAbo.id}</p>
|
||
</div>
|
||
<div className="rounded-md bg-white/80 border border-gray-200 px-3 py-2">
|
||
<p className="text-gray-500">Price</p>
|
||
<p className="font-medium text-gray-900">{formatMoney(selectedAbo.price, selectedAbo.currency)}</p>
|
||
</div>
|
||
<div className="rounded-md bg-white/80 border border-gray-200 px-3 py-2">
|
||
<p className="text-gray-500">Frequency</p>
|
||
<p className="font-medium text-gray-900">{selectedAbo.frequency || '—'}</p>
|
||
</div>
|
||
<div className="rounded-md bg-white/80 border border-gray-200 px-3 py-2">
|
||
<p className="text-gray-500">Country</p>
|
||
<p className="font-medium text-gray-900">{(selectedAbo.country || '').toUpperCase() || '—'}</p>
|
||
</div>
|
||
<div className="rounded-md bg-white/80 border border-gray-200 px-3 py-2">
|
||
<p className="text-gray-500">Started</p>
|
||
<p className="font-medium text-gray-900">{formatDate(selectedAbo.startedAt || selectedAbo.createdAt)}</p>
|
||
</div>
|
||
<div className="rounded-md bg-white/80 border border-gray-200 px-3 py-2">
|
||
<p className="text-gray-500">Next billing</p>
|
||
<p className="font-medium text-gray-900">{formatDate(selectedAbo.nextBillingAt)}</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="rounded-lg border border-white/60 bg-white/70 backdrop-blur-md p-4 sm:p-6 shadow-lg">
|
||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||
<div>
|
||
<h2 className="text-lg font-semibold text-gray-900">Included in your subscription</h2>
|
||
<p className="text-sm text-gray-600 mt-1">{includedItems.length} item(s), {totalPacks} total pack(s)</p>
|
||
<p className="text-xs text-gray-500 mt-1">Changes apply from your next billing cycle.</p>
|
||
</div>
|
||
{!editingContent && canChangeContent && (
|
||
<button
|
||
onClick={startEditingContent}
|
||
className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50"
|
||
>
|
||
Change coffees for next month
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{!canChangeContent && (
|
||
<p className="mt-3 text-xs text-gray-600">
|
||
Coffee content can only be changed while a subscription is issued, ongoing, or paused.
|
||
</p>
|
||
)}
|
||
|
||
{includedItems.length === 0 ? (
|
||
<div className="mt-4 rounded-md bg-white/80 border border-gray-200 p-3 text-sm text-gray-600">
|
||
No included items were returned for this subscription.
|
||
</div>
|
||
) : (
|
||
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{includedItems.map((item, index) => {
|
||
const imageUrl = item.coffeeId ? coffeeImageById[String(item.coffeeId)] : ''
|
||
return (
|
||
<div key={`${item.coffeeId || 'coffee'}-${index}`} className="rounded-md bg-white/80 border border-gray-200 p-3">
|
||
<div className="flex items-start justify-between gap-3">
|
||
<div className="min-w-0">
|
||
<p className="text-sm font-semibold text-gray-900">{item.coffeeName || `Coffee #${item.coffeeId || index + 1}`}</p>
|
||
<p className="text-xs text-gray-600 mt-1">Coffee ID: {item.coffeeId || '—'}</p>
|
||
<p className="text-xs text-gray-600">Packs included: {Number(item.quantity) || 0}</p>
|
||
</div>
|
||
{imageUrl ? (
|
||
<img
|
||
src={imageUrl}
|
||
alt={item.coffeeName || `Coffee #${item.coffeeId || index + 1}`}
|
||
className="h-14 w-14 rounded-md object-cover border border-gray-200 shrink-0"
|
||
/>
|
||
) : (
|
||
<div className="h-14 w-14 rounded-md bg-gray-100 border border-gray-200 shrink-0" />
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{editingContent && canChangeContent && (
|
||
<div className="mt-4 rounded-md border border-gray-200 bg-white/90 p-4">
|
||
<div className="flex items-center justify-between gap-2 flex-wrap mb-3">
|
||
<h3 className="text-sm font-semibold text-gray-900">Edit coffee content</h3>
|
||
<p className="text-xs text-gray-600">Selected packs: {draftTotalPacks} (must be 6 or 12)</p>
|
||
</div>
|
||
|
||
{coffeesLoading ? (
|
||
<p className="text-sm text-gray-600">Loading available coffees…</p>
|
||
) : coffeesError ? (
|
||
<p className="text-sm text-red-600">{coffeesError}</p>
|
||
) : (
|
||
<div className="space-y-2 max-h-72 overflow-auto pr-1">
|
||
{coffees.map((coffee) => {
|
||
const key = String(coffee.id)
|
||
const qty = draftItems[key] || 0
|
||
return (
|
||
<div key={key} className="flex items-center justify-between gap-3 rounded-md border border-gray-200 px-3 py-2">
|
||
<div>
|
||
<p className="text-sm font-medium text-gray-900">{coffee.name}</p>
|
||
<p className="text-xs text-gray-600 line-clamp-1">{coffee.description || 'No description'}</p>
|
||
</div>
|
||
<input
|
||
type="number"
|
||
min={0}
|
||
step={1}
|
||
value={qty}
|
||
onChange={(e) => updateDraftQty(key, Number(e.target.value))}
|
||
className="w-20 rounded-md border border-gray-300 px-2 py-1 text-sm text-gray-900"
|
||
/>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{contentError && (
|
||
<p className="mt-3 text-xs text-red-600">{contentError}</p>
|
||
)}
|
||
|
||
<div className="mt-4 flex items-center gap-2">
|
||
<button
|
||
onClick={saveContentChanges}
|
||
disabled={savingContent || coffeesLoading}
|
||
className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed"
|
||
>
|
||
{savingContent ? 'Saving…' : 'Save changes'}
|
||
</button>
|
||
<button
|
||
onClick={cancelEditingContent}
|
||
disabled={savingContent}
|
||
className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed"
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<FinanceInvoices abonementId={selectedAbo.id} />
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</BlueBlurryBackground>
|
||
|
||
<ConfirmActionModal
|
||
open={Boolean(statusConfirmTarget)}
|
||
pending={statusBusy}
|
||
title={confirmTitle}
|
||
description={confirmDescription}
|
||
confirmText={confirmButtonText}
|
||
intent={statusConfirmTarget === 'cancelled' ? 'danger' : 'default'}
|
||
onClose={closeStatusConfirm}
|
||
onConfirm={confirmStatusChange}
|
||
/>
|
||
</PageLayout>
|
||
)
|
||
}
|