117 lines
5.1 KiB
TypeScript
117 lines
5.1 KiB
TypeScript
import React, { useState, useCallback } from 'react'
|
|
import {
|
|
View, Text, FlatList, Modal, TextInput, TouchableOpacity,
|
|
StyleSheet, Alert, RefreshControl, ActivityIndicator,
|
|
} from 'react-native'
|
|
import { useFocusEffect } from 'expo-router'
|
|
import { COLORS } from '../../constants/Config'
|
|
import { getSLAExceptionPending, requestSLAException } from '../../services/api'
|
|
|
|
export default function SLAExceptionScreen() {
|
|
const [items, setItems] = useState<any[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [modal, setModal] = useState<any>(null)
|
|
const [reason, setReason] = useState('')
|
|
const [deadline, setDeadline] = useState('')
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
try {
|
|
const r = await getSLAExceptionPending()
|
|
setItems(r.data?.items ?? r.data ?? [])
|
|
} catch { setItems([]) }
|
|
finally { setLoading(false) }
|
|
}, [])
|
|
|
|
useFocusEffect(useCallback(() => { load() }, [load]))
|
|
|
|
const open = (item: any) => { setModal(item); setReason(''); setDeadline('') }
|
|
|
|
const submit = async () => {
|
|
if (!reason.trim() || !deadline.trim()) { Alert.alert('오류', '사유와 새 기한을 입력해주세요.'); return }
|
|
setSaving(true)
|
|
try {
|
|
await requestSLAException(modal.sr_id, { reason, new_deadline: deadline })
|
|
setModal(null); load()
|
|
} catch { Alert.alert('오류', '제출 중 오류가 발생했습니다.') }
|
|
finally { setSaving(false) }
|
|
}
|
|
|
|
const renderItem = ({ item }: { item: any }) => (
|
|
<TouchableOpacity style={s.card} onPress={() => open(item)}>
|
|
<Text style={s.title} numberOfLines={2}>{item.title}</Text>
|
|
<View style={s.row}>
|
|
<View style={[s.badge, { backgroundColor: item.sla_breached ? COLORS.danger : COLORS.warning }]}>
|
|
<Text style={s.badgeText}>{item.sla_breached ? 'SLA 위반' : 'SLA 임박'}</Text>
|
|
</View>
|
|
<Text style={s.meta}>기한: {item.sla_deadline?.slice(0, 10) ?? '-'}</Text>
|
|
</View>
|
|
<Text style={s.hint}>탭하여 예외 승인 요청</Text>
|
|
</TouchableOpacity>
|
|
)
|
|
|
|
return (
|
|
<View style={s.container}>
|
|
<FlatList
|
|
data={items}
|
|
keyExtractor={i => String(i.sr_id)}
|
|
renderItem={renderItem}
|
|
refreshControl={<RefreshControl refreshing={loading} onRefresh={load} />}
|
|
ListEmptyComponent={<Text style={s.empty}>SLA 예외 대기 항목이 없습니다.</Text>}
|
|
contentContainerStyle={{ padding: 12 }}
|
|
/>
|
|
|
|
<Modal visible={!!modal} transparent animationType="slide">
|
|
<View style={s.overlay}>
|
|
<View style={s.modalBox}>
|
|
<Text style={s.modalTitle}>SLA 예외 승인 요청</Text>
|
|
<Text style={s.modalSR}>{modal?.title}</Text>
|
|
<TextInput
|
|
style={s.input}
|
|
value={reason}
|
|
onChangeText={setReason}
|
|
placeholder="예외 사유를 입력하세요"
|
|
multiline
|
|
/>
|
|
<TextInput
|
|
style={s.input}
|
|
value={deadline}
|
|
onChangeText={setDeadline}
|
|
placeholder="새 기한 (YYYY-MM-DD HH:MM)"
|
|
/>
|
|
<View style={s.modalBtns}>
|
|
<TouchableOpacity style={[s.btn, { backgroundColor: COLORS.border }]} onPress={() => setModal(null)}>
|
|
<Text style={s.btnText}>취소</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity style={[s.btn, { backgroundColor: COLORS.accent }]} onPress={submit} disabled={saving}>
|
|
<Text style={[s.btnText, { color: '#fff' }]}>{saving ? '제출 중...' : '제출'}</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: COLORS.bg },
|
|
card: { backgroundColor: '#fff', borderRadius: 10, padding: 14, marginBottom: 8, elevation: 1 },
|
|
title: { fontSize: 14, fontWeight: '600', color: COLORS.text, marginBottom: 6 },
|
|
row: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 4 },
|
|
badge: { borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2 },
|
|
badgeText: { fontSize: 11, color: '#fff', fontWeight: '700' },
|
|
meta: { fontSize: 12, color: COLORS.muted },
|
|
hint: { fontSize: 11, color: COLORS.accent, marginTop: 4 },
|
|
empty: { textAlign: 'center', color: COLORS.muted, marginTop: 40 },
|
|
overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
|
|
modalBox: { backgroundColor: '#fff', borderTopLeftRadius: 16, borderTopRightRadius: 16, padding: 20 },
|
|
modalTitle: { fontSize: 17, fontWeight: '800', color: COLORS.text, marginBottom: 8 },
|
|
modalSR: { fontSize: 13, color: COLORS.muted, marginBottom: 12 },
|
|
input: { borderWidth: 1, borderColor: COLORS.border, borderRadius: 8, padding: 10, marginBottom: 10, fontSize: 14, color: COLORS.text },
|
|
modalBtns: { flexDirection: 'row', gap: 10, marginTop: 4 },
|
|
btn: { flex: 1, borderRadius: 8, padding: 12, alignItems: 'center' },
|
|
btnText: { fontWeight: '700', fontSize: 14 },
|
|
})
|