[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:
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user