129 lines
5.4 KiB
TypeScript
129 lines
5.4 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import {
|
|
View, Text, ScrollView, StyleSheet, TouchableOpacity,
|
|
ActivityIndicator, RefreshControl, Alert,
|
|
} from 'react-native'
|
|
import { router } from 'expo-router'
|
|
import { COLORS, PRIORITY_COLOR } from '../../constants/Config'
|
|
import { getSRList, patchSR } from '../../services/api'
|
|
|
|
interface SR {
|
|
id: number
|
|
sr_id?: string
|
|
title: string
|
|
status?: string
|
|
priority?: string
|
|
}
|
|
|
|
const COLUMNS: { key: string; label: string; color: string }[] = [
|
|
{ key: 'RECEIVED', label: '접수', color: '#94a3b8' },
|
|
{ key: 'IN_PROGRESS', label: '진행중', color: '#4f6ef7' },
|
|
{ key: 'PENDING_APPROVAL', label: '승인대기', color: '#f59e0b' },
|
|
{ key: 'COMPLETED', label: '완료', color: '#22c55e' },
|
|
]
|
|
|
|
/**
|
|
* 기능 #5 — Kanban SR 보드
|
|
* 상태별 컬럼. 카드의 ◀ ▶ 버튼으로 이전/다음 상태 이동 (PATCH /api/tasks/{id}).
|
|
*/
|
|
export default function KanbanScreen() {
|
|
const [items, setItems] = useState<SR[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [refresh, setRefresh] = useState(false)
|
|
|
|
const load = async (r = false) => {
|
|
r ? setRefresh(true) : setLoading(true)
|
|
try {
|
|
const res = await getSRList(0, 100)
|
|
setItems(res.data?.content ?? res.data?.items ?? res.data ?? [])
|
|
} catch { setItems([]) }
|
|
finally { setLoading(false); setRefresh(false) }
|
|
}
|
|
|
|
useEffect(() => { load() }, [])
|
|
|
|
const move = async (sr: SR, dir: -1 | 1) => {
|
|
const idx = COLUMNS.findIndex(c => c.key === sr.status)
|
|
const nextIdx = idx + dir
|
|
if (idx < 0 || nextIdx < 0 || nextIdx >= COLUMNS.length) return
|
|
const nextStatus = COLUMNS[nextIdx].key
|
|
setItems(prev => prev.map(i => (i.id === sr.id ? { ...i, status: nextStatus } : i)))
|
|
try {
|
|
await patchSR(sr.id, { status: nextStatus })
|
|
} catch (e: any) {
|
|
setItems(prev => prev.map(i => (i.id === sr.id ? { ...i, status: sr.status } : i)))
|
|
Alert.alert('오류', e.response?.data?.detail ?? '상태 변경 실패')
|
|
}
|
|
}
|
|
|
|
if (loading) return <ActivityIndicator style={{ marginTop: 60 }} color={COLORS.accent} />
|
|
|
|
return (
|
|
<ScrollView
|
|
horizontal
|
|
style={{ flex: 1, backgroundColor: COLORS.bg }}
|
|
contentContainerStyle={{ padding: 12 }}
|
|
refreshControl={<RefreshControl refreshing={refresh} onRefresh={() => load(true)} />}
|
|
>
|
|
{COLUMNS.map((col, ci) => {
|
|
const cards = items.filter(i => i.status === col.key)
|
|
return (
|
|
<View key={col.key} style={s.column}>
|
|
<View style={[s.colHead, { borderTopColor: col.color }]}>
|
|
<Text style={s.colTitle}>{col.label}</Text>
|
|
<Text style={[s.colCount, { color: col.color }]}>{cards.length}</Text>
|
|
</View>
|
|
<ScrollView style={{ flex: 1 }}>
|
|
{cards.length === 0 && <Text style={s.emptyCol}>비어 있음</Text>}
|
|
{cards.map(sr => (
|
|
<View key={sr.id} style={s.card}>
|
|
<TouchableOpacity onPress={() => router.push({ pathname: '/(tabs)/sr_detail', params: { id: String(sr.id) } })}>
|
|
<Text style={s.cardId}>{sr.sr_id ?? `#${sr.id}`}</Text>
|
|
<Text style={s.cardTitle} numberOfLines={3}>{sr.title}</Text>
|
|
{!!sr.priority && (
|
|
<Text style={[s.cardPri, { color: PRIORITY_COLOR[sr.priority] ?? COLORS.muted }]}>● {sr.priority}</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
<View style={s.moveRow}>
|
|
<TouchableOpacity
|
|
style={[s.moveBtn, ci === 0 && s.moveBtnOff]}
|
|
onPress={() => move(sr, -1)}
|
|
disabled={ci === 0}
|
|
>
|
|
<Text style={s.moveText}>◀</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[s.moveBtn, ci === COLUMNS.length - 1 && s.moveBtnOff]}
|
|
onPress={() => move(sr, 1)}
|
|
disabled={ci === COLUMNS.length - 1}
|
|
>
|
|
<Text style={s.moveText}>▶</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
))}
|
|
<View style={{ height: 20 }} />
|
|
</ScrollView>
|
|
</View>
|
|
)
|
|
})}
|
|
</ScrollView>
|
|
)
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
column: { width: 240, marginRight: 12, backgroundColor: '#fff', borderRadius: 12, padding: 8, maxHeight: '100%' },
|
|
colHead: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderTopWidth: 3, paddingTop: 8, paddingHorizontal: 6, paddingBottom: 8, marginBottom: 6 },
|
|
colTitle: { fontSize: 14, fontWeight: '800', color: COLORS.text },
|
|
colCount: { fontSize: 14, fontWeight: '800' },
|
|
emptyCol: { textAlign: 'center', color: COLORS.muted, fontSize: 12, paddingVertical: 16 },
|
|
card: { backgroundColor: COLORS.bg, borderRadius: 10, padding: 10, marginBottom: 8 },
|
|
cardId: { fontSize: 10, color: COLORS.accent, fontWeight: '700' },
|
|
cardTitle: { fontSize: 13, color: COLORS.text, marginTop: 4, lineHeight: 18 },
|
|
cardPri: { fontSize: 10, fontWeight: '700', marginTop: 6 },
|
|
moveRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 },
|
|
moveBtn: { backgroundColor: COLORS.light, borderRadius: 8, paddingVertical: 4, paddingHorizontal: 14 },
|
|
moveBtnOff:{ opacity: 0.3 },
|
|
moveText: { color: COLORS.accent, fontWeight: '700', fontSize: 13 },
|
|
})
|