aboutsummaryrefslogtreecommitdiff
path: root/web/src/pages/NewTicketPage.tsx
diff options
context:
space:
mode:
authorzwlucas <lucas.fariamo08@gmail.com>2026-07-25 21:23:29 +0000
committerzwlucas <lucas.fariamo08@gmail.com>2026-07-25 21:23:29 +0000
commitef55d9bb24430610b60c1b77821307cd72c08c55 (patch)
treee6935a810b34370b0a4c821e55f6397aac50c832 /web/src/pages/NewTicketPage.tsx
downloadhelpdesk-master.tar.gz
helpdesk-master.zip
feat: upload of the projectHEADmaster
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'web/src/pages/NewTicketPage.tsx')
-rw-r--r--web/src/pages/NewTicketPage.tsx203
1 files changed, 203 insertions, 0 deletions
diff --git a/web/src/pages/NewTicketPage.tsx b/web/src/pages/NewTicketPage.tsx
new file mode 100644
index 0000000..d716f1e
--- /dev/null
+++ b/web/src/pages/NewTicketPage.tsx
@@ -0,0 +1,203 @@
+/**
+ * NewTicketPage.tsx
+ *
+ * Formulário para abertura de um novo chamado de TI.
+ * Visão do Solicitante — Fase 4, Passo 2.
+ */
+
+import { useState, type FormEvent } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { api, type TicketPriority } from '@/lib/api';
+import { useAuth } from '@/context/AuthContext';
+import { useToast } from '@/context/ToastContext';
+import styles from './NewTicketPage.module.css';
+
+// ─── Dados estáticos dos selects ──────────────────────────────────────────────
+
+const PRIORITIES: { value: TicketPriority; label: string; description: string }[] = [
+ { value: 'low', label: '🟢 Baixa', description: 'Sem impacto imediato' },
+ { value: 'medium', label: '🔵 Média', description: 'Impacto moderado' },
+ { value: 'high', label: '🟡 Alta', description: 'Impacto significativo' },
+ { value: 'critical', label: '🔴 Crítica', description: 'Sistema/serviço fora do ar' },
+];
+
+// ─── Componente ───────────────────────────────────────────────────────────────
+
+export default function NewTicketPage() {
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const { user } = useAuth();
+ const { addToast } = useToast();
+
+ // Estado do formulário
+ const [title, setTitle] = useState('');
+ const [description, setDesc] = useState('');
+ const [priority, setPriority] = useState<TicketPriority>('medium');
+ const [categoryId, setCategoryId] = useState<number | undefined>(undefined);
+
+ // Query: categorias
+ const { data: categories = [], isLoading: isLoadingCategories } = useQuery({
+ queryKey: ['categories'],
+ queryFn: () => api.categories.list(),
+ });
+
+ // Mutation: criação do ticket via API
+ const { mutate: createTicket, isPending } = useMutation({
+ mutationFn: () =>
+ api.tickets.create({
+ title: title.trim(),
+ description: description.trim(),
+ priority,
+ categoryId,
+ creatorId: user!.id,
+ }),
+ onSuccess: (data) => {
+ void queryClient.invalidateQueries({ queryKey: ['tickets'] });
+ addToast('Chamado criado com sucesso!', 'success');
+ navigate(`/tickets/${data.id}`);
+ },
+ onError: (err: any) => {
+ addToast(`Erro ao criar chamado: ${err.message}`, 'error');
+ },
+ });
+
+ function handleSubmit(e: FormEvent) {
+ e.preventDefault();
+ createTicket();
+ }
+
+ return (
+ <div className={styles.page}>
+
+ {/* Cabeçalho da página */}
+ <div className={styles.header}>
+ <h2 className={styles.heading}>Novo Chamado</h2>
+ <p className={styles.subheading}>
+ Descreva o problema com o máximo de detalhes possível para agilizar o atendimento.
+ </p>
+ </div>
+
+ {/* Formulário */}
+ <form className={styles.form} onSubmit={handleSubmit} noValidate>
+
+ {/* Título */}
+ <div className={styles.fieldGroup}>
+ <label htmlFor="ticket-title" className={styles.label}>
+ Título <span className={styles.required} aria-hidden="true">*</span>
+ </label>
+ <input
+ id="ticket-title"
+ type="text"
+ className="input"
+ placeholder="Ex: Computador não liga após atualização"
+ value={title}
+ onChange={(e) => setTitle(e.target.value)}
+ required
+ minLength={5}
+ maxLength={150}
+ disabled={isPending}
+ aria-describedby="title-hint"
+ />
+ <p id="title-hint" className={styles.hint}>
+ Seja objetivo — este é o resumo que aparece na fila dos agentes.
+ </p>
+ </div>
+
+ {/* Descrição */}
+ <div className={styles.fieldGroup}>
+ <label htmlFor="ticket-description" className={styles.label}>
+ Descrição detalhada <span className={styles.required} aria-hidden="true">*</span>
+ </label>
+ <textarea
+ id="ticket-description"
+ className={`input ${styles.textarea}`}
+ placeholder="Descreva o que aconteceu, quando começou, quais passos já foram tentados e o impacto no seu trabalho..."
+ value={description}
+ onChange={(e) => setDesc(e.target.value)}
+ required
+ minLength={10}
+ rows={6}
+ disabled={isPending}
+ />
+ </div>
+
+ {/* Linha: Prioridade + Categoria */}
+ <div className={styles.row}>
+
+ {/* Prioridade */}
+ <div className={styles.fieldGroup}>
+ <label htmlFor="ticket-priority" className={styles.label}>
+ Prioridade
+ </label>
+ <select
+ id="ticket-priority"
+ className="input"
+ value={priority}
+ onChange={(e) => setPriority(e.target.value as TicketPriority)}
+ disabled={isPending}
+ >
+ {PRIORITIES.map((p) => (
+ <option key={p.value} value={p.value}>
+ {p.label} — {p.description}
+ </option>
+ ))}
+ </select>
+ </div>
+
+ {/* Categoria */}
+ <div className={styles.fieldGroup}>
+ <label htmlFor="ticket-category" className={styles.label}>
+ Categoria
+ </label>
+ <select
+ id="ticket-category"
+ className="input"
+ value={categoryId ?? ''}
+ onChange={(e) =>
+ setCategoryId(e.target.value ? Number(e.target.value) : undefined)
+ }
+ disabled={isPending || isLoadingCategories}
+ >
+ <option value="">Selecione uma categoria...</option>
+ {categories.map((c) => (
+ <option key={c.id} value={c.id}>
+ {c.name}
+ </option>
+ ))}
+ </select>
+ </div>
+ </div>
+
+ {/* Ações */}
+ <div className={styles.actions}>
+ <button
+ type="button"
+ className="btn btn--ghost"
+ onClick={() => navigate('/dashboard')}
+ disabled={isPending}
+ >
+ Cancelar
+ </button>
+
+ <button
+ type="submit"
+ className="btn btn--primary"
+ disabled={isPending || title.trim().length < 5 || description.trim().length < 10}
+ aria-busy={isPending}
+ >
+ {isPending ? (
+ <>
+ <span className="spinner" style={{ width: 16, height: 16, borderWidth: 2 }} />
+ Enviando...
+ </>
+ ) : (
+ 'Abrir Chamado'
+ )}
+ </button>
+ </div>
+
+ </form>
+ </div>
+ );
+}