2026-06-25 16:45:11 +02:00
|
|
|
/**
|
|
|
|
|
* MeterManager — electricity meter timeline UI.
|
|
|
|
|
*
|
|
|
|
|
* Features:
|
|
|
|
|
* - Table of meter epochs: label / interval (started_at → ended_at or "active") /
|
|
|
|
|
* active badge / reason.
|
|
|
|
|
* - "Declare New Meter" button: form with label + date (started_at) + reason +
|
|
|
|
|
* optional note. Sends local-midnight naive datetime per FU10 convention.
|
|
|
|
|
* - Edit modal: update label, note, or correct started_at (retroactive).
|
|
|
|
|
* - Retroactive feedback: if started_at is changed, a success notice mentions
|
|
|
|
|
* that affected billing periods have been recomputed.
|
|
|
|
|
* - Loading / error / empty states.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { useState } from 'react'
|
|
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
Button,
|
|
|
|
|
Group,
|
|
|
|
|
Text,
|
|
|
|
|
Loader,
|
|
|
|
|
Center,
|
|
|
|
|
Alert,
|
|
|
|
|
Stack,
|
|
|
|
|
Badge,
|
|
|
|
|
ScrollArea,
|
|
|
|
|
Modal,
|
|
|
|
|
TextInput,
|
|
|
|
|
Textarea,
|
|
|
|
|
Select,
|
|
|
|
|
Notification,
|
|
|
|
|
} from '@mantine/core'
|
|
|
|
|
import {
|
|
|
|
|
useMeters,
|
|
|
|
|
useDeclareMeter,
|
|
|
|
|
useUpdateMeter,
|
|
|
|
|
type MeterResponse,
|
|
|
|
|
type MeterReason,
|
|
|
|
|
} from './hooks'
|
|
|
|
|
import { ApiError } from '../api/client'
|
2026-06-25 18:17:24 +02:00
|
|
|
import { formatLocalDate, parseBackendTimestamp } from '../utils/datetime'
|
2026-06-25 16:45:11 +02:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Helpers
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
const REASON_OPTIONS: { value: MeterReason; label: string }[] = [
|
|
|
|
|
{ value: 'initial', label: 'Initial installation' },
|
|
|
|
|
{ value: 'meter_swap', label: 'Meter swap (same address)' },
|
|
|
|
|
{ value: 'home_move', label: 'Home / address move' },
|
|
|
|
|
{ value: 'other', label: 'Other' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convert a local date string "YYYY-MM-DD" to a naive local-midnight datetime
|
|
|
|
|
* string (no Z suffix) following the FU10 / ContractForm convention.
|
|
|
|
|
* The backend interprets naive datetimes as server local wall-clock time.
|
|
|
|
|
*/
|
|
|
|
|
function toLocalMidnightNaive(dateStr: string): string {
|
|
|
|
|
return `${dateStr}T00:00:00`
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 18:17:24 +02:00
|
|
|
/**
|
|
|
|
|
* Format a Date object as a "YYYY-MM-DD" string using the browser's local timezone.
|
|
|
|
|
* Used to populate <input type="date"> fields.
|
|
|
|
|
* Returns '' if the Date is invalid.
|
|
|
|
|
*/
|
|
|
|
|
function toLocalDateInputString(d: Date): string {
|
|
|
|
|
if (isNaN(d.getTime())) return ''
|
|
|
|
|
const y = d.getFullYear()
|
|
|
|
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
|
|
|
return `${y}-${m}-${day}`
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 16:45:11 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Declare meter form (modal)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
interface DeclareMeterFormProps {
|
|
|
|
|
onClose: () => void
|
|
|
|
|
onSaved: () => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DeclareMeterForm({ onClose, onSaved }: DeclareMeterFormProps) {
|
|
|
|
|
const [label, setLabel] = useState('')
|
|
|
|
|
const [dateStr, setDateStr] = useState('')
|
|
|
|
|
const [reason, setReason] = useState<string | null>(null)
|
|
|
|
|
const [note, setNote] = useState('')
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
const declareMutation = useDeclareMeter()
|
|
|
|
|
|
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
setError(null)
|
|
|
|
|
|
|
|
|
|
if (!label.trim()) {
|
|
|
|
|
setError('Label is required.')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (!dateStr) {
|
|
|
|
|
setError('Start date is required.')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (!reason) {
|
|
|
|
|
setError('Reason is required.')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await declareMutation.mutateAsync({
|
|
|
|
|
label: label.trim(),
|
|
|
|
|
started_at: toLocalMidnightNaive(dateStr),
|
|
|
|
|
reason: reason as MeterReason,
|
|
|
|
|
note: note.trim() || undefined,
|
|
|
|
|
commodity: 'electricity',
|
|
|
|
|
})
|
|
|
|
|
onSaved()
|
|
|
|
|
onClose()
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof ApiError) {
|
|
|
|
|
const detail = (err.body as { detail?: string } | null)?.detail
|
|
|
|
|
setError(detail ?? `Error ${err.status}: failed to declare meter.`)
|
|
|
|
|
} else {
|
|
|
|
|
setError('Failed to declare meter. Please try again.')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
opened
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
title="Declare New Meter"
|
|
|
|
|
size="md"
|
|
|
|
|
data-testid="declare-meter-modal"
|
|
|
|
|
>
|
|
|
|
|
<form onSubmit={handleSubmit} data-testid="declare-meter-form">
|
|
|
|
|
<Stack gap="sm">
|
|
|
|
|
<TextInput
|
|
|
|
|
label="Label"
|
|
|
|
|
description={'Human-readable identifier, e.g. "2G meter @ Dorpsstraat 1"'}
|
|
|
|
|
required
|
|
|
|
|
value={label}
|
|
|
|
|
onChange={(e) => setLabel(e.currentTarget.value)}
|
|
|
|
|
data-testid="meter-label"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<TextInput
|
|
|
|
|
label="Start date"
|
|
|
|
|
description="Date in YYYY-MM-DD format (interpreted as local midnight)"
|
|
|
|
|
type="date"
|
|
|
|
|
required
|
|
|
|
|
value={dateStr}
|
|
|
|
|
onChange={(e) => setDateStr(e.currentTarget.value)}
|
|
|
|
|
data-testid="meter-started-at"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Select
|
|
|
|
|
label="Reason"
|
|
|
|
|
required
|
|
|
|
|
data={REASON_OPTIONS}
|
|
|
|
|
value={reason}
|
|
|
|
|
onChange={setReason}
|
|
|
|
|
data-testid="meter-reason"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Textarea
|
|
|
|
|
label="Note (optional)"
|
|
|
|
|
value={note}
|
|
|
|
|
onChange={(e) => setNote(e.currentTarget.value)}
|
|
|
|
|
autosize
|
|
|
|
|
minRows={2}
|
|
|
|
|
data-testid="meter-note"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<Alert color="red" data-testid="declare-meter-error">
|
|
|
|
|
{error}
|
|
|
|
|
</Alert>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Group justify="flex-end" gap="sm">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="default"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
data-testid="declare-meter-cancel"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
loading={declareMutation.isPending}
|
|
|
|
|
data-testid="declare-meter-submit"
|
|
|
|
|
>
|
|
|
|
|
Declare Meter
|
|
|
|
|
</Button>
|
|
|
|
|
</Group>
|
|
|
|
|
</Stack>
|
|
|
|
|
</form>
|
|
|
|
|
</Modal>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Edit meter form (modal)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
interface EditMeterFormProps {
|
|
|
|
|
meter: MeterResponse
|
|
|
|
|
onClose: () => void
|
|
|
|
|
onSaved: (retroactive: boolean) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EditMeterForm({ meter, onClose, onSaved }: EditMeterFormProps) {
|
|
|
|
|
const [label, setLabel] = useState(meter.label)
|
|
|
|
|
const [note, setNote] = useState(meter.note ?? '')
|
2026-06-25 18:17:24 +02:00
|
|
|
// Convert started_at to a local date string for the <input type="date">.
|
|
|
|
|
// parseBackendTimestamp correctly handles naive strings (no tz marker → treated as UTC,
|
|
|
|
|
// matching the backend's storage convention), Z-suffixed strings, and strings with
|
|
|
|
|
// explicit offsets like +02:00. We then extract the local-timezone date components
|
|
|
|
|
// so the displayed date matches the local wall-clock date of the meter start.
|
|
|
|
|
const initialDateStr = toLocalDateInputString(parseBackendTimestamp(meter.started_at))
|
2026-06-25 16:45:11 +02:00
|
|
|
const [dateStr, setDateStr] = useState(initialDateStr)
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
const updateMutation = useUpdateMeter()
|
|
|
|
|
|
|
|
|
|
// Detect if the user changed started_at (retroactive correction).
|
|
|
|
|
const startedAtChanged = dateStr !== initialDateStr
|
|
|
|
|
|
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
setError(null)
|
|
|
|
|
|
|
|
|
|
const patchBody: { label?: string | null; note?: string | null; started_at?: string | null } = {}
|
|
|
|
|
if (label.trim() !== meter.label) patchBody.label = label.trim()
|
|
|
|
|
const noteVal = note.trim() || null
|
|
|
|
|
if (noteVal !== meter.note) patchBody.note = noteVal
|
|
|
|
|
if (startedAtChanged && dateStr) {
|
|
|
|
|
patchBody.started_at = toLocalMidnightNaive(dateStr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Object.keys(patchBody).length === 0) {
|
|
|
|
|
onClose()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await updateMutation.mutateAsync({ id: meter.id, body: patchBody })
|
|
|
|
|
onSaved(startedAtChanged)
|
|
|
|
|
onClose()
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof ApiError) {
|
|
|
|
|
const detail = (err.body as { detail?: string } | null)?.detail
|
|
|
|
|
setError(detail ?? `Error ${err.status}: failed to update meter.`)
|
|
|
|
|
} else {
|
|
|
|
|
setError('Failed to update meter. Please try again.')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
opened
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
title={`Edit Meter — ${meter.label}`}
|
|
|
|
|
size="md"
|
|
|
|
|
data-testid="edit-meter-modal"
|
|
|
|
|
>
|
|
|
|
|
<form onSubmit={handleSubmit} data-testid="edit-meter-form">
|
|
|
|
|
<Stack gap="sm">
|
|
|
|
|
<TextInput
|
|
|
|
|
label="Label"
|
|
|
|
|
required
|
|
|
|
|
value={label}
|
|
|
|
|
onChange={(e) => setLabel(e.currentTarget.value)}
|
|
|
|
|
data-testid="edit-meter-label"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<TextInput
|
|
|
|
|
label="Start date"
|
|
|
|
|
description="Retroactive correction: shifts the epoch boundary and re-judges billing periods"
|
|
|
|
|
type="date"
|
|
|
|
|
value={dateStr}
|
|
|
|
|
onChange={(e) => setDateStr(e.currentTarget.value)}
|
|
|
|
|
data-testid="edit-meter-started-at"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Textarea
|
|
|
|
|
label="Note (optional)"
|
|
|
|
|
value={note}
|
|
|
|
|
onChange={(e) => setNote(e.currentTarget.value)}
|
|
|
|
|
autosize
|
|
|
|
|
minRows={2}
|
|
|
|
|
data-testid="edit-meter-note"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<Alert color="red" data-testid="edit-meter-error">
|
|
|
|
|
{error}
|
|
|
|
|
</Alert>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Group justify="flex-end" gap="sm">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="default"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
data-testid="edit-meter-cancel"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
loading={updateMutation.isPending}
|
|
|
|
|
data-testid="edit-meter-submit"
|
|
|
|
|
>
|
|
|
|
|
Save
|
|
|
|
|
</Button>
|
|
|
|
|
</Group>
|
|
|
|
|
</Stack>
|
|
|
|
|
</form>
|
|
|
|
|
</Modal>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Meter timeline table
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
interface MeterTableProps {
|
|
|
|
|
meters: MeterResponse[]
|
|
|
|
|
onEdit: (meter: MeterResponse) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function MeterTable({ meters, onEdit }: MeterTableProps) {
|
|
|
|
|
if (meters.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<Text c="dimmed" ta="center" size="sm" data-testid="meters-empty">
|
|
|
|
|
No meters declared yet. Click "Declare New Meter" to add one.
|
|
|
|
|
</Text>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ScrollArea>
|
|
|
|
|
<Table striped highlightOnHover withTableBorder data-testid="meters-table">
|
|
|
|
|
<Table.Thead>
|
|
|
|
|
<Table.Tr>
|
|
|
|
|
<Table.Th>Label</Table.Th>
|
|
|
|
|
<Table.Th>Commodity</Table.Th>
|
|
|
|
|
<Table.Th>From</Table.Th>
|
|
|
|
|
<Table.Th>To</Table.Th>
|
|
|
|
|
<Table.Th>Status</Table.Th>
|
|
|
|
|
<Table.Th>Reason</Table.Th>
|
|
|
|
|
<Table.Th style={{ textAlign: 'right' }}>Actions</Table.Th>
|
|
|
|
|
</Table.Tr>
|
|
|
|
|
</Table.Thead>
|
|
|
|
|
<Table.Tbody>
|
|
|
|
|
{meters.map((meter) => {
|
|
|
|
|
const isActive = meter.ended_at === null
|
|
|
|
|
return (
|
|
|
|
|
<Table.Tr key={meter.id} data-testid={`meter-row-${meter.id}`}>
|
|
|
|
|
<Table.Td>
|
|
|
|
|
<Text fw={500} size="sm">
|
|
|
|
|
{meter.label}
|
|
|
|
|
</Text>
|
|
|
|
|
</Table.Td>
|
|
|
|
|
<Table.Td>
|
|
|
|
|
<Badge variant="outline" size="sm">
|
|
|
|
|
{meter.commodity}
|
|
|
|
|
</Badge>
|
|
|
|
|
</Table.Td>
|
|
|
|
|
<Table.Td>
|
|
|
|
|
<Text size="xs" c="dimmed">
|
|
|
|
|
{formatLocalDate(meter.started_at)}
|
|
|
|
|
</Text>
|
|
|
|
|
</Table.Td>
|
|
|
|
|
<Table.Td>
|
|
|
|
|
<Text size="xs" c="dimmed">
|
|
|
|
|
{meter.ended_at ? formatLocalDate(meter.ended_at) : '—'}
|
|
|
|
|
</Text>
|
|
|
|
|
</Table.Td>
|
|
|
|
|
<Table.Td>
|
|
|
|
|
<Badge
|
|
|
|
|
color={isActive ? 'green' : 'gray'}
|
|
|
|
|
variant="light"
|
|
|
|
|
size="sm"
|
|
|
|
|
data-testid={`meter-status-${meter.id}`}
|
|
|
|
|
>
|
|
|
|
|
{isActive ? 'active' : 'closed'}
|
|
|
|
|
</Badge>
|
|
|
|
|
</Table.Td>
|
|
|
|
|
<Table.Td>
|
|
|
|
|
<Badge variant="outline" size="sm" color="blue">
|
|
|
|
|
{meter.reason}
|
|
|
|
|
</Badge>
|
|
|
|
|
</Table.Td>
|
|
|
|
|
<Table.Td>
|
|
|
|
|
<Group justify="flex-end" gap="xs">
|
|
|
|
|
<Button
|
|
|
|
|
size="xs"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => onEdit(meter)}
|
|
|
|
|
data-testid={`meter-edit-${meter.id}`}
|
|
|
|
|
>
|
|
|
|
|
Edit
|
|
|
|
|
</Button>
|
|
|
|
|
</Group>
|
|
|
|
|
</Table.Td>
|
|
|
|
|
</Table.Tr>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</Table.Tbody>
|
|
|
|
|
</Table>
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// MeterManager — top-level
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
export function MeterManager() {
|
|
|
|
|
const metersQuery = useMeters()
|
|
|
|
|
|
|
|
|
|
const [showDeclareForm, setShowDeclareForm] = useState(false)
|
|
|
|
|
const [editMeter, setEditMeter] = useState<MeterResponse | null>(null)
|
|
|
|
|
const [recomputeNotice, setRecomputeNotice] = useState(false)
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Render states
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
if (metersQuery.isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<Center py="xl" data-testid="meters-loading">
|
|
|
|
|
<Loader />
|
|
|
|
|
</Center>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (metersQuery.isError || !metersQuery.data) {
|
|
|
|
|
return (
|
|
|
|
|
<Alert color="red" data-testid="meters-load-error">
|
|
|
|
|
Failed to load meters. Please refresh.
|
|
|
|
|
</Alert>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const meters = metersQuery.data.items
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Stack gap="lg" data-testid="meter-manager">
|
|
|
|
|
<Group justify="space-between" align="center">
|
|
|
|
|
<Text fw={500}>Electricity Meters</Text>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => setShowDeclareForm(true)}
|
|
|
|
|
data-testid="meter-declare-button"
|
|
|
|
|
>
|
|
|
|
|
Declare New Meter
|
|
|
|
|
</Button>
|
|
|
|
|
</Group>
|
|
|
|
|
|
|
|
|
|
{recomputeNotice && (
|
|
|
|
|
<Notification
|
|
|
|
|
color="teal"
|
|
|
|
|
title="Billing periods recomputed"
|
|
|
|
|
onClose={() => setRecomputeNotice(false)}
|
|
|
|
|
data-testid="meter-recompute-notice"
|
|
|
|
|
>
|
|
|
|
|
The start date was corrected. Affected billing periods have been
|
|
|
|
|
re-judged and cost attributions updated.
|
|
|
|
|
</Notification>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<MeterTable meters={meters} onEdit={(m) => setEditMeter(m)} />
|
|
|
|
|
|
|
|
|
|
{/* Declare new meter */}
|
|
|
|
|
{showDeclareForm && (
|
|
|
|
|
<DeclareMeterForm
|
|
|
|
|
onClose={() => setShowDeclareForm(false)}
|
|
|
|
|
onSaved={() => setShowDeclareForm(false)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Edit existing meter */}
|
|
|
|
|
{editMeter && (
|
|
|
|
|
<EditMeterForm
|
|
|
|
|
meter={editMeter}
|
|
|
|
|
onClose={() => setEditMeter(null)}
|
|
|
|
|
onSaved={(retroactive) => {
|
|
|
|
|
setEditMeter(null)
|
|
|
|
|
if (retroactive) setRecomputeNotice(true)
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</Stack>
|
|
|
|
|
)
|
|
|
|
|
}
|