Add support

This commit is contained in:
Arthur Belleville 2025-07-06 21:46:18 +02:00
parent ecde2d798c
commit 7b879ebfb8
No known key found for this signature in database
6 changed files with 290 additions and 3 deletions

View file

@ -151,6 +151,13 @@ export type Database = {
referencedRelation: "user_tablos"
referencedColumns: ["id"]
},
{
foreignKeyName: "fk_tablo_access_user_id_from_profiles"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "profiles"
referencedColumns: ["id"]
},
]
}
tablo_invites: {
@ -244,7 +251,15 @@ export type Database = {
status: string | null
user_id: string | null
}
Relationships: []
Relationships: [
{
foreignKeyName: "fk_tablo_access_user_id_from_profiles"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "profiles"
referencedColumns: ["id"]
},
]
}
}
Functions: {

View file

@ -17,6 +17,7 @@ import { PlanningPage } from "./pages/planning";
import { ChantiersPage } from "./pages/chantiers";
import { ChatPage } from "./pages/chat";
import { FeedbackPage } from "./pages/feedback";
import { SupportPage } from "./pages/support";
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
import ChatProvider from "./providers/ChatProvider";
import { UserStoreProvider } from "./providers/UserStoreProvider";
@ -106,6 +107,14 @@ export const App = () => {
</Layout>
}
/>
<Route
path="support"
element={
<Layout>
<SupportPage />
</Layout>
}
/>
</Route>
<Route

View file

@ -329,7 +329,7 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
<li>
<NavLink>
<RouterLink
to="/"
to="/support"
className={twMerge("w-full", isCollapsed ? "" : "pl-2")}
aria-label={isCollapsed ? "Support" : undefined}
>

41
ui/src/hooks/support.ts Normal file
View file

@ -0,0 +1,41 @@
import { useMutation } from "@tanstack/react-query";
import { supabase } from "./auth";
import { useUser } from "@ui/providers/UserStoreProvider";
export interface SupportTicketData {
issue_type:
| "bug"
| "performance"
| "security"
| "feature_request"
| "account"
| "other";
severity: "low" | "medium" | "high" | "critical";
title: string;
description: string;
}
// Create new support ticket
export const useCreateSupportTicket = () => {
const user = useUser();
const { mutate, isSuccess, isPending } = useMutation({
mutationFn: async ({
issue_type,
severity,
title,
description,
}: SupportTicketData) => {
const { error } = await supabase.from("support_tickets").insert({
issue_type,
severity,
title,
description,
user_id: user?.id ?? "",
});
if (error) throw error;
},
onSuccess: () => {},
});
return { createSupportTicket: mutate, isSuccess, isPending };
};

207
ui/src/pages/support.tsx Normal file
View file

@ -0,0 +1,207 @@
import React, { useState } from "react";
import { Button } from "@ui/ui-library/button";
import { Form } from "@ui/ui-library/form";
import { TextField, Label, TextArea, Description } from "@ui/ui-library/field";
import { Text } from "@ui/ui-library/text";
import { Separator } from "react-aria-components";
import { SendIcon, ArrowLeftIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { useNavigate } from "react-router-dom";
import { useCreateSupportTicket, SupportTicketData } from "@ui/hooks/support";
export function SupportPage() {
const navigate = useNavigate();
const [formData, setFormData] = useState<SupportTicketData>({
issue_type: "bug",
severity: "medium",
title: "",
description: "",
});
const { createSupportTicket, isSuccess, isPending } =
useCreateSupportTicket();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
createSupportTicket(formData);
};
const handleInputChange = (field: keyof SupportTicketData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
return (
<div className="max-w-2xl mx-auto p-6">
{/* Header */}
<div className="mb-6">
<div className="flex items-center gap-4 mb-4">
<Button
variant="outline"
isIconOnly
onPress={() => navigate(-1)}
aria-label="Retour"
className="shrink-0"
>
<ArrowLeftIcon className="w-4 h-4" />
</Button>
<div>
<Text className="text-2xl font-bold">Support technique</Text>
<Text className="text-gray-600 dark:text-gray-400 mt-1">
Signalez un problème ou demandez de l&apos;aide
</Text>
</div>
</div>
</div>
<Separator className="mb-6" />
{isSuccess ? (
<div className="text-center py-12">
<div className="text-green-600 mb-4">
<SendIcon className="w-12 h-12 mx-auto" />
</div>
<Text className="text-xl font-medium text-green-600 mb-2">
Votre ticket de support a é créé !
</Text>
<Text className="text-gray-600 dark:text-gray-400 mb-6">
Votre demande a é envoyée avec succès. Notre équipe de support
vous répondra dans les plus brefs délais.
</Text>
<Button variant="outline" onPress={() => navigate(-1)}>
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Retour
</Button>
</div>
) : (
<div className="bg-white dark:bg-gray-800 rounded-lg border p-6">
<Form onSubmit={handleSubmit} className="space-y-6">
{/* Issue Type */}
<TextField>
<Label>Type de problème</Label>
<select
value={formData.issue_type}
onChange={(e) =>
handleInputChange(
"issue_type",
e.target.value as SupportTicketData["issue_type"]
)
}
className={twMerge(
"w-full rounded-md border border-gray-300 dark:border-gray-600",
"px-3 py-2 bg-white dark:bg-gray-700",
"text-gray-900 dark:text-gray-100",
"focus:border-blue-500 focus:ring-1 focus:ring-blue-500",
"outline-none"
)}
required
>
<option value="bug">Bug / Erreur</option>
<option value="performance">Problème de performance</option>
<option value="security">Problème de sécurité</option>
<option value="account">Problème de compte</option>
<option value="other">Autre</option>
</select>
</TextField>
{/* Severity */}
<TextField>
<Label>Niveau de priorité</Label>
<select
value={formData.severity}
onChange={(e) =>
handleInputChange(
"severity",
e.target.value as SupportTicketData["severity"]
)
}
className={twMerge(
"w-full rounded-md border border-gray-300 dark:border-gray-600",
"px-3 py-2 bg-white dark:bg-gray-700",
"text-gray-900 dark:text-gray-100",
"focus:border-blue-500 focus:ring-1 focus:ring-blue-500",
"outline-none"
)}
required
>
<option value="low">Faible</option>
<option value="medium">Moyen</option>
<option value="high">Élevé</option>
<option value="critical">Critique</option>
</select>
</TextField>
{/* Title Field */}
<TextField>
<Label>Titre du problème</Label>
<input
type="text"
value={formData.title}
onChange={(e) => handleInputChange("title", e.target.value)}
placeholder="Titre du problème"
className={twMerge(
"w-full rounded-md border border-gray-300 dark:border-gray-600",
"px-3 py-2 bg-white dark:bg-gray-700",
"text-gray-900 dark:text-gray-100",
"placeholder-gray-400 dark:placeholder-gray-500",
"focus:border-blue-500 focus:ring-1 focus:ring-blue-500",
"outline-none"
)}
required
/>
</TextField>
{/* Description Field */}
<TextField>
<Label>Description détaillée</Label>
<TextArea
value={formData.description}
onChange={(e) =>
handleInputChange("description", e.target.value)
}
placeholder="Veuillez décrire votre problème en détail..."
rows={8}
required
className="resize-none"
/>
<Description>
Plus vous donnez de détails, plus nous pourrons vous aider
rapidement
</Description>
</TextField>
{/* Submit Button */}
<div className="flex justify-end gap-3 pt-4">
<Button
variant="outline"
onPress={() => navigate(-1)}
type="button"
>
Annuler
</Button>
<Button
variant="solid"
type="submit"
isDisabled={
isPending || !formData.title || !formData.description
}
className="min-w-32"
>
{isPending ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Envoi en cours...
</span>
) : (
<span className="flex items-center gap-2">
<SendIcon className="w-4 h-4" />
Envoyer le ticket
</span>
)}
</Button>
</div>
</Form>
</div>
)}
</div>
);
}

View file

@ -151,6 +151,13 @@ export type Database = {
referencedRelation: "user_tablos"
referencedColumns: ["id"]
},
{
foreignKeyName: "fk_tablo_access_user_id_from_profiles"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "profiles"
referencedColumns: ["id"]
},
]
}
tablo_invites: {
@ -244,7 +251,15 @@ export type Database = {
status: string | null
user_id: string | null
}
Relationships: []
Relationships: [
{
foreignKeyName: "fk_tablo_access_user_id_from_profiles"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "profiles"
referencedColumns: ["id"]
},
]
}
}
Functions: {