/** * Contenido del detalle de una incidencia. Se reutiliza en móvil (pantalla * navegada) y en tablet (panel derecho del maestro-detalle). * * Todas las acciones pasan por src/sync/mutations.ts: escriben en local y * encolan la operación. Tras cada cambio refrescamos desde la BD. */ import React, { useCallback, useEffect, useState } from 'react'; import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { Issue, IssueComment, IssuePriority, IssueStatus, IssueTask, } from '../../api/types'; import { hasPermission, useSession } from '../../auth/session'; import { getIssue, getIssueComments, getIssueTasks, } from '../../db/repositories'; import { createIssueComment, createIssueTask, updateIssue, updateIssueTask, } from '../../sync/mutations'; import { Badge, Card, ChipSelect, EmptyState, Field, ISSUE_PRIORITY_COLOR, ISSUE_STATUS_COLOR, PrimaryButton, SectionTitle, } from '../../ui/components'; import { MediaStrip } from '../../ui/MediaStrip'; const STATUSES: readonly IssueStatus[] = ['open', 'in_review', 'resolved', 'closed']; const PRIORITIES: readonly IssuePriority[] = ['low', 'medium', 'high', 'critical']; export function IssueDetailContent({ issueId }: { issueId: number }) { const { user } = useSession(); const canEdit = hasPermission(user, 'edit issues'); const [issue, setIssue] = useState(null); const [tasks, setTasks] = useState([]); const [comments, setComments] = useState([]); const [newTask, setNewTask] = useState(''); const [newComment, setNewComment] = useState(''); const refresh = useCallback(async () => { const [i, t, c] = await Promise.all([ getIssue(issueId), getIssueTasks(issueId), getIssueComments(issueId), ]); setIssue(i); setTasks(t); setComments(c); }, [issueId]); useEffect(() => { void refresh(); }, [refresh]); const onStatus = useCallback( async (status: IssueStatus) => { await updateIssue({ id: issueId, status }); await refresh(); }, [issueId, refresh], ); const onPriority = useCallback( async (priority: IssuePriority) => { await updateIssue({ id: issueId, priority }); await refresh(); }, [issueId, refresh], ); const onToggleTask = useCallback( async (task: IssueTask) => { await updateIssueTask({ id: task.id, is_done: !task.is_done }); await refresh(); }, [refresh], ); const onAddTask = useCallback(async () => { const title = newTask.trim(); if (!title) return; setNewTask(''); await createIssueTask({ issue_id: issueId, title }); await refresh(); }, [newTask, issueId, refresh]); const onAddComment = useCallback(async () => { const body = newComment.trim(); if (!body) return; setNewComment(''); await createIssueComment({ issue_id: issueId, body }); await refresh(); }, [newComment, issueId, refresh]); if (!issue) { return ( Cargando… ); } const done = tasks.filter((t) => t.is_done).length; const progress = tasks.length ? Math.round((done / tasks.length) * 100) : 0; return ( {issue.title} {issue.status && ( )} {issue.priority && ( )} {issue.type && } {issue.description ? {issue.description} : null} {canEdit && ( void onStatus(v)} /> void onPriority(v)} /> )} Tareas · {done}/{tasks.length} ({progress}%) {tasks.length === 0 && } {tasks.map((t) => ( void onToggleTask(t)} > {t.is_done ? '☑' : '☐'} {t.title} ))} {canEdit && ( void onAddTask()} /> void onAddTask()} disabled={!newTask.trim()} /> )} Comentarios {comments.length === 0 && } {comments.map((c) => ( {c.body} {c.created_at ? ( {new Date(c.created_at).toLocaleString()} ) : null} ))} void onAddComment()} disabled={!newComment.trim()} /> ); } const styles = StyleSheet.create({ center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, body: { padding: 16, gap: 4 }, title: { fontSize: 20, fontWeight: '700' }, badges: { flexDirection: 'row', gap: 6, marginTop: 6, flexWrap: 'wrap' }, desc: { marginTop: 8, color: '#333', fontSize: 14 }, editCard: { marginTop: 12, gap: 4 }, taskRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8, gap: 10 }, checkbox: { fontSize: 20 }, taskText: { fontSize: 15, flex: 1 }, taskDone: { textDecorationLine: 'line-through', color: '#666' }, // muted addRow: { flexDirection: 'row', alignItems: 'flex-end', gap: 8, marginTop: 4 }, comment: { marginBottom: 6 }, commentBody: { fontSize: 14 }, commentMeta: { fontSize: 11, color: '#666', marginTop: 4 }, // muted });