Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 134 additions & 28 deletions frontend/src/app/(client)/(tabs)/tickets.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useCallback } from 'react';
import { View, Text, ScrollView, TouchableOpacity, ActivityIndicator, Alert, RefreshControl } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { View, Text, ScrollView, TouchableOpacity, ActivityIndicator, Alert, RefreshControl, Modal, TextInput } from 'react-native';
import { Ionicons, Feather } from '@expo/vector-icons';
import { useRouter, useFocusEffect } from 'expo-router';
import { api, useAuth } from '@/contexts/AuthContext';
import {
Expand All @@ -10,7 +10,6 @@ import {
TicketStatus,
} from '@/components/tickets/TicketCard';


export default function Tickets() {
const router = useRouter();
const { user } = useAuth();
Expand All @@ -19,6 +18,9 @@ export default function Tickets() {
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [activeFilter, setActiveFilter] = useState('Todos');
const [showCloseModal, setShowCloseModal] = useState(false);
const [ticketToClose, setTicketToClose] = useState<string | null>(null);
const [closeReason, setCloseReason] = useState('');

const fetchTickets = async () => {
try {
Expand Down Expand Up @@ -62,6 +64,35 @@ export default function Tickets() {
fetchTickets();
};

const confirmClosing = async () => {
if (!closeReason.trim()) {
Alert.alert('Aviso', 'Por favor, descreva o motivo para encerrar o chamado.');
return;
}

try {
await api.put(`/tickets/${ticketToClose}/status`, {
status: 'CLOSED',
solution: closeReason,
});

setShowCloseModal(false);
setCloseReason('');
setTicketToClose(null);
Alert.alert('Sucesso', 'Chamado encerrado com sucesso!');

fetchTickets();

} catch (error: any) {
const apiMessage = error?.response?.data?.message;
const errorMessage = Array.isArray(apiMessage)
? apiMessage.join('\n')
: (apiMessage || 'Falha ao encerrar chamado.');

Alert.alert('Erro', errorMessage);
}
};

const filteredTickets = tickets.filter(ticket => {
if (activeFilter === 'Todos') return true;
if (activeFilter === 'Abertos') return ticket.status === 'OPEN';
Expand Down Expand Up @@ -118,32 +149,46 @@ export default function Tickets() {
</View>
) : (
filteredTickets.map(ticket => (
<TicketCard
key={ticket._id}
ticket={ticket}
onPress={async () => {
try {
const res = await api.get(`/chat/ticket/${ticket._id}`);
const chatData = res.data;
const chatId = chatData?.id || chatData?._id;

if (chatId) {
router.push({
pathname: '/(client)/ticket/[id]',
params: {
id: chatId,
ticketId: ticket._id
}
});
} else {
Alert.alert('Aviso', 'O chat deste chamado ainda não foi criado.');
<View key={ticket._id} className="relative">
<TicketCard
ticket={ticket}
onPress={async () => {
try {
const res = await api.get(`/chat/ticket/${ticket._id}`);
const chatData = res.data;
const chatId = chatData?.id || chatData?._id;

if (chatId) {
router.push({
pathname: '/(client)/ticket/[id]',
params: {
id: chatId,
ticketId: ticket._id
}
});
} else {
Alert.alert('Aviso', 'O chat deste chamado ainda não foi criado.');
}
} catch (e: any) {
console.log("ERRO API CHAT CLIENTE:", e?.response?.data || e.message);
Alert.alert('Erro', 'Falha ao conectar na sala do chamado.');
}
} catch (e: any) {
console.log("ERRO API CHAT CLIENTE:", e?.response?.data || e.message);
Alert.alert('Erro', 'Falha ao conectar na sala do chamado.');
}
}}
/>
}}
/>

{/* BOTÃO DE 3 PONTINHOS SOBREPOSTO AO CARD */}
{ticket.status !== 'CLOSED' && (
<TouchableOpacity
onPress={() => {
setTicketToClose(ticket._id);
setShowCloseModal(true);
}}
className="absolute top-4 right-4 w-8 h-8 bg-red-50 rounded-full items-center justify-center z-10 border border-red-100 shadow-sm"
>
<Feather color="#ef4444" name="x" size={16}/>
</TouchableOpacity>
)}
</View>
))
)}

Expand All @@ -155,6 +200,67 @@ export default function Tickets() {

<View className="h-10" />
</ScrollView>

{/* MODAL DE ENCERRAR CHAMADO (CLIENTE) */}
<Modal
visible={showCloseModal}
transparent={true}
animationType="slide"
onRequestClose={() => setShowCloseModal(false)}
>
<View className="flex-1 justify-end bg-black/50">
<View className="bg-white pt-4 pb-8 px-6 rounded-t-3xl shadow-2xl">

{/* Tracinho de arrastar */}
<View className="items-center mb-6">
<View className="w-12 h-1.5 bg-gray-200 rounded-full" />
</View>

<View className="flex-row items-center mb-4">
<Feather name="check-circle" size={24} color="#ef4444" className="mr-2" />
<Text className="text-xl font-bold text-slate-800 ml-2">
Encerrar Chamado
</Text>
</View>

<Text className="text-slate-500 font-medium mb-2 text-sm">
Por que você está encerrando este chamado?
</Text>
<TextInput
className="bg-gray-50 border border-gray-200 rounded-2xl p-4 text-slate-700 mb-6 h-32"
placeholder="Ex: O problema já foi resolvido, não preciso mais de ajuda..."
placeholderTextColor="#94a3b8"
multiline
textAlignVertical="top"
value={closeReason}
onChangeText={setCloseReason}
/>

{/* BOTÕES DE AÇÃO */}
<View className="flex-row justify-between">
<TouchableOpacity
onPress={() => {
setShowCloseModal(false);
setCloseReason('');
setTicketToClose(null);
}}
className="flex-1 bg-gray-100 py-4 rounded-xl items-center mr-2"
>
<Text className="text-slate-500 font-bold text-base">Cancelar</Text>
</TouchableOpacity>

<TouchableOpacity
onPress={confirmClosing}
className="flex-1 bg-red-500 py-4 rounded-xl items-center ml-2 flex-row justify-center"
>
<Text className="text-white font-bold text-base ml-1">Confirmar Encerramento</Text>
</TouchableOpacity>
</View>

</View>
</View>
</Modal>

</View>
);
}
6 changes: 3 additions & 3 deletions frontend/src/components/tickets/TicketCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,19 @@ const STATUS_MAP = {
[TicketStatus.OPEN]: {
label: 'ABERTO',
color: 'text-amber-600',
bg: 'bg-amber-50',
bg: 'bg-amber-50 mr-8',
icon: 'clock-outline',
},
[TicketStatus.IN_PROGRESS]: {
label: 'EM ATENDIMENTO',
color: 'text-blue-600',
bg: 'bg-blue-50',
bg: 'bg-blue-50 mr-8',
icon: 'message-text-outline',
},
[TicketStatus.ESCALATED]: {
label: 'ESCALADO',
color: 'text-orange-600',
bg: 'bg-orange-50',
bg: 'bg-orange-50 mr-8',
icon: 'alert-circle-outline',
},
[TicketStatus.CLOSED]: {
Expand Down