Yaltopia-Tickets-App/app/help.tsx
2026-06-05 13:39:37 +03:00

558 lines
19 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,
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-xl bg-card border border-dashed border-border">
<View className="h-20 w-20 rounded-full bg-muted items-center justify-center mb-4">
<MessageSquare size={36} color="#765D58" />
</View>
<Text variant="title" className="text-center">
All clear
</Text>
<Text variant="muted" className="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 px-8"
>
<Text className="text-primary font-sans-semibold">Get Help</Text>
</Button>
</View>
) : (
<View className="gap-2.5">
{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)}
>
<Card>
<CardContent className="pt-3.5 pb-3.5">
<View className="flex-row justify-between items-center mb-3">
<Text className="text-xs text-muted-foreground font-mono tracking-tight">
{ticket.ticketNumber}
</Text>
<Text className="text-xs text-muted-foreground">
{new Date(ticket.createdAt).toLocaleDateString(undefined, {
month: "short",
day: "numeric",
})}
</Text>
</View>
<Text
className="text-foreground font-sans-bold text-[15px] leading-5 mb-3"
numberOfLines={2}
>
{ticket.subject}
</Text>
<View className="flex-row items-center gap-2">
<View
className={cn(
"px-2 py-0.5 rounded-[4px] 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-[9px] font-semibold uppercase tracking-widest",
pInfo.color,
)}
>
{pInfo.label}
</Text>
</View>
<View
className={cn(
"px-2 py-0.5 rounded-[4px] flex-row items-center gap-1.5",
sInfo.bg,
)}
>
<Text
className={cn(
"text-[9px] font-semibold uppercase tracking-widest",
sInfo.color,
)}
>
{sInfo.label}
</Text>
</View>
</View>
</CardContent>
</Card>
</Pressable>
);
})}
</View>
)}
</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-xl overflow-hidden border border-border">
<View className="px-5 py-4 border-b border-border flex-row justify-between items-center">
<View>
<Text variant="small" className="text-primary uppercase tracking-widest mb-0.5">
TICKET
</Text>
<Text variant="title">
{selectedTicket?.ticketNumber}
</Text>
</View>
<Pressable
onPress={() => setSelectedTicket(null)}
className="h-8 w-8 rounded-lg items-center justify-center"
>
<X size={20} color={iconColor} />
</Pressable>
</View>
<ScrollView className="max-h-[70%] px-5 pt-5 pb-6">
<Text variant="title" className="mb-3">
{selectedTicket?.subject}
</Text>
<View className="flex-row flex-wrap gap-2 mb-5">
<View className="bg-muted px-2.5 py-1 rounded-[4px] flex-row items-center gap-1.5">
<Tag size={12} color="#765D58" />
<Text className="text-[9px] font-semibold uppercase tracking-widest text-muted-foreground">
{selectedTicket?.priority}
</Text>
</View>
<View className="bg-muted px-2.5 py-1 rounded-[4px] flex-row items-center gap-1.5">
{selectedTicket && getStatusInfo(selectedTicket.status).icon}
<Text className="text-[9px] font-semibold uppercase tracking-widest text-muted-foreground">
{selectedTicket?.status}
</Text>
</View>
</View>
<View className="p-4 bg-muted rounded-lg mb-6">
<Text className="text-foreground leading-6">
{selectedTicket?.message}
</Text>
</View>
{selectedTicket?.resolution && (
<View className="mb-6">
<View className="flex-row items-center gap-2 mb-2">
<View className="h-5 w-5 rounded-full bg-green-500/20 items-center justify-center">
<CheckCircle2 size={12} color="#10b981" />
</View>
<Text className="text-foreground font-sans-bold text-sm">
Resolution
</Text>
</View>
<View className="p-4 bg-muted rounded-lg">
<Text className="text-foreground leading-6">
{selectedTicket.resolution}
</Text>
</View>
</View>
)}
<View className="border-t border-border pt-4 gap-3">
<View className="flex-row items-center justify-between">
<View className="flex-row items-center gap-2">
<Calendar size={14} color="#765D58" />
<Text className="text-sm text-muted-foreground">Created</Text>
</View>
<Text className="text-sm text-foreground font-sans-semibold">
{selectedTicket &&
new Date(selectedTicket.createdAt).toLocaleDateString()}
</Text>
</View>
<View className="flex-row items-center justify-between">
<View className="flex-row items-center gap-2">
<UserIcon size={14} color="#765D58" />
<Text className="text-sm text-muted-foreground">Requester</Text>
</View>
<Text className="text-sm text-foreground font-sans-semibold">
{selectedTicket?.requesterName ||
selectedTicket?.requesterEmail}
</Text>
</View>
</View>
</ScrollView>
<View className="px-5 py-4 border-t border-border">
<Button onPress={() => setSelectedTicket(null)}>
<Text className="text-white font-sans-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-4 border-b border-border flex-row justify-between items-center">
<Text variant="title">New Ticket</Text>
<Pressable
onPress={() => setIsModalVisible(false)}
className="h-8 w-8 rounded-lg items-center justify-center"
>
<X size={20} color={iconColor} />
</Pressable>
</View>
<ScrollView
className="flex-1 px-5 pt-6"
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View className="mb-6">
<Text variant="small" className="mb-2 ml-1">
Subject
</Text>
<TextInput
value={subject}
onChangeText={setSubject}
placeholder="Brief summary of the issue"
placeholderTextColor="#765D58"
className="bg-card border border-input-border rounded-md px-4 h-12 text-foreground"
/>
</View>
<View className="mb-6">
<Text variant="small" className="mb-2 ml-1">
Priority
</Text>
<View className="flex-row flex-wrap gap-2">
{PRIORITIES.map((p) => (
<Pressable
key={p.value}
onPress={() => setPriority(p.value)}
className={cn(
"px-4 py-1.5 rounded-[4px] border",
priority === p.value
? "border-primary bg-primary/10"
: "border-border bg-card",
)}
>
<Text
className={cn(
"text-sm font-sans-semibold",
priority === p.value
? "text-primary"
: "text-muted-foreground",
)}
>
{p.label}
</Text>
</Pressable>
))}
</View>
</View>
<View className="mb-8">
<Text variant="small" className="mb-2 ml-1">
Message
</Text>
<TextInput
value={message}
onChangeText={setMessage}
placeholder="Describe the issue in detail so we can help faster..."
placeholderTextColor="#765D58"
multiline
numberOfLines={6}
textAlignVertical="top"
className="bg-card border border-input-border rounded-md px-4 py-3 text-foreground min-h-[140px]"
/>
</View>
<Button
onPress={handleSubmit}
disabled={isSubmitting}
className="h-12"
>
{isSubmitting ? (
<ActivityIndicator color="white" size="small" />
) : (
<Text className="text-white font-sans-bold">Submit Ticket</Text>
)}
</Button>
<View className="h-20" />
</ScrollView>
</KeyboardAvoidingView>
</Modal>
</ScreenWrapper>
);
}