162 lines
6.7 KiB
TypeScript
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' },
|
|
})
|