guardia-messenger/app/(tabs)/sr_batch.tsx

162 lines
6.7 KiB
TypeScript

import { useEffect, useState } from 'react'
import {
View, Text, ScrollView, StyleSheet, TouchableOpacity,
ActivityIndicator, Alert, RefreshControl,
} from 'react-native'
import { COLORS, PRIORITY_COLOR, STATUS_COLOR } from '../../constants/Config'
import { getSRList, batchUpdateSR } from '../../services/api'
const STATUS_OPTIONS = ['IN_PROGRESS', 'PENDING_APPROVAL', 'COMPLETED', 'REJECTED']
interface SR {
id: number
sr_id?: string
title: string
status?: string
priority?: string
}
/**
* 기능 #12 — 일괄 SR 상태 변경
* 체크박스 다중 선택 → PATCH /api/tasks/batch { ids, status }
*/
export default function SRBatchScreen() {
const [items, setItems] = useState<SR[]>([])
const [selected, setSelected] = useState<Set<number>>(new Set())
const [loading, setLoading] = useState(true)
const [refresh, setRefresh] = useState(false)
const [applying, setApplying] = useState(false)
const [target, setTarget] = useState<string>('IN_PROGRESS')
const load = async (r = false) => {
r ? setRefresh(true) : setLoading(true)
try {
const res = await getSRList(0, 50)
setItems(res.data?.content ?? res.data?.items ?? res.data ?? [])
} catch { setItems([]) }
finally { setLoading(false); setRefresh(false) }
}
useEffect(() => { load() }, [])
const toggle = (id: number) => {
setSelected(prev => {
const next = new Set(prev)
next.has(id) ? next.delete(id) : next.add(id)
return next
})
}
const toggleAll = () => {
setSelected(prev =>
prev.size === items.length ? new Set() : new Set(items.map(i => i.id))
)
}
const apply = async () => {
if (selected.size === 0) { Alert.alert('SR을 선택하세요.'); return }
Alert.alert(
'일괄 변경',
`${selected.size}건을 '${target}' 상태로 변경할까요?`,
[
{ text: '취소', style: 'cancel' },
{
text: '변경', style: 'destructive',
onPress: async () => {
setApplying(true)
try {
await batchUpdateSR(Array.from(selected), target)
setSelected(new Set())
await load()
Alert.alert('완료', '일괄 상태 변경이 적용되었습니다.')
} catch (e: any) {
Alert.alert('오류', e.response?.data?.detail ?? '일괄 변경 실패')
} finally { setApplying(false) }
},
},
]
)
}
return (
<View style={{ flex: 1, backgroundColor: COLORS.bg }}>
<View style={s.toolbar}>
<TouchableOpacity onPress={toggleAll}>
<Text style={s.selAll}>
{selected.size === items.length && items.length > 0 ? '☑ 전체 해제' : '☐ 전체 선택'}
</Text>
</TouchableOpacity>
<Text style={s.count}>{selected.size} </Text>
</View>
{/* 대상 상태 선택 */}
<View style={s.statusRow}>
{STATUS_OPTIONS.map(st => (
<TouchableOpacity
key={st}
style={[s.statusChip, target === st && { backgroundColor: STATUS_COLOR[st], borderColor: STATUS_COLOR[st] }]}
onPress={() => setTarget(st)}
>
<Text style={[s.statusChipText, target === st && { color: '#fff', fontWeight: '700' }]}>{st}</Text>
</TouchableOpacity>
))}
</View>
{loading ? (
<ActivityIndicator style={{ marginTop: 50 }} color={COLORS.accent} />
) : (
<ScrollView refreshControl={<RefreshControl refreshing={refresh} onRefresh={() => load(true)} />}>
{items.map(sr => {
const on = selected.has(sr.id)
return (
<TouchableOpacity key={sr.id} style={[s.card, on && s.cardOn]} onPress={() => toggle(sr.id)}>
<Text style={[s.check, on && s.checkOn]}>{on ? '☑' : '☐'}</Text>
<View style={{ flex: 1 }}>
<View style={s.cardHead}>
<Text style={s.srId}>{sr.sr_id ?? `#${sr.id}`}</Text>
{!!sr.status && (
<Text style={[s.status, { color: STATUS_COLOR[sr.status] ?? COLORS.muted }]}>{sr.status}</Text>
)}
</View>
<Text style={s.title} numberOfLines={1}>{sr.title}</Text>
{!!sr.priority && (
<Text style={[s.pri, { color: PRIORITY_COLOR[sr.priority] ?? COLORS.muted }]}> {sr.priority}</Text>
)}
</View>
</TouchableOpacity>
)
})}
<View style={{ height: 90 }} />
</ScrollView>
)}
<View style={s.footer}>
<TouchableOpacity style={[s.applyBtn, (selected.size === 0 || applying) && { opacity: 0.5 }]} onPress={apply} disabled={selected.size === 0 || applying}>
{applying ? <ActivityIndicator color="#fff" /> : <Text style={s.applyText}> {selected.size} {target}</Text>}
</TouchableOpacity>
</View>
</View>
)
}
const s = StyleSheet.create({
toolbar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#fff', paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: COLORS.border },
selAll: { fontSize: 14, fontWeight: '700', color: COLORS.accent },
count: { fontSize: 13, color: COLORS.muted },
statusRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, padding: 12, backgroundColor: '#fff', borderBottomWidth: 1, borderBottomColor: COLORS.border },
statusChip: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 16, borderWidth: 1, borderColor: COLORS.border },
statusChipText: { fontSize: 11, color: COLORS.text },
card: { flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: '#fff', marginHorizontal: 16, marginTop: 10, borderRadius: 10, padding: 14 },
cardOn: { borderWidth: 1.5, borderColor: COLORS.accent, backgroundColor: COLORS.light },
check: { fontSize: 22, color: COLORS.muted },
checkOn: { color: COLORS.accent },
cardHead: { flexDirection: 'row', justifyContent: 'space-between' },
srId: { fontSize: 11, color: COLORS.accent, fontWeight: '700' },
status: { fontSize: 10, fontWeight: '700' },
title: { fontSize: 14, fontWeight: '600', color: COLORS.text, marginTop: 3 },
pri: { fontSize: 10, fontWeight: '700', marginTop: 4 },
footer: { position: 'absolute', bottom: 0, left: 0, right: 0, padding: 14, backgroundColor: '#fff', borderTopWidth: 1, borderTopColor: COLORS.border },
applyBtn: { backgroundColor: COLORS.primary, borderRadius: 12, padding: 15, alignItems: 'center' },
applyText: { color: '#fff', fontSize: 15, fontWeight: '800' },
})