Skip to content

Commit 7901178

Browse files
authored
Merge pull request #18 from WildCodeSchool/feature/add-ticket-interfaces
Add ticket interfaces
2 parents ce6b843 + 5c59907 commit 7901178

File tree

18 files changed

+1871
-801
lines changed

18 files changed

+1871
-801
lines changed

package-lock.json

Lines changed: 807 additions & 794 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@
1616
"@graphql-codegen/typescript-operations": "^4.6.0",
1717
"@graphql-codegen/typescript-react-apollo": "^4.3.2",
1818
"@hookform/resolvers": "^5.2.1",
19-
"@radix-ui/react-checkbox": "^1.3.2",
2019
"@radix-ui/react-dropdown-menu": "^2.1.16",
20+
"@radix-ui/react-checkbox": "^1.3.3",
2121
"@radix-ui/react-label": "^2.1.7",
22+
"@radix-ui/react-popover": "^1.1.15",
23+
"@radix-ui/react-scroll-area": "^1.2.10",
2224
"@radix-ui/react-slot": "^1.2.3",
2325
"@radix-ui/react-tabs": "^1.1.12",
2426
"@radix-ui/react-tooltip": "^1.2.7",
2527
"@tailwindcss/vite": "^4.1.11",
28+
"@tanstack/react-table": "^8.21.3",
2629
"class-variance-authority": "^0.7.1",
2730
"clsx": "^2.1.1",
31+
"dayjs": "^1.11.18",
2832
"graphql": "^16.10.0",
2933
"lucide-react": "^0.534.0",
3034
"qrcode.react": "^4.2.0",

public/avatar-example.jpg

202 KB
Loading

src/App.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
4747
--color-sidebar-border: var(--sidebar-border);
4848
--color-sidebar-ring: var(--sidebar-ring);
49+
--color-status-done: var(--status-done);
50+
--color-status-created: var(--status-created);
51+
--color-status-canceled: var(--status-canceled);
52+
--color-status-pending: var(--status-pending);
53+
--color-status-inprogress: var(--status-inprogress);
4954
}
5055

5156
:root {
@@ -81,6 +86,11 @@
8186
--sidebar-accent-foreground: oklch(0.205 0 0);
8287
--sidebar-border: oklch(0.922 0 0);
8388
--sidebar-ring: oklch(0.708 0 0);
89+
--status-done: oklch(0.9612 0.0193 147.85);
90+
--status-created: oklch(0.6861 0.129 149.18);
91+
--status-canceled: oklch(0.398 0.07 227.392);
92+
--status-pending: oklch(0.828 0.189 84.429);
93+
--status-inprogress: oklch(0.769 0.188 70.08);
8494
}
8595

