83 lines
3.8 KiB
TypeScript
83 lines
3.8 KiB
TypeScript
import { useCallback, useState } from 'react'
|
|
import { View, Text, FlatList, TouchableOpacity, StyleSheet, TextInput, RefreshControl } from 'react-native'
|
|
import { useFocusEffect, router } from 'expo-router'
|
|
import { Feather } from '@expo/vector-icons'
|
|
import { useTheme } from '../../constants/ThemeContext'
|
|
import { type ThemeTokens } from '../../constants/ItmsTheme'
|
|
import { StatusPill, EmptyState } from '../../components/ui'
|
|
import { rowTitle, rowStatus, rowDate, rowAuthor, incidentSeq } from '../../constants/rows'
|
|
import { itmsSearchIncidents, listRows, itmsErrorMessage, type RowMap } from '../itmsApi'
|
|
|
|
/*
|
|
* 서비스데스크 인시던트 목록(SvcDeskController /srm/searchIncident.do).
|
|
* 담당자/관리자 관점 처리현황 조회. 검색 + 새로고침. 서버 미기동 시 빈 상태.
|
|
*/
|
|
export default function Incidents() {
|
|
const { t } = useTheme()
|
|
const s = makeStyles(t)
|
|
const [rows, setRows] = useState<RowMap[]>([])
|
|
const [keyword, setKeyword] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [err, setErr] = useState('')
|
|
|
|
const load = useCallback(async (kw?: string) => {
|
|
setLoading(true); setErr('')
|
|
try {
|
|
const filter: RowMap = {}
|
|
const k = (kw ?? keyword).trim()
|
|
if (k) { filter.searchKeyword = k; filter.searchWrd = k }
|
|
setRows(listRows(await itmsSearchIncidents(filter)))
|
|
} catch (e) { setErr(itmsErrorMessage(e)); setRows([]) }
|
|
finally { setLoading(false) }
|
|
}, [keyword])
|
|
|
|
useFocusEffect(useCallback(() => { load() }, [load]))
|
|
|
|
return (
|
|
<View style={s.wrap}>
|
|
<View style={s.head}>
|
|
<Text style={s.title}>인시던트</Text>
|
|
<View style={s.searchBar}>
|
|
<Feather name="search" size={16} color={t.muted} />
|
|
<TextInput
|
|
style={s.searchInput} value={keyword} onChangeText={setKeyword}
|
|
placeholder="제목 검색" placeholderTextColor={t.muted}
|
|
returnKeyType="search" onSubmitEditing={() => load()}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
<FlatList
|
|
data={rows}
|
|
keyExtractor={(_, i) => String(i)}
|
|
contentContainerStyle={rows.length === 0 ? { flexGrow: 1 } : { padding: 16 }}
|
|
refreshControl={<RefreshControl refreshing={loading} onRefresh={() => load()} tintColor={t.accent} />}
|
|
ListEmptyComponent={loading ? null : <EmptyState icon="alert-circle" text={err || '표시할 인시던트가 없습니다.'} />}
|
|
renderItem={({ item }) => (
|
|
<TouchableOpacity
|
|
style={s.card}
|
|
onPress={() => router.push({ pathname: '/incident-detail', params: { seq: incidentSeq(item), raw: JSON.stringify(item) } })}
|
|
>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={s.cardTitle} numberOfLines={2}>{rowTitle(item)}</Text>
|
|
<Text style={s.cardSub}>{[rowAuthor(item), rowDate(item)].filter(Boolean).join(' · ')}</Text>
|
|
</View>
|
|
<StatusPill label={rowStatus(item)} />
|
|
</TouchableOpacity>
|
|
)}
|
|
/>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const makeStyles = (t: ThemeTokens) => StyleSheet.create({
|
|
wrap: { flex: 1, backgroundColor: t.bg },
|
|
head: { paddingHorizontal: 16, paddingTop: 16, paddingBottom: 8, gap: 12 },
|
|
title: { color: t.text, fontSize: 22, fontWeight: '900' },
|
|
searchBar: { flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: t.card, borderRadius: 12, borderWidth: 1, borderColor: t.border, paddingHorizontal: 12 },
|
|
searchInput: { flex: 1, color: t.text, fontSize: 14, paddingVertical: 10 },
|
|
card: { flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: t.card, borderRadius: 12, borderWidth: 1, borderColor: t.border, padding: 14, marginBottom: 10 },
|
|
cardTitle: { color: t.text, fontSize: 15, fontWeight: '700' },
|
|
cardSub: { color: t.muted, fontSize: 12, marginTop: 4 },
|
|
})
|