[CLAUDE] CICD+FE-Admin+FE-User: deploy pool-state guard + SlaTimer component
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
CICD: check app pool state before Stop-WebAppPool (idempotent). FE: new SlaTimer component with color-coded countdown (emerald/amber/red) and progress bar. Two variants: - inline: used in list tables (Inbox, Contracts list x2, MyContracts) - full: used in ContractDetail card with progress bar + deadline timestamp Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -65,8 +65,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
Import-Module WebAdministration
|
Import-Module WebAdministration
|
||||||
|
|
||||||
# Stop app pool so DLLs are writable
|
# Stop app pool (if running) so DLLs are writable
|
||||||
if (Get-WebAppPoolState -Name SolutionErp-Api -ErrorAction SilentlyContinue) {
|
$poolState = (Get-WebAppPoolState -Name SolutionErp-Api -ErrorAction SilentlyContinue).Value
|
||||||
|
if ($poolState -eq 'Started') {
|
||||||
Stop-WebAppPool -Name SolutionErp-Api
|
Stop-WebAppPool -Name SolutionErp-Api
|
||||||
Start-Sleep -Seconds 3
|
Start-Sleep -Seconds 3
|
||||||
}
|
}
|
||||||
|
|||||||
88
fe-admin/src/components/SlaTimer.tsx
Normal file
88
fe-admin/src/components/SlaTimer.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { cn } from '@/lib/cn'
|
||||||
|
|
||||||
|
type Tone = 'muted' | 'safe' | 'warn' | 'danger'
|
||||||
|
|
||||||
|
type ComputedSla = {
|
||||||
|
tone: Tone
|
||||||
|
label: string
|
||||||
|
percent: number
|
||||||
|
deadline: Date | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function compute(deadline: string | null, createdAt: string | null | undefined): ComputedSla {
|
||||||
|
if (!deadline) return { tone: 'muted', label: 'Không có SLA', percent: 0, deadline: null }
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const end = new Date(deadline).getTime()
|
||||||
|
const start = createdAt ? new Date(createdAt).getTime() : end - 7 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
const remaining = end - now
|
||||||
|
const total = Math.max(end - start, 1)
|
||||||
|
const elapsed = Math.min(Math.max(now - start, 0), total)
|
||||||
|
const percent = (elapsed / total) * 100
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
const overdueMs = -remaining
|
||||||
|
const d = Math.floor(overdueMs / 86_400_000)
|
||||||
|
const h = Math.floor((overdueMs % 86_400_000) / 3_600_000)
|
||||||
|
const overdueLabel = d > 0 ? `${d} ngày ${h}h` : `${h}h`
|
||||||
|
return { tone: 'danger', label: `Quá hạn ${overdueLabel}`, percent: 100, deadline: new Date(end) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = Math.floor(remaining / 86_400_000)
|
||||||
|
const hours = Math.floor((remaining % 86_400_000) / 3_600_000)
|
||||||
|
const remainingLabel = days > 0 ? `${days} ngày ${hours}h` : `${hours}h`
|
||||||
|
|
||||||
|
const tone: Tone = percent >= 80 ? 'warn' : 'safe'
|
||||||
|
return { tone, label: `Còn ${remainingLabel}`, percent, deadline: new Date(end) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const toneClasses: Record<Tone, { text: string; bar: string; track: string }> = {
|
||||||
|
muted: { text: 'text-slate-400', bar: 'bg-slate-300', track: 'bg-slate-100' },
|
||||||
|
safe: { text: 'text-emerald-600', bar: 'bg-emerald-500', track: 'bg-emerald-50' },
|
||||||
|
warn: { text: 'text-amber-600', bar: 'bg-amber-500', track: 'bg-amber-50' },
|
||||||
|
danger: { text: 'text-red-600', bar: 'bg-red-500', track: 'bg-red-50' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SlaTimer({
|
||||||
|
deadline,
|
||||||
|
createdAt,
|
||||||
|
variant = 'inline',
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
deadline: string | null
|
||||||
|
createdAt?: string | null
|
||||||
|
variant?: 'inline' | 'full'
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const s = compute(deadline, createdAt)
|
||||||
|
const cls = toneClasses[s.tone]
|
||||||
|
|
||||||
|
if (variant === 'inline') {
|
||||||
|
return (
|
||||||
|
<span className={cn('inline-flex items-center gap-2', className)}>
|
||||||
|
{s.tone === 'danger' && <span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-red-500" />}
|
||||||
|
<span className={cn('text-xs font-medium', cls.text)}>{s.label}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('space-y-1.5', className)}>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className={cn('font-medium', cls.text)}>{s.label}</span>
|
||||||
|
{s.deadline && (
|
||||||
|
<span className="text-slate-400">
|
||||||
|
Hạn: {s.deadline.toLocaleString('vi-VN', { dateStyle: 'short', timeStyle: 'short' })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={cn('h-1.5 overflow-hidden rounded-full', cls.track)}>
|
||||||
|
<div
|
||||||
|
className={cn('h-full rounded-full transition-all', cls.bar)}
|
||||||
|
style={{ width: `${Math.min(s.percent, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import { ArrowRight, CheckCircle2, MessageSquare, Clock, ArrowLeft, XCircle } fr
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||||
|
import { SlaTimer } from '@/components/SlaTimer'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { Select } from '@/components/ui/Select'
|
import { Select } from '@/components/ui/Select'
|
||||||
import { Textarea } from '@/components/ui/Textarea'
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
@ -140,7 +141,7 @@ export function ContractDetailPage() {
|
|||||||
<div><dt className="text-slate-500">Dự án</dt><dd>{c.projectName}</dd></div>
|
<div><dt className="text-slate-500">Dự án</dt><dd>{c.projectName}</dd></div>
|
||||||
<div><dt className="text-slate-500">Phòng ban</dt><dd>{c.departmentName ?? '—'}</dd></div>
|
<div><dt className="text-slate-500">Phòng ban</dt><dd>{c.departmentName ?? '—'}</dd></div>
|
||||||
<div><dt className="text-slate-500">Người soạn</dt><dd>{c.drafterName ?? '—'}</dd></div>
|
<div><dt className="text-slate-500">Người soạn</dt><dd>{c.drafterName ?? '—'}</dd></div>
|
||||||
<div><dt className="text-slate-500">SLA</dt><dd>{c.slaDeadline ? fmt(c.slaDeadline) : '—'}</dd></div>
|
<div className="col-span-2"><dt className="text-slate-500 mb-1">SLA</dt><dd><SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} variant="full" /></dd></div>
|
||||||
<div><dt className="text-slate-500">Bypass CCM</dt><dd>{c.bypassProcurementAndCCM ? 'Có (HĐ Chủ đầu tư)' : 'Không'}</dd></div>
|
<div><dt className="text-slate-500">Bypass CCM</dt><dd>{c.bypassProcurementAndCCM ? 'Có (HĐ Chủ đầu tư)' : 'Không'}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
{c.noiDung && (
|
{c.noiDung && (
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { DataTable, Pagination, type Column } from '@/components/DataTable'
|
import { DataTable, Pagination, type Column } from '@/components/DataTable'
|
||||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||||
|
import { SlaTimer } from '@/components/SlaTimer'
|
||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Select } from '@/components/ui/Select'
|
import { Select } from '@/components/ui/Select'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
@ -12,15 +13,6 @@ import { ContractPhase, ContractPhaseLabel, type ContractListItem } from '@/type
|
|||||||
import { ContractTypeLabel } from '@/types/forms'
|
import { ContractTypeLabel } from '@/types/forms'
|
||||||
|
|
||||||
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
||||||
const fmtSla = (s: string | null) => {
|
|
||||||
if (!s) return '—'
|
|
||||||
const ms = new Date(s).getTime() - Date.now()
|
|
||||||
const days = Math.floor(ms / (1000 * 60 * 60 * 24))
|
|
||||||
const hours = Math.floor((ms % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
|
||||||
if (ms < 0) return <span className="text-red-600">Quá hạn</span>
|
|
||||||
if (days > 0) return `còn ${days}d ${hours}h`
|
|
||||||
return <span className="text-amber-600">còn {hours}h</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContractsListPage() {
|
export function ContractsListPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -46,7 +38,7 @@ export function ContractsListPage() {
|
|||||||
{ key: 'supplierName', header: 'NCC', render: c => c.supplierName },
|
{ key: 'supplierName', header: 'NCC', render: c => c.supplierName },
|
||||||
{ key: 'projectName', header: 'Dự án', render: c => c.projectName },
|
{ key: 'projectName', header: 'Dự án', render: c => c.projectName },
|
||||||
{ key: 'giaTri', header: 'Giá trị', align: 'right', width: 'w-32', render: c => fmtMoney(c.giaTri) },
|
{ key: 'giaTri', header: 'Giá trị', align: 'right', width: 'w-32', render: c => fmtMoney(c.giaTri) },
|
||||||
{ key: 'slaDeadline', header: 'SLA', width: 'w-32', render: c => fmtSla(c.slaDeadline) },
|
{ key: 'slaDeadline', header: 'SLA', width: 'w-40', render: c => <SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} /> },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
88
fe-user/src/components/SlaTimer.tsx
Normal file
88
fe-user/src/components/SlaTimer.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { cn } from '@/lib/cn'
|
||||||
|
|
||||||
|
type Tone = 'muted' | 'safe' | 'warn' | 'danger'
|
||||||
|
|
||||||
|
type ComputedSla = {
|
||||||
|
tone: Tone
|
||||||
|
label: string
|
||||||
|
percent: number
|
||||||
|
deadline: Date | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function compute(deadline: string | null, createdAt: string | null | undefined): ComputedSla {
|
||||||
|
if (!deadline) return { tone: 'muted', label: 'Không có SLA', percent: 0, deadline: null }
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const end = new Date(deadline).getTime()
|
||||||
|
const start = createdAt ? new Date(createdAt).getTime() : end - 7 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
const remaining = end - now
|
||||||
|
const total = Math.max(end - start, 1)
|
||||||
|
const elapsed = Math.min(Math.max(now - start, 0), total)
|
||||||
|
const percent = (elapsed / total) * 100
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
const overdueMs = -remaining
|
||||||
|
const d = Math.floor(overdueMs / 86_400_000)
|
||||||
|
const h = Math.floor((overdueMs % 86_400_000) / 3_600_000)
|
||||||
|
const overdueLabel = d > 0 ? `${d} ngày ${h}h` : `${h}h`
|
||||||
|
return { tone: 'danger', label: `Quá hạn ${overdueLabel}`, percent: 100, deadline: new Date(end) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = Math.floor(remaining / 86_400_000)
|
||||||
|
const hours = Math.floor((remaining % 86_400_000) / 3_600_000)
|
||||||
|
const remainingLabel = days > 0 ? `${days} ngày ${hours}h` : `${hours}h`
|
||||||
|
|
||||||
|
const tone: Tone = percent >= 80 ? 'warn' : 'safe'
|
||||||
|
return { tone, label: `Còn ${remainingLabel}`, percent, deadline: new Date(end) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const toneClasses: Record<Tone, { text: string; bar: string; track: string }> = {
|
||||||
|
muted: { text: 'text-slate-400', bar: 'bg-slate-300', track: 'bg-slate-100' },
|
||||||
|
safe: { text: 'text-emerald-600', bar: 'bg-emerald-500', track: 'bg-emerald-50' },
|
||||||
|
warn: { text: 'text-amber-600', bar: 'bg-amber-500', track: 'bg-amber-50' },
|
||||||
|
danger: { text: 'text-red-600', bar: 'bg-red-500', track: 'bg-red-50' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SlaTimer({
|
||||||
|
deadline,
|
||||||
|
createdAt,
|
||||||
|
variant = 'inline',
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
deadline: string | null
|
||||||
|
createdAt?: string | null
|
||||||
|
variant?: 'inline' | 'full'
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const s = compute(deadline, createdAt)
|
||||||
|
const cls = toneClasses[s.tone]
|
||||||
|
|
||||||
|
if (variant === 'inline') {
|
||||||
|
return (
|
||||||
|
<span className={cn('inline-flex items-center gap-2', className)}>
|
||||||
|
{s.tone === 'danger' && <span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-red-500" />}
|
||||||
|
<span className={cn('text-xs font-medium', cls.text)}>{s.label}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('space-y-1.5', className)}>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className={cn('font-medium', cls.text)}>{s.label}</span>
|
||||||
|
{s.deadline && (
|
||||||
|
<span className="text-slate-400">
|
||||||
|
Hạn: {s.deadline.toLocaleString('vi-VN', { dateStyle: 'short', timeStyle: 'short' })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={cn('h-1.5 overflow-hidden rounded-full', cls.track)}>
|
||||||
|
<div
|
||||||
|
className={cn('h-full rounded-full transition-all', cls.bar)}
|
||||||
|
style={{ width: `${Math.min(s.percent, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -4,21 +4,13 @@ import { Inbox } from 'lucide-react'
|
|||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { DataTable, type Column } from '@/components/DataTable'
|
import { DataTable, type Column } from '@/components/DataTable'
|
||||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||||
|
import { SlaTimer } from '@/components/SlaTimer'
|
||||||
import { useAuth } from '@/contexts/AuthContext'
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import type { ContractListItem } from '@/types/contracts'
|
import type { ContractListItem } from '@/types/contracts'
|
||||||
import { ContractTypeLabel } from '@/types/forms'
|
import { ContractTypeLabel } from '@/types/forms'
|
||||||
|
|
||||||
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
||||||
const fmtSla = (s: string | null) => {
|
|
||||||
if (!s) return '—'
|
|
||||||
const ms = new Date(s).getTime() - Date.now()
|
|
||||||
const days = Math.floor(ms / (1000 * 60 * 60 * 24))
|
|
||||||
const hours = Math.floor((ms % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
|
||||||
if (ms < 0) return <span className="text-red-600">Quá hạn</span>
|
|
||||||
if (days > 0) return `còn ${days}d ${hours}h`
|
|
||||||
return <span className="text-amber-600">còn {hours}h</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InboxPage() {
|
export function InboxPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -36,7 +28,7 @@ export function InboxPage() {
|
|||||||
{ key: 'phase', header: 'Phase hiện tại', width: 'w-36', render: c => <PhaseBadge phase={c.phase} /> },
|
{ key: 'phase', header: 'Phase hiện tại', width: 'w-36', render: c => <PhaseBadge phase={c.phase} /> },
|
||||||
{ key: 'supplierName', header: 'NCC', render: c => c.supplierName },
|
{ key: 'supplierName', header: 'NCC', render: c => c.supplierName },
|
||||||
{ key: 'giaTri', header: 'Giá trị', align: 'right', width: 'w-32', render: c => fmtMoney(c.giaTri) },
|
{ key: 'giaTri', header: 'Giá trị', align: 'right', width: 'w-32', render: c => fmtMoney(c.giaTri) },
|
||||||
{ key: 'slaDeadline', header: 'SLA', width: 'w-32', render: c => fmtSla(c.slaDeadline) },
|
{ key: 'slaDeadline', header: 'SLA', width: 'w-40', render: c => <SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} /> },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { ArrowRight, CheckCircle2, MessageSquare, Clock, ArrowLeft, XCircle } fr
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||||
|
import { SlaTimer } from '@/components/SlaTimer'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { Select } from '@/components/ui/Select'
|
import { Select } from '@/components/ui/Select'
|
||||||
import { Textarea } from '@/components/ui/Textarea'
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
@ -137,7 +138,7 @@ export function ContractDetailPage() {
|
|||||||
<div><dt className="text-slate-500">NCC</dt><dd>{c.supplierName}</dd></div>
|
<div><dt className="text-slate-500">NCC</dt><dd>{c.supplierName}</dd></div>
|
||||||
<div><dt className="text-slate-500">Dự án</dt><dd>{c.projectName}</dd></div>
|
<div><dt className="text-slate-500">Dự án</dt><dd>{c.projectName}</dd></div>
|
||||||
<div><dt className="text-slate-500">Người soạn</dt><dd>{c.drafterName ?? '—'}</dd></div>
|
<div><dt className="text-slate-500">Người soạn</dt><dd>{c.drafterName ?? '—'}</dd></div>
|
||||||
<div><dt className="text-slate-500">SLA</dt><dd>{c.slaDeadline ? fmt(c.slaDeadline) : '—'}</dd></div>
|
<div className="col-span-2"><dt className="text-slate-500 mb-1">SLA</dt><dd><SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} variant="full" /></dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
{c.noiDung && (
|
{c.noiDung && (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { FileText } from 'lucide-react'
|
|||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { DataTable, type Column } from '@/components/DataTable'
|
import { DataTable, type Column } from '@/components/DataTable'
|
||||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||||
|
import { SlaTimer } from '@/components/SlaTimer'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import type { Paged } from '@/types/master'
|
import type { Paged } from '@/types/master'
|
||||||
import type { ContractListItem } from '@/types/contracts'
|
import type { ContractListItem } from '@/types/contracts'
|
||||||
@ -26,6 +27,7 @@ export function MyContractsPage() {
|
|||||||
{ key: 'phase', header: 'Phase', width: 'w-36', render: c => <PhaseBadge phase={c.phase} /> },
|
{ key: 'phase', header: 'Phase', width: 'w-36', render: c => <PhaseBadge phase={c.phase} /> },
|
||||||
{ key: 'supplierName', header: 'NCC', render: c => c.supplierName },
|
{ key: 'supplierName', header: 'NCC', render: c => c.supplierName },
|
||||||
{ key: 'giaTri', header: 'Giá trị', align: 'right', width: 'w-32', render: c => fmtMoney(c.giaTri) },
|
{ key: 'giaTri', header: 'Giá trị', align: 'right', width: 'w-32', render: c => fmtMoney(c.giaTri) },
|
||||||
|
{ key: 'slaDeadline', header: 'SLA', width: 'w-40', render: c => <SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} /> },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user