Yaltopia-Tickets-App/app/help.tsx
2026-05-14 22:29:28 +03:00

590 lines
20 KiB
TypeScript

import React, { useEffect, useState } from "react";
import {
View,
ScrollView,
ActivityIndicator,
Pressable,
Modal,
TextInput,
KeyboardAvoidingView,
Platform,
} from "react-native";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { StandardHeader } from "@/components/StandardHeader";
import { Text } from "@/components/ui/text";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { supportApi } from "@/lib/api";
import { useAuthStore } from "@/lib/auth-store";
import { toast } from "@/lib/toast-store";
import {
Plus,
MessageSquare,
AlertCircle,
Clock,
CheckCircle2,
ChevronRight,
X,
Calendar,
User as UserIcon,
Tag,
} from "@/lib/icons";
import { useColorScheme } from "nativewind";
import { cn } from "@/lib/utils";
interface SupportTicket {
id: string;
ticketNumber: string;
subject: string;
message: string;
status: "OPEN" | "PENDING" | "IN_PROGRESS" | "RESOLVED" | "CLOSED";
priority: "URGENT" | "HIGH" | "MEDIUM" | "LOW";
createdAt: string;
updatedAt: string;
resolution?: string;
requesterName?: string;
requesterEmail: string;
}
const PRIORITIES = [
{
label: "Urgent",
value: "URGENT",
color: "text-red-500",
bg: "bg-red-500/10",
dot: "bg-red-500",
},
{
label: "High",
value: "HIGH",
color: "text-orange-500",
bg: "bg-orange-500/10",
dot: "bg-orange-500",
},
{
label: "Medium",
value: "MEDIUM",
color: "text-blue-500",
bg: "bg-blue-500/10",
dot: "bg-blue-500",
},
{
label: "Low",
value: "LOW",
color: "text-green-500",
bg: "bg-green-500/10",
dot: "bg-green-500",
},
] as const;
export default function HelpScreen() {
const [tickets, setTickets] = useState<SupportTicket[]>([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedTicket, setSelectedTicket] = useState<SupportTicket | null>(
null,
);
const [isSubmitting, setIsSubmitting] = useState(false);
// Form State
const [subject, setSubject] = useState("");
const [message, setMessage] = useState("");
const [priority, setPriority] = useState<
"URGENT" | "HIGH" | "MEDIUM" | "LOW"
>("MEDIUM");
const { user } = useAuthStore();
const { colorScheme } = useColorScheme();
const isDark = colorScheme === "dark";
const iconColor = isDark ? "#f1f5f9" : "#0f172a";
const fetchTickets = async () => {
try {
const response = await supportApi.getAll();
const ticketData = (response as any)?.data || response;
setTickets(Array.isArray(ticketData) ? ticketData : []);
} catch (error) {
console.error("[Support] Fetch failed:", error);
setTickets([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTickets();
}, []);
const handleSubmit = async () => {
const cleanSubject = subject.trim();
const cleanMessage = message.trim();
console.log(
"[Support] handleSubmit - subject length:",
cleanSubject.length,
);
console.log(
"[Support] handleSubmit - message length:",
cleanMessage.length,
);
if (!cleanSubject || !cleanMessage) {
console.log("[Support] Validation failed: Empty fields");
toast.error("Required Fields", "Please enter a subject and message.");
return;
}
if (cleanSubject.length < 5) {
console.log("[Support] Validation failed: Subject too short");
toast.error(
"Subject too short",
"The subject must be at least 5 characters.",
);
return;
}
if (cleanMessage.length < 10) {
console.log("[Support] Validation failed: Message too short");
toast.error(
"Message too short",
"Please describe your issue in more detail (min 10 chars).",
);
return;
}
setIsSubmitting(true);
try {
console.log("[Support] Sending ticket data...");
const payload = {
requesterEmail:
user?.email && user.email.includes("@")
? user.email
: "anonymous@yaltopia.com",
requesterName:
`${user?.firstName || ""} ${user?.lastName || ""}`.trim() ||
"Anonymous",
subject: cleanSubject,
message: cleanMessage,
priority,
};
console.log("[Support] Payload:", JSON.stringify(payload));
await supportApi.create({
body: payload,
});
console.log("[Support] Ticket created successfully");
toast.success("Ticket Created", "We'll get back to you soon.");
setIsModalVisible(false);
setSubject("");
setMessage("");
setPriority("MEDIUM");
fetchTickets();
} catch (error: any) {
console.error("[Support] Create failed:", error);
const errorMsg = Array.isArray(error?.message)
? error.message.join(", ")
: error?.message || "Could not create support ticket.";
toast.error("Submission Failed", errorMsg);
} finally {
setIsSubmitting(false);
}
};
const getStatusInfo = (status: string) => {
switch (status) {
case "OPEN":
case "PENDING":
return {
label: status,
color: "text-blue-500",
bg: "bg-blue-500/10",
icon: <Clock size={14} color="#3b82f6" />,
};
case "IN_PROGRESS":
return {
label: "In Progress",
color: "text-orange-500",
bg: "bg-orange-500/10",
icon: <Clock size={14} color="#f59e0b" />,
};
case "RESOLVED":
case "CLOSED":
return {
label: status,
color: "text-green-500",
bg: "bg-green-500/10",
icon: <CheckCircle2 size={14} color="#10b981" />,
};
default:
return {
label: status,
color: "text-muted-foreground",
bg: "bg-muted/10",
icon: <AlertCircle size={14} color="#6b7280" />,
};
}
};
return (
<ScreenWrapper className="bg-background">
<StandardHeader
title="Help & Support"
showBack
right={
<Pressable
onPress={() => setIsModalVisible(true)}
className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border"
>
<Plus size={20} color={iconColor} />
</Pressable>
}
/>
{loading ? (
<View className="flex-1 items-center justify-center">
<ActivityIndicator color="#ea580c" />
</View>
) : (
<ScrollView
className="flex-1"
contentContainerStyle={{ padding: 16, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
{tickets.length === 0 ? (
<View className="items-center justify-center mt-10 p-10 rounded-3xl bg-card border border-dashed border-border">
<View className="h-20 w-20 rounded-full bg-muted/30 items-center justify-center mb-4">
<MessageSquare size={36} color="#94a3b8" />
</View>
<Text className="text-foreground font-bold text-lg text-center">
All clear!
</Text>
<Text className="text-muted-foreground text-sm text-center mt-2 leading-5 px-4">
You don't have any active support tickets. Need assistance?
Create one now.
</Text>
<Button
onPress={() => setIsModalVisible(true)}
variant="outline"
className="mt-6 border-primary/20 rounded-[6px] px-8"
>
<Text className="text-primary font-semibold">Get Help</Text>
</Button>
</View>
) : (
tickets.map((ticket: any) => {
const pInfo =
PRIORITIES.find((p) => p.value === ticket.priority) ||
PRIORITIES[2];
const sInfo = getStatusInfo(ticket.status);
return (
<Pressable
key={ticket.id}
onPress={() => setSelectedTicket(ticket)}
className="mb-4"
>
<Card className="border border-border/50 bg-card shadow-sm rounded-[6px] overflow-hidden">
<View className="absolute left-0 top-0 bottom-0 w-1 bg-primary/20" />
<CardContent className="p-4">
<View className="flex-row justify-between items-center mb-3">
<View className="flex-row items-center gap-2">
<View
className={cn(
"px-2.5 py-1 rounded-[6px] flex-row items-center gap-1.5",
pInfo.bg,
)}
>
<View
className={cn(
"h-1.5 w-1.5 rounded-full",
pInfo.dot,
)}
/>
<Text
className={cn(
"text-[10px] font-bold uppercase",
pInfo.color,
)}
>
{pInfo.label}
</Text>
</View>
<Text className="text-[10px] font-mono text-muted-foreground/60">
{ticket.ticketNumber}
</Text>
</View>
<View
className={cn(
"px-2.5 py-1 rounded-[6px] flex-row items-center gap-1.5",
sInfo.bg,
)}
>
{sInfo.icon}
<Text
className={cn(
"text-[10px] font-bold uppercase",
sInfo.color,
)}
>
{sInfo.label}
</Text>
</View>
</View>
<Text
className="text-foreground font-bold text-lg mb-1.5"
numberOfLines={1}
>
{ticket.subject}
</Text>
<View className="flex-row justify-between items-center pt-3 border-t border-border/40">
<View className="flex-row items-center gap-1.5">
<Calendar size={12} color="#94a3b8" />
<Text className="text-[10px] text-muted-foreground font-medium">
Created{" "}
{new Date(ticket.createdAt).toLocaleDateString()}
</Text>
</View>
<View className="flex-row items-center gap-1">
<Text className="text-primary text-xs font-bold">
View details
</Text>
<ChevronRight
size={14}
color="#ea580c"
strokeWidth={3}
/>
</View>
</View>
</CardContent>
</Card>
</Pressable>
);
})
)}
</ScrollView>
)}
{/* Ticket Detail Modal */}
<Modal
visible={!!selectedTicket}
animationType="fade"
transparent
onRequestClose={() => setSelectedTicket(null)}
>
<View className="flex-1 bg-black/60 justify-center px-5">
<View className="bg-card rounded-[6px] overflow-hidden shadow-2xl border border-border/50">
<View className="px-5 py-5 border-b border-border/50 flex-row justify-between items-center bg-muted/20">
<View>
<Text className="text-[10px] font-bold text-primary uppercase tracking-widest mb-0.5">
Ticket Details
</Text>
<Text className="text-foreground font-bold text-lg">
{selectedTicket?.ticketNumber}
</Text>
</View>
<Pressable
onPress={() => setSelectedTicket(null)}
className="h-8 w-8 rounded-[10px] bg-card items-center justify-center border border-border"
>
<X size={20} color={iconColor} />
</Pressable>
</View>
<ScrollView className="max-h-[70%] px-5 pt-6 pb-8">
<View className="mb-6">
<Text className="text-foreground font-bold text-xl mb-2">
{selectedTicket?.subject}
</Text>
<View className="flex-row flex-wrap gap-2 mb-4">
<View className="bg-muted/40 px-3 py-1.5 rounded-[6px] flex-row items-center gap-2">
<Tag size={14} color="#94a3b8" />
<Text className="text-xs text-foreground font-medium">
{selectedTicket?.priority}
</Text>
</View>
<View className="bg-muted/40 px-3 py-1.5 rounded-[6px] flex-row items-center gap-2">
{selectedTicket &&
getStatusInfo(selectedTicket.status).icon}
<Text className="text-xs text-foreground font-medium uppercase">
{selectedTicket?.status}
</Text>
</View>
</View>
<View className="p-4 bg-muted/20 rounded-[6px] border border-border/30">
<Text className="text-foreground leading-6">
{selectedTicket?.message}
</Text>
</View>
</View>
{selectedTicket?.resolution && (
<View className="mb-6">
<View className="flex-row items-center gap-2 mb-3">
<View className="h-6 w-6 rounded-full bg-green-500/20 items-center justify-center">
<CheckCircle2 size={14} color="#10b981" />
</View>
<Text className="text-foreground font-bold">
Resolution
</Text>
</View>
<View className="p-4 bg-green-500/5 rounded-2xl border border-green-500/20">
<Text className="text-foreground leading-6">
{selectedTicket.resolution}
</Text>
</View>
</View>
)}
<View className="mb-4 gap-3">
<View className="flex-row items-center justify-between p-3 bg-muted/10 rounded-xl">
<View className="flex-row items-center gap-2">
<Calendar size={14} color="#94a3b8" />
<Text className="text-xs text-muted-foreground">
Created on
</Text>
</View>
<Text className="text-xs text-foreground font-semibold">
{selectedTicket &&
new Date(selectedTicket.createdAt).toLocaleString()}
</Text>
</View>
<View className="flex-row items-center justify-between p-3 bg-muted/10 rounded-xl">
<View className="flex-row items-center gap-2">
<UserIcon size={14} color="#94a3b8" />
<Text className="text-xs text-muted-foreground">
Requester
</Text>
</View>
<Text className="text-xs text-foreground font-semibold">
{selectedTicket?.requesterName ||
selectedTicket?.requesterEmail}
</Text>
</View>
</View>
</ScrollView>
<View className="p-5 bg-muted/20 border-t border-border/50">
<Button
onPress={() => setSelectedTicket(null)}
className="rounded-[6px]"
>
<Text className="text-white font-bold">Close Details</Text>
</Button>
</View>
</View>
</View>
</Modal>
{/* New Ticket Modal */}
<Modal
visible={isModalVisible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={() => setIsModalVisible(false)}
>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
className="flex-1 bg-background"
>
<View className="px-5 py-6 border-b border-border flex-row justify-between items-center">
<Text variant="h4" className="text-foreground font-bold">
New Ticket
</Text>
<Pressable
onPress={() => setIsModalVisible(false)}
className="h-8 w-8 rounded-[10px] bg-card items-center justify-center border border-border"
>
<X size={20} color={iconColor} />
</Pressable>
</View>
<ScrollView
className="flex-1 px-5 pt-6"
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View className="mb-6">
<Text className="text-foreground font-bold mb-3">
What's the issue?
</Text>
<TextInput
value={subject}
onChangeText={setSubject}
placeholder="Subject of your request"
placeholderTextColor="#94a3b8"
className="bg-card border border-border rounded-[6px] px-4 py-4 text-foreground shadow-sm"
/>
</View>
<View className="mb-6">
<Text className="text-foreground font-bold mb-3">
Priority Level
</Text>
<View className="flex-row flex-wrap gap-2">
{PRIORITIES.map((p) => (
<Pressable
key={p.value}
onPress={() => setPriority(p.value)}
className={cn(
"px-5 py-1 rounded-[6px] border flex-row items-center gap-2",
priority === p.value
? "border-primary bg-primary/10"
: "border-border bg-card",
)}
>
<Text
className={cn(
"font-bold text-sm",
priority === p.value
? "text-primary"
: "text-muted-foreground",
)}
>
{p.label}
</Text>
</Pressable>
))}
</View>
</View>
<View className="mb-8">
<Text className="text-foreground font-bold mb-3">
Message Details
</Text>
<TextInput
value={message}
onChangeText={setMessage}
placeholder="Explain the problem in detail so we can help you faster..."
placeholderTextColor="#94a3b8"
multiline
numberOfLines={6}
textAlignVertical="top"
className="bg-card border border-border rounded-[6px] px-4 py-4 text-foreground min-h-[160px] shadow-sm"
/>
</View>
<Button
onPress={handleSubmit}
disabled={isSubmitting}
className="rounded-[6px] bg-primary h-12"
>
{isSubmitting ? (
<ActivityIndicator color="white" size="small" />
) : (
<Text className="text-white font-bold">Submit Ticket</Text>
)}
</Button>
<View className="h-20" />
</ScrollView>
</KeyboardAvoidingView>
</Modal>
</ScreenWrapper>
);
}