558 lines
19 KiB
TypeScript
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>
|
|
);
|
|
}
|