8696
@layer base {

src/components/dashboard/DashboardLayout.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Outlet } from "react-router";
1+
import { Outlet, useLocation } from "react-router";
22
import DashboardMenuTooltip from "./DashboardMenuTooltip";
33
import { FaTicketSimple } from "react-icons/fa6";
44
import { MdRoomService } from "react-icons/md";
@@ -16,6 +16,8 @@ export type DashboardMenuItem = {
1616
export default function DashboardLayout() {
1717
const { user, logout } = useAuth();
1818

19+
const location = useLocation();
20+
1921
const dashboardMenu: DashboardMenuItem[] = [
2022
{
2123
name: "Accueil",
@@ -46,7 +48,11 @@ export default function DashboardLayout() {
4648
};
4749

4850
const isActive = (path: string) => {
49-
return window.location.pathname === path;
51+
if (path === "/dashboard") {
52+
return location.pathname === path;
53+
}
54+
55+
return location.pathname.startsWith(path);
5056
};
5157

5258
return (

src/components/dashboard/tickets/.gitkeep

Whitespace-only changes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createElement } from "react";
2+
import { IconType } from "react-icons/lib";
3+
4+
export default function TicketInfos({
5+
information,
6+
icon,
7+
}: {
8+
information: string;
9+
icon: IconType;
10+
}) {
11+
return (
12+
<div className="flex flex-row items-center justify-start">
13+
{icon && createElement(icon, { className: "mr-4", size: 20 })}
14+
<p>{information || "N/A"}</p>
15+
</div>
16+
);
17+
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { useNavigate, useParams } from "react-router-dom";
2+
import { IoIosArrowBack, IoIosMore } from "react-icons/io";
3+
import { GET_TICKET_INFOS } from "@/requests/tickets.requests";
4+
import { useQuery } from "@apollo/client/react";
5+
import { MdOutlineEmail } from "react-icons/md";
6+
import { FaPhoneAlt } from "react-icons/fa";
7+
import { FaPerson } from "react-icons/fa6";
8+
import { MdRoomService } from "react-icons/md";
9+
import { FaPlus } from "react-icons/fa6";
10+
import { MdOutlineEdit } from "react-icons/md";
11+
import { FaTicketSimple } from "react-icons/fa6";
12+
import { GET_TICKET_LOGS } from "@/requests/ticketLogs.requests";
13+
import { useState } from "react";
14+
import { Textarea } from "@/components/ui/textarea";
15+
import { Button } from "@/components/ui/button";
16+
import { statusOptions } from "@/lib/ticketUtils";
17+
import TicketInfos from "./TicketInfos";
18+
19+
type RouteParams = {
20+
id: string;
21+
};
22+
23+
type GetTicketType = {
24+
ticket: Ticket;
25+
};
26+
27+
export type Ticket = {
28+
code: string;
29+
email: string;
30+
createdAt: string;
31+
firstName: string;
32+
id: string;
33+
lastName: string;
34+
phone: string;
35+
status: string;
36+
updatedAt: string;
37+
service: {
38+
id: string;
39+
name: string;
40+
};
41+
};
42+
43+
export default function TicketPage() {
44+
const { id } = useParams<RouteParams>();
45+
46+
const navigate = useNavigate();
47+
48+
const { data, loading, error } = useQuery<GetTicketType>(GET_TICKET_INFOS, {
49+
variables: { ticketId: id },
50+
});
51+
52+
const ticketOptions = statusOptions.find(
53+
(option) => option.value === data?.ticket.status
54+
);
55+
56+
const [isEditingComments, setIsEditingComments] = useState(false);
57+
const [comments, setComments] = useState("");
58+
59+
const {
60+
data: ticketLogs,
61+
loading: ticketLogsLoading,
62+
error: ticketLogsError,
63+
} = useQuery(GET_TICKET_LOGS, {
64+
variables: { field: { ticketId: id } },
65+
});
66+
67+
const ticketLogSentence = (log: any) => {
68+
switch (log.status) {
69+
case "CREATED":
70+
return `Ticket créé`;
71+
case "INPROGRESS":
72+
return `Ticket en cours de traitement`;
73+
case "CANCELED":
74+
return `Ticket annulé`;
75+
case "DONE":
76+
return `Ticket traité`;
77+
case "ARCHIVED":
78+
return `Ticket archivé`;
79+
case "PENDING":
80+
return `Ticket mis en attente`;
81+
default:
82+
return "Statut inconnu";
83+
}
84+
};
85+
86+
if (!id) {
87+
return <p>Aucun ID fourni</p>;
88+
}
89+
90+
if (loading) return <p>Chargement...</p>;
91+
if (!data) return <p>Aucun ticket trouvé</p>;
92+
if (error) return <p>Erreur: {error.message}</p>;
93+
94+
return (
95+
<>
96+
<div className="flex flex-row items-center justify-start w-full">
97+
<div
98+
className="bg-card flex items-center justify-center rounded-full p-3 mr-5 cursor-pointer"
99+
onClick={() => navigate(-1)}
100+
>
101+
<IoIosArrowBack className="w-6 h-6 text-foreground cursor-pointer" />
102+
</div>
103+
<h1 className="scroll-m-20 text-4xl font-light tracking-tight text-balance mr-2">
104+
{data.ticket.firstName} {data.ticket.lastName}
105+
</h1>
106+
<span className="ml-4 px-4 py-2 rounded-lg text-sm font-light bg-primary text-white">
107+
Ticket {data.ticket.code}
108+
</span>
109+
<span
110+
className={`ml-4 px-4 py-2 rounded-lg text-sm font-light mr-6 ${
111+
ticketOptions ? ticketOptions.badgeStyle : ""
112+
}`}
113+
>
114+
{ticketOptions ? ticketOptions.label : data.ticket.status}
115+
</span>
116+
<IoIosMore size={20} />
117+
</div>
118+
<div className="flex flex-row items-stretch justify-between w-full h-full mt-10 gap-10">
119+
<div className="flex flex-col items-start justify-start gap-6 w-full">
120+
<div className="bg-card p-6 rounded-lg flex flex-col items-start justify-start gap-4 text-left w-full mr-4">
121+
<h2 className="scroll-m-20 text-xl font-light tracking-tight text-balance text-muted-foreground">
122+
Informations personnelles
123+
</h2>
124+
<TicketInfos
125+
information={`${data.ticket.firstName} ${data.ticket.lastName}`}
126+
icon={FaPerson}
127+
/>
128+
<TicketInfos
129+
information={data.ticket.email}
130+
icon={MdOutlineEmail}
131+
/>
132+
<TicketInfos information={data.ticket.phone} icon={FaPhoneAlt} />
133+
</div>
134+
<div className="bg-card p-6 rounded-lg flex flex-col items-start justify-start gap-4 text-left w-full mr-4">
135+
<h2 className="scroll-m-20 text-xl font-light tracking-tight text-balance text-muted-foreground">
136+
Informations sur le ticket
137+
</h2>
138+
<TicketInfos information={data.ticket.code} icon={FaTicketSimple} />
139+
<TicketInfos
140+
information={data.ticket.service.name || "N/A"}
141+
icon={MdRoomService}
142+
/>
143+
<TicketInfos
144+
information={`${new Date(
145+
data.ticket.createdAt
146+
).toLocaleDateString()} à ${" "}
147+
${new Date(data.ticket.createdAt).toLocaleTimeString()}`}
148+
icon={FaPlus}
149+
/>
150+
<TicketInfos
151+
information={`${new Date(
152+
data.ticket.updatedAt
153+
).toLocaleDateString()} à ${" "}
154+
${new Date(data.ticket.updatedAt).toLocaleTimeString()}`}
155+
icon={MdOutlineEdit}
156+
/>
157+
</div>
158+
</div>
159+
<div className="flex flex-col items-stretch justify-start w-full h-full gap-10">
160+
<div className="bg-card p-6 rounded-lg flex flex-col items-start justify-start gap-4 text-left w-full h-[45%]">
161+
<h2 className="text-xl font-light tracking-tight text-balance text-muted-foreground">
162+
Historique du ticket
163+
</h2>
164+
<div className="flex flex-col items-start justify-start w-full h-full overflow-y-auto">
165+
{ticketLogs &&
166+
ticketLogs.ticketLogsByProperty.map((log: any, idx: number) => {
167+
const isLast =
168+
idx === ticketLogs.ticketLogsByProperty.length - 1;
169+
return (
170+
<div
171+
key={log.id}
172+
className={`flex flex-row items-center justify-between p-4 w-full text-sm ${
173+
!isLast ? "border-b-2 border-muted" : ""
174+
}`}
175+
>
176+
<div className="flex flex-row items-center justify-start mr-4 gap-3">
177+
<img
178+
src="/avatar-example.jpg"
179+
alt=""
180+
className="w-7 h-7 rounded-full"
181+
/>
182+
{log.manager ? (
183+
<p className="mr-4 font-medium">
184+
{log.manager.firstName} {log.manager.lastName}
185+
</p>
186+
) : (
187+
<p className="mr-4 font-medium">Système</p>
188+
)}
189+
<p className="font-light">{ticketLogSentence(log)}</p>
190+
</div>
191+
<p className="font-light text-xs text-muted-foreground ml-4">
192+
{new Date(log.createdAt).toLocaleDateString("fr-FR", {
193+
day: "numeric",
194+
month: "numeric",
195+
year: "2-digit",
196+
})}
197+
<br />
198+
{new Date(log.createdAt).toLocaleTimeString("fr-FR", {
199+
hour: "2-digit",
200+
minute: "2-digit",
201+
})}
202+
</p>
203+
</div>
204+
);
205+
})}
206+
</div>
207+
</div>
208+
<div className="bg-card p-6 rounded-lg flex flex-col items-start justify-start gap-4 text-left w-full h-[55%]">
209+
<div className="flex flex-row items-center justify-between w-full">
210+
<h2 className="scroll-m-20 text-xl font-light tracking-tight text-balance text-muted-foreground">
211+
Commentaires
212+
</h2>
213+
<Button onClick={() => setIsEditingComments(true)}>
214+
Modifier les commentaires
215+
</Button>
216+
</div>
217+
{isEditingComments ? (
218+
<>
219+
<Textarea
220+
value={comments}
221+
onChange={(e) => setComments(e.target.value)}
222+
placeholder="Ajouter un commentaire..."
223+
rows={4}
224+
/>
225+
<Button onClick={() => setIsEditingComments(false)}>
226+
Sauvegarder
227+
</Button>
228+
</>
229+
) : (
230+
<p>{comments ? comments : "Aucun commentaire"}</p>
231+
)}
232+
</div>
233+
</div>
234+
</div>
235+
</>
236+
);
237+
}

0 commit comments

Comments
 (0)