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

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 },
})