Fix LogsPage missing after clone by correcting frontend .gitignore

This commit is contained in:
debu-2 2026-10-02 11:41:33 +05:30
parent efa5080897
commit 38eb2a11d5
2 changed files with 279 additions and 2 deletions

View file

@ -0,0 +1,277 @@
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { logsApi, type LogType, type LogsResponse } from '../../lib/api'
const LOG_TYPES: Array<{ value: LogType; label: string }> = [
{ value: 'outbound', label: 'Outbound' },
{ value: 'inbound', label: 'Inbound' },
{ value: 'sms', label: 'SMS' },
{ value: 'otp', label: 'OTP' },
]
const PAGE_SIZE = 10
const DEFAULT_RANGE_DAYS = 7
const MAX_RANGE_DAYS = 31
function formatDateInput(date: Date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
function startOfDay(date: Date) {
const copy = new Date(date)
copy.setHours(0, 0, 0, 0)
return copy
}
function endOfDay(date: Date) {
const copy = new Date(date)
copy.setHours(23, 59, 59, 999)
return copy
}
function formatTimestamp(value: string) {
return new Date(value).toLocaleString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
function formatPhone(value: string) {
const digits = value.replace(/\D/g, '')
if (digits.length === 12 && digits.startsWith('91')) {
return `+91 ${digits.slice(2, 7)} ${digits.slice(7)}`
}
return value
}
function statusClass(status: string) {
if (['sent', 'delivered', 'accepted', 'ended', 'active'].includes(status)) return 'success'
if (['failed', 'no_answer'].includes(status)) return 'failed'
return 'pending'
}
function defaultRange() {
const to = new Date()
const from = new Date()
from.setDate(from.getDate() - DEFAULT_RANGE_DAYS)
return { from: formatDateInput(from), to: formatDateInput(to) }
}
export function LogsPage() {
const initialRange = useMemo(() => defaultRange(), [])
const [logType, setLogType] = useState<LogType>('outbound')
const [fromDate, setFromDate] = useState(initialRange.from)
const [toDate, setToDate] = useState(initialRange.to)
const [page, setPage] = useState(1)
const [data, setData] = useState<LogsResponse | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const rangeError = useMemo(() => {
const from = startOfDay(new Date(fromDate))
const to = endOfDay(new Date(toDate))
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) {
return 'Enter valid dates.'
}
if (from > to) {
return 'Start date must be before end date.'
}
const spanDays = Math.ceil((to.getTime() - from.getTime()) / (1000 * 60 * 60 * 24))
if (spanDays > MAX_RANGE_DAYS) {
return `Maximum range is ${MAX_RANGE_DAYS} days.`
}
return ''
}, [fromDate, toDate])
const loadLogs = useCallback(async () => {
if (rangeError) {
setLoading(false)
return
}
setLoading(true)
setError('')
try {
const fromIso = startOfDay(new Date(fromDate)).toISOString()
const toIso = endOfDay(new Date(toDate)).toISOString()
const response = await logsApi.list({
type: logType,
from: fromIso,
to: toIso,
page,
})
setData(response)
} catch (reason) {
setData(null)
setError(reason instanceof Error ? reason.message : 'Unable to load logs')
} finally {
setLoading(false)
}
}, [fromDate, logType, page, rangeError, toDate])
useEffect(() => {
loadLogs()
}, [loadLogs])
function changeType(nextType: LogType) {
setLogType(nextType)
setPage(1)
}
function applyDateChange(kind: 'from' | 'to', value: string) {
if (kind === 'from') setFromDate(value)
else setToDate(value)
setPage(1)
}
const columns = useMemo(() => {
switch (logType) {
case 'sms':
return ['From', 'To', 'Template', 'Status', 'Time']
case 'otp':
return ['To', 'OTP type', 'Project', 'Status', 'Time']
case 'inbound':
return ['From', 'To', 'Status', 'Time']
default:
return ['From', 'To', 'Socket', 'Status', 'Time']
}
}, [logType])
const rows = data?.items ?? []
const totalPages = data?.total_pages ?? 0
return (
<div className="logs-page">
<div className="account-heading">
<span className="eyebrow">Test & observe</span>
<h1>Logs</h1>
<p>Browse outbound and inbound calls, SMS sends, and OTP deliveries. Default view covers the last week, up to one month.</p>
</div>
<section className="panel logs-toolbar">
<div className="logs-toolbar-row">
<label className="logs-field">
<span>Log type</span>
<select value={logType} onChange={(event) => changeType(event.target.value as LogType)}>
{LOG_TYPES.map((item) => (
<option key={item.value} value={item.value}>{item.label}</option>
))}
</select>
</label>
<label className="logs-field">
<span>From</span>
<input
type="date"
value={fromDate}
max={toDate}
onChange={(event) => applyDateChange('from', event.target.value)}
/>
</label>
<label className="logs-field">
<span>To</span>
<input
type="date"
value={toDate}
min={fromDate}
max={formatDateInput(new Date())}
onChange={(event) => applyDateChange('to', event.target.value)}
/>
</label>
</div>
<div className="logs-toolbar-meta">
<span>{PAGE_SIZE} records per page</span>
{data && (
<span>
Showing page {data.page} of {Math.max(totalPages, 1)} · {data.total} total
</span>
)}
</div>
</section>
{(rangeError || error) && (
<p className="auth-error">{rangeError || error}</p>
)}
<section className="panel logs-table-panel">
{loading ? (
<p className="explorer-loading">Loading logs…</p>
) : rows.length === 0 ? (
<p className="logs-empty">No {logType} logs in this time range.</p>
) : (
<div className="logs-table">
<div className={`logs-table-head logs-cols-${columns.length}`}>
{columns.map((column) => <span key={column}>{column}</span>)}
</div>
{rows.map((row) => (
<div className={`logs-table-row logs-cols-${columns.length}`} key={row.id}>
{logType === 'sms' && (
<>
<strong>{row.from}</strong>
<span>{formatPhone(row.to ?? '')}</span>
<span>{row.template}</span>
<span className={`logs-status ${statusClass(row.status ?? '')}`}>{row.status}</span>
<span>{formatTimestamp(row.time)}</span>
</>
)}
{logType === 'otp' && (
<>
<span>{formatPhone(row.to ?? '')}</span>
<span>{row.otp_type?.replace(/_/g, ' ')}</span>
<span>{row.project}</span>
<span className={`logs-status ${statusClass(row.status ?? '')}`}>{row.status}</span>
<span>{formatTimestamp(row.time)}</span>
</>
)}
{logType === 'inbound' && (
<>
<span>{formatPhone(row.from ?? '')}</span>
<span>{formatPhone(row.to ?? '')}</span>
<span className={`logs-status ${statusClass(row.status ?? '')}`}>{row.status}</span>
<span>{formatTimestamp(row.time)}</span>
</>
)}
{logType === 'outbound' && (
<>
<span>{formatPhone(row.from ?? '')}</span>
<span>{formatPhone(row.to ?? '')}</span>
<span>{row.socket_name || '—'}</span>
<span className={`logs-status ${statusClass(row.status ?? '')}`}>{row.status}</span>
<span>{formatTimestamp(row.time)}</span>
</>
)}
</div>
))}
</div>
)}
{!loading && totalPages > 1 && (
<div className="logs-pagination">
<button
className="secondary-button"
disabled={page <= 1}
onClick={() => setPage((current) => Math.max(1, current - 1))}
>
<ChevronLeft size={15} /> Previous
</button>
<span>Page {page} of {totalPages}</span>
<button
className="secondary-button"
disabled={page >= totalPages}
onClick={() => setPage((current) => current + 1)}
>
Next <ChevronRight size={15} />
</button>
</div>
)}
</section>
</div>
)
}