From 02407a31fbec64d6292f0a75cc1f08207a014901 Mon Sep 17 00:00:00 2001 From: Ricel Leite Date: Wed, 18 Feb 2026 18:37:29 -0300 Subject: [PATCH] feat: Enterprise-grade TicketHub portal FEATURES: - Dashboard with KPIs and recent activity - Tickets list with filters (project, status, priority) - Ticket detail with comments, assignee, status management - Kanban board with drag-and-drop - Projects management (CRUD, webhooks) - Team management (invite, roles) - Reports & Analytics (charts, KPIs, top performers) - Integrations (GitHub, GitLab, Slack, JIRA AI Fixer) - Automation rules engine - Settings (general, notifications, security, API) UI: - Professional light theme - Reusable component library - Responsive sidebar navigation - Search functionality - Modal dialogs TECH: - React 18 + TypeScript - TailwindCSS - React Query - React Router --- .gitignore | 23 +++ README.md | 125 +++++++------- frontend/index.html | 13 ++ frontend/package.json | 30 ++++ frontend/postcss.config.js | 3 + frontend/public/favicon.svg | 1 + frontend/src/App.tsx | 33 ++++ frontend/src/components/Layout.tsx | 135 +++++++++++++++ frontend/src/components/ui/Avatar.tsx | 6 + frontend/src/components/ui/Badge.tsx | 16 ++ frontend/src/components/ui/Button.tsx | 31 ++++ frontend/src/components/ui/Card.tsx | 8 + frontend/src/components/ui/Input.tsx | 24 +++ frontend/src/components/ui/Modal.tsx | 23 +++ frontend/src/components/ui/Select.tsx | 17 ++ frontend/src/components/ui/Table.tsx | 25 +++ frontend/src/components/ui/Tabs.tsx | 30 ++++ frontend/src/components/ui/index.ts | 9 + frontend/src/index.css | 4 + frontend/src/main.tsx | 18 ++ frontend/src/pages/Automation.tsx | 163 ++++++++++++++++++ frontend/src/pages/Board.tsx | 99 +++++++++++ frontend/src/pages/Dashboard.tsx | 143 ++++++++++++++++ frontend/src/pages/Integrations.tsx | 128 ++++++++++++++ frontend/src/pages/NewTicket.tsx | 80 +++++++++ frontend/src/pages/Projects.tsx | 172 +++++++++++++++++++ frontend/src/pages/Reports.tsx | 135 +++++++++++++++ frontend/src/pages/Settings.tsx | 175 +++++++++++++++++++ frontend/src/pages/Team.tsx | 118 +++++++++++++ frontend/src/pages/TicketDetail.tsx | 232 ++++++++++++++++++++++++++ frontend/src/pages/Tickets.tsx | 141 ++++++++++++++++ frontend/src/services/api.ts | 62 +++++++ frontend/tailwind.config.js | 5 + frontend/tsconfig.json | 17 ++ frontend/vite.config.ts | 11 ++ 35 files changed, 2190 insertions(+), 65 deletions(-) create mode 100644 .gitignore create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/Layout.tsx create mode 100644 frontend/src/components/ui/Avatar.tsx create mode 100644 frontend/src/components/ui/Badge.tsx create mode 100644 frontend/src/components/ui/Button.tsx create mode 100644 frontend/src/components/ui/Card.tsx create mode 100644 frontend/src/components/ui/Input.tsx create mode 100644 frontend/src/components/ui/Modal.tsx create mode 100644 frontend/src/components/ui/Select.tsx create mode 100644 frontend/src/components/ui/Table.tsx create mode 100644 frontend/src/components/ui/Tabs.tsx create mode 100644 frontend/src/components/ui/index.ts create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/Automation.tsx create mode 100644 frontend/src/pages/Board.tsx create mode 100644 frontend/src/pages/Dashboard.tsx create mode 100644 frontend/src/pages/Integrations.tsx create mode 100644 frontend/src/pages/NewTicket.tsx create mode 100644 frontend/src/pages/Projects.tsx create mode 100644 frontend/src/pages/Reports.tsx create mode 100644 frontend/src/pages/Settings.tsx create mode 100644 frontend/src/pages/Team.tsx create mode 100644 frontend/src/pages/TicketDetail.tsx create mode 100644 frontend/src/pages/Tickets.tsx create mode 100644 frontend/src/services/api.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71a9a7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Python +__pycache__/ +*.py[cod] +.env +venv/ + +# Node +node_modules/ +dist/ +.env.local + +# IDE +.idea/ +.vscode/ +*.swp + +# OS +.DS_Store +Thumbs.db + +# Database +*.db +*.sqlite diff --git a/README.md b/README.md index 774f1d0..55c4453 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,83 @@ -# 🎫 TicketHub +# TicketHub -Lightweight open-source ticket/issue tracking system with webhook support. +Enterprise-grade open-source ticket and issue tracking system. ## Features -- **Projects** - Organize tickets by project with unique keys (e.g., PROJ-123) -- **Tickets** - Create, update, and track issues with status and priority -- **Comments** - Add comments to tickets for collaboration -- **Webhooks** - Trigger external systems on ticket events -- **Simple** - SQLite database, no complex setup required +### Work Management +- πŸ“Š **Dashboard** - Overview with KPIs and recent activity +- 🎫 **Tickets** - Full CRUD with filters, search, and bulk actions +- πŸ“‹ **Kanban Board** - Drag-and-drop ticket management +- πŸ“ **Projects** - Organize tickets by project with unique keys + +### Team Collaboration +- πŸ‘₯ **Team Management** - Invite members with role-based access +- πŸ’¬ **Comments** - Discussion threads on tickets +- πŸ”” **Notifications** - Email and Slack alerts + +### Enterprise Features +- πŸ“ˆ **Reports & Analytics** - Performance metrics and insights +- πŸ”Œ **Integrations** - GitHub, GitLab, Jira, ServiceNow, Slack +- ⚑ **Automation** - Rules engine for repetitive tasks +- πŸ” **Security** - SSO, 2FA, IP restrictions + +### API & Webhooks +- RESTful API with full documentation +- Incoming/outgoing webhooks +- API key management + +## Tech Stack + +### Backend +- Python 3.11 + FastAPI +- SQLite (development) / PostgreSQL (production) +- Async/await architecture + +### Frontend +- React 18 + TypeScript +- TailwindCSS +- React Query +- React Router ## Quick Start -### Docker - -```bash -docker-compose up -d -``` - -Access at http://localhost:8080 - -### Manual - ```bash +# Backend cd backend pip install -r requirements.txt -uvicorn app.main:app --host 0.0.0.0 --port 8000 +uvicorn app.main:app --reload + +# Frontend +cd frontend +npm install +npm run dev ``` ## API Endpoints -### Projects -- `GET /api/projects` - List all projects -- `POST /api/projects` - Create project -- `GET /api/projects/{id}` - Get project -- `DELETE /api/projects/{id}` - Delete project - -### Tickets -- `GET /api/tickets` - List tickets (filter by `project_id`, `status`) -- `POST /api/tickets` - Create ticket -- `GET /api/tickets/{id}` - Get ticket -- `GET /api/tickets/key/{key}` - Get ticket by key (e.g., PROJ-123) -- `PATCH /api/tickets/{id}` - Update ticket -- `DELETE /api/tickets/{id}` - Delete ticket - -### Comments -- `GET /api/tickets/{id}/comments` - List comments -- `POST /api/tickets/{id}/comments` - Add comment - -### Webhooks -- `GET /api/webhooks` - List webhooks -- `POST /api/webhooks` - Create webhook -- `DELETE /api/webhooks/{id}` - Delete webhook -- `PATCH /api/webhooks/{id}/toggle` - Enable/disable webhook - -## Webhook Events - -When configured, TicketHub sends POST requests to your webhook URL: - -```json -{ - "event": "ticket.created", - "timestamp": "2026-02-18T12:00:00Z", - "data": { - "id": 1, - "key": "PROJ-1", - "title": "Issue title", - "description": "...", - "status": "open", - "priority": "medium" - } -} ``` +GET /api/projects +POST /api/projects +GET /api/projects/:id +PATCH /api/projects/:id +DELETE /api/projects/:id -Events: `ticket.created`, `ticket.updated`, `comment.added` +GET /api/tickets +POST /api/tickets +GET /api/tickets/:id +PATCH /api/tickets/:id +DELETE /api/tickets/:id -## Integration with JIRA AI Fixer +GET /api/tickets/:id/comments +POST /api/tickets/:id/comments -Configure webhook URL in your project pointing to JIRA AI Fixer: - -``` -https://jira-fixer.example.com/api/webhook/tickethub +POST /api/webhooks/incoming ``` ## License MIT + +## Credits + +Created by StartData diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c79f63c --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + TicketHub + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..524557c --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "tickethub-portal", + "version": "1.0.0", + "description": "TicketHub - Enterprise Issue Tracking System", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.0", + "@tanstack/react-query": "^5.17.0", + "axios": "^1.6.0", + "date-fns": "^3.3.0" + }, + "devDependencies": { + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.17", + "postcss": "^8.4.33", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", + "vite": "^5.0.12" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..be56e0e --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,3 @@ +export default { + plugins: { tailwindcss: {}, autoprefixer: {} }, +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..820929c --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ +🎫 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..14efeea --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,33 @@ +import { Routes, Route } from 'react-router-dom' +import Layout from './components/Layout' +import Dashboard from './pages/Dashboard' +import Tickets from './pages/Tickets' +import TicketDetail from './pages/TicketDetail' +import NewTicket from './pages/NewTicket' +import Board from './pages/Board' +import Projects from './pages/Projects' +import Team from './pages/Team' +import Reports from './pages/Reports' +import Integrations from './pages/Integrations' +import Automation from './pages/Automation' +import Settings from './pages/Settings' + +export default function App() { + return ( + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ) +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..2dfaa2f --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,135 @@ +import { Outlet, NavLink, useLocation } from 'react-router-dom' +import { useState } from 'react' + +const navSections = [ + { + title: 'Work', + items: [ + { to: '/', label: 'Dashboard', icon: 'πŸ“Š' }, + { to: '/tickets', label: 'All Tickets', icon: '🎫' }, + { to: '/board', label: 'Board', icon: 'πŸ“‹' }, + ], + }, + { + title: 'Management', + items: [ + { to: '/projects', label: 'Projects', icon: 'πŸ“' }, + { to: '/team', label: 'Team', icon: 'πŸ‘₯' }, + { to: '/reports', label: 'Reports', icon: 'πŸ“ˆ' }, + ], + }, + { + title: 'Configuration', + items: [ + { to: '/integrations', label: 'Integrations', icon: 'πŸ”Œ' }, + { to: '/automation', label: 'Automation', icon: '⚑' }, + { to: '/settings', label: 'Settings', icon: 'βš™οΈ' }, + ], + }, +] + +export default function Layout() { + const [collapsed, setCollapsed] = useState(false) + const location = useLocation() + + return ( +
+ {/* Sidebar */} + + +
+ {/* Top Bar */} +
+
+
+ + + + +
+
+
+ + +
+ RL +
+
+
+ +
+ +
+
+
+ ) +} diff --git a/frontend/src/components/ui/Avatar.tsx b/frontend/src/components/ui/Avatar.tsx new file mode 100644 index 0000000..d606278 --- /dev/null +++ b/frontend/src/components/ui/Avatar.tsx @@ -0,0 +1,6 @@ +export function Avatar({ name, size = 'md', color }: { name: string; size?: 'sm' | 'md' | 'lg'; color?: string }) { + const initials = name.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase() + const sizes = { sm: 'w-8 h-8 text-xs', md: 'w-10 h-10 text-sm', lg: 'w-12 h-12 text-base' } + const bg = color || `bg-blue-600` + return
{initials}
+} diff --git a/frontend/src/components/ui/Badge.tsx b/frontend/src/components/ui/Badge.tsx new file mode 100644 index 0000000..64704a5 --- /dev/null +++ b/frontend/src/components/ui/Badge.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from 'react' + +export function Badge({ children, variant = 'default', size = 'sm' }: { + children: ReactNode; variant?: 'success' | 'warning' | 'error' | 'info' | 'default' | 'purple'; size?: 'sm' | 'md' +}) { + const variants = { + success: 'bg-green-100 text-green-700 border-green-200', + warning: 'bg-yellow-100 text-yellow-700 border-yellow-200', + error: 'bg-red-100 text-red-700 border-red-200', + info: 'bg-blue-100 text-blue-700 border-blue-200', + default: 'bg-gray-100 text-gray-700 border-gray-200', + purple: 'bg-purple-100 text-purple-700 border-purple-200', + } + const sizes = { sm: 'px-2 py-0.5 text-xs', md: 'px-3 py-1 text-sm' } + return {children} +} diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx new file mode 100644 index 0000000..4cdbfa2 --- /dev/null +++ b/frontend/src/components/ui/Button.tsx @@ -0,0 +1,31 @@ +import { ButtonHTMLAttributes, ReactNode } from 'react' + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'success' + size?: 'sm' | 'md' | 'lg' + children: ReactNode + loading?: boolean +} + +export function Button({ variant = 'primary', size = 'md', children, loading, className = '', disabled, ...props }: ButtonProps) { + const variants = { + primary: 'bg-blue-600 hover:bg-blue-700 text-white', + secondary: 'bg-gray-100 hover:bg-gray-200 text-gray-700 border border-gray-300', + danger: 'bg-red-600 hover:bg-red-700 text-white', + ghost: 'bg-transparent hover:bg-gray-100 text-gray-600', + success: 'bg-green-600 hover:bg-green-700 text-white', + } + const sizes = { sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2', lg: 'px-6 py-3 text-lg' } + + return ( + + ) +} diff --git a/frontend/src/components/ui/Card.tsx b/frontend/src/components/ui/Card.tsx new file mode 100644 index 0000000..319203f --- /dev/null +++ b/frontend/src/components/ui/Card.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' + +export function Card({ children, className = '', padding = 'md' }: { + children: ReactNode; className?: string; padding?: 'none' | 'sm' | 'md' | 'lg' +}) { + const p = { none: '', sm: 'p-4', md: 'p-6', lg: 'p-8' } + return
{children}
+} diff --git a/frontend/src/components/ui/Input.tsx b/frontend/src/components/ui/Input.tsx new file mode 100644 index 0000000..a4c46b9 --- /dev/null +++ b/frontend/src/components/ui/Input.tsx @@ -0,0 +1,24 @@ +import { InputHTMLAttributes, forwardRef } from 'react' + +interface InputProps extends InputHTMLAttributes { + label?: string + error?: string + hint?: string +} + +export const Input = forwardRef( + ({ label, error, hint, className = '', ...props }, ref) => ( +
+ {label && } + + {hint && !error &&

{hint}

} + {error &&

{error}

} +
+ ) +) diff --git a/frontend/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx new file mode 100644 index 0000000..777d9a8 --- /dev/null +++ b/frontend/src/components/ui/Modal.tsx @@ -0,0 +1,23 @@ +import { ReactNode, useEffect } from 'react' + +export function Modal({ open, onClose, title, children, size = 'md' }: { + open: boolean; onClose: () => void; title: string; children: ReactNode; size?: 'sm' | 'md' | 'lg' | 'xl' +}) { + useEffect(() => { document.body.style.overflow = open ? 'hidden' : ''; return () => { document.body.style.overflow = '' } }, [open]) + if (!open) return null + const sizes = { sm: 'max-w-md', md: 'max-w-lg', lg: 'max-w-2xl', xl: 'max-w-4xl' } + return ( +
+
+
+
+

{title}

+ +
+
{children}
+
+
+ ) +} diff --git a/frontend/src/components/ui/Select.tsx b/frontend/src/components/ui/Select.tsx new file mode 100644 index 0000000..f05a292 --- /dev/null +++ b/frontend/src/components/ui/Select.tsx @@ -0,0 +1,17 @@ +import { SelectHTMLAttributes, forwardRef } from 'react' + +interface SelectProps extends SelectHTMLAttributes { + label?: string + options: { value: string; label: string }[] +} + +export const Select = forwardRef( + ({ label, options, className = '', ...props }, ref) => ( +
+ {label && } + +
+ ) +) diff --git a/frontend/src/components/ui/Table.tsx b/frontend/src/components/ui/Table.tsx new file mode 100644 index 0000000..3dd7166 --- /dev/null +++ b/frontend/src/components/ui/Table.tsx @@ -0,0 +1,25 @@ +import { ReactNode } from 'react' + +export function Table({ children }: { children: ReactNode }) { + return {children}
+} + +export function TableHeader({ children }: { children: ReactNode }) { + return {children} +} + +export function TableHead({ children, className = '' }: { children: ReactNode; className?: string }) { + return {children} +} + +export function TableBody({ children }: { children: ReactNode }) { + return {children} +} + +export function TableRow({ children, onClick, className = '' }: { children: ReactNode; onClick?: () => void; className?: string }) { + return {children} +} + +export function TableCell({ children, className = '' }: { children: ReactNode; className?: string }) { + return {children} +} diff --git a/frontend/src/components/ui/Tabs.tsx b/frontend/src/components/ui/Tabs.tsx new file mode 100644 index 0000000..1b5fbd9 --- /dev/null +++ b/frontend/src/components/ui/Tabs.tsx @@ -0,0 +1,30 @@ +import { ReactNode, useState, createContext, useContext } from 'react' + +const TabsContext = createContext<{ activeTab: string; setActiveTab: (tab: string) => void } | null>(null) + +export function Tabs({ defaultValue, children }: { defaultValue: string; children: ReactNode }) { + const [activeTab, setActiveTab] = useState(defaultValue) + return {children} +} + +export function TabsList({ children }: { children: ReactNode }) { + return
{children}
+} + +export function TabsTrigger({ value, children }: { value: string; children: ReactNode }) { + const ctx = useContext(TabsContext) + if (!ctx) return null + return ( + + ) +} + +export function TabsContent({ value, children }: { value: string; children: ReactNode }) { + const ctx = useContext(TabsContext) + if (!ctx || ctx.activeTab !== value) return null + return
{children}
+} diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts new file mode 100644 index 0000000..0cc04b5 --- /dev/null +++ b/frontend/src/components/ui/index.ts @@ -0,0 +1,9 @@ +export * from './Card' +export * from './Button' +export * from './Input' +export * from './Select' +export * from './Badge' +export * from './Modal' +export * from './Tabs' +export * from './Avatar' +export * from './Table' diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..90b5364 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,4 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +body { font-family: 'Inter', system-ui, sans-serif; } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..cb66cea --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import './index.css' + +const queryClient = new QueryClient() + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + , +) diff --git a/frontend/src/pages/Automation.tsx b/frontend/src/pages/Automation.tsx new file mode 100644 index 0000000..97f7c74 --- /dev/null +++ b/frontend/src/pages/Automation.tsx @@ -0,0 +1,163 @@ +import { useState } from 'react' +import { Card, Button, Input, Select, Modal, Badge } from '../components/ui' + +interface Rule { + id: string + name: string + enabled: boolean + trigger: string + actions: string[] + runs: number +} + +const mockRules: Rule[] = [ + { id: '1', name: 'Auto-assign critical tickets', enabled: true, trigger: 'When priority is Critical', actions: ['Assign to On-Call', 'Send Slack notification'], runs: 23 }, + { id: '2', name: 'Close stale tickets', enabled: true, trigger: 'When ticket has no activity for 14 days', actions: ['Add comment', 'Close ticket'], runs: 45 }, + { id: '3', name: 'Escalate high priority', enabled: false, trigger: 'When high priority ticket is open for 24h', actions: ['Send email to manager'], runs: 12 }, +] + +export default function Automation() { + const [rules, setRules] = useState(mockRules) + const [showCreate, setShowCreate] = useState(false) + + const toggleRule = (id: string) => { + setRules(rules.map(r => r.id === id ? { ...r, enabled: !r.enabled } : r)) + } + + return ( +
+
+
+

Automation

+

Automate repetitive tasks with rules

+
+ +
+ + {/* Stats */} +
+ +
+
{rules.length}
+
Total Rules
+
+
+ +
+
{rules.filter(r => r.enabled).length}
+
Active Rules
+
+
+ +
+
{rules.reduce((a, r) => a + r.runs, 0)}
+
Total Runs
+
+
+
+ + {/* Rules List */} +
+ {rules.map(rule => ( + +
+
+ +
+
+

{rule.name}

+ {rule.enabled ? 'Active' : 'Disabled'} +
+

{rule.trigger}

+
+ {rule.actions.map((action, i) => ( + {action} + ))} +
+
+
+
+
+
{rule.runs}
+
runs
+
+ +
+
+
+ ))} +
+ + {/* Create Modal */} + setShowCreate(false)} title="Create Automation Rule" size="lg"> +
+ + +
+ + + +
+
+ +
+ +
+ +
+ ({ value: String(p.id), label: p.name })) || [])]} + value={projectId} + onChange={e => setProjectId(e.target.value)} + className="w-48" + /> +
+ +
+ {columns.map(col => ( +
+
+
+

{col.label}

+ ({getTicketsByStatus(col.id).length}) +
+ +
e.preventDefault()} + onDrop={e => { + const ticketId = e.dataTransfer.getData('ticketId') + if (ticketId) updateMutation.mutate({ id: Number(ticketId), status: col.id }) + }} + > + {getTicketsByStatus(col.id).map(ticket => ( + e.dataTransfer.setData('ticketId', String(ticket.id))} + className={`block bg-white rounded-lg p-3 shadow-sm border-l-4 ${priorityColors[ticket.priority]} hover:shadow-md transition-shadow cursor-grab active:cursor-grabbing`} + > +
+ {ticket.key} +
+

{ticket.title}

+
+ + {ticket.priority} + + {ticket.assignee && } +
+ + ))} + {getTicketsByStatus(col.id).length === 0 && ( +
No tickets
+ )} +
+
+ ))} +
+
+ ) +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..4409dc9 --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,143 @@ +import { useQuery } from '@tanstack/react-query' +import { Link } from 'react-router-dom' +import { Card } from '../components/ui' +import { projectsApi, ticketsApi, Ticket } from '../services/api' + +export default function Dashboard() { + const { data: projects } = useQuery({ queryKey: ['projects'], queryFn: projectsApi.list }) + const { data: tickets } = useQuery({ queryKey: ['tickets'], queryFn: () => ticketsApi.list() }) + + const stats = { + total: tickets?.length || 0, + open: tickets?.filter(t => t.status === 'open').length || 0, + inProgress: tickets?.filter(t => t.status === 'in_progress').length || 0, + resolved: tickets?.filter(t => t.status === 'resolved' || t.status === 'closed').length || 0, + critical: tickets?.filter(t => t.priority === 'critical').length || 0, + } + + const recentTickets = tickets?.slice(0, 5) || [] + + return ( +
+
+
+

Dashboard

+

Overview of your workspace

+
+ + + +
+ + {/* Stats */} +
+ + + + + +
+ +
+ {/* Recent Tickets */} +
+ +
+

Recent Tickets

+ View all β†’ +
+
+ {recentTickets.length === 0 ? ( +
No tickets yet
+ ) : ( + recentTickets.map(ticket => ) + )} +
+
+
+ + {/* Projects */} +
+ +
+

Projects

+ Manage β†’ +
+
+ {projects?.map(project => ( + +
+
+

{project.name}

+

{project.key}

+
+ {project.ticket_count} tickets +
+ + ))} + {!projects?.length &&
No projects yet
} +
+
+
+
+
+ ) +} + +function StatCard({ label, value, color }: { label: string; value: number; color: string }) { + const colors: Record = { + gray: 'bg-gray-100 text-gray-600', + blue: 'bg-blue-100 text-blue-600', + yellow: 'bg-yellow-100 text-yellow-600', + green: 'bg-green-100 text-green-600', + red: 'bg-red-100 text-red-600', + } + return ( + +
+
+

{label}

+

{value}

+
+
+ 🎫 +
+
+
+ ) +} + +function TicketRow({ ticket }: { ticket: Ticket }) { + const statusColors: Record = { + open: 'bg-blue-100 text-blue-700', + in_progress: 'bg-yellow-100 text-yellow-700', + resolved: 'bg-green-100 text-green-700', + closed: 'bg-gray-100 text-gray-700', + } + const priorityIcons: Record = { + low: '🟒', medium: '🟑', high: '🟠', critical: 'πŸ”΄' + } + return ( + +
+
+ {priorityIcons[ticket.priority]} +
+
+ {ticket.key} + + {ticket.status.replace('_', ' ')} + +
+

{ticket.title}

+
+
+
+ + ) +} diff --git a/frontend/src/pages/Integrations.tsx b/frontend/src/pages/Integrations.tsx new file mode 100644 index 0000000..9bcbce0 --- /dev/null +++ b/frontend/src/pages/Integrations.tsx @@ -0,0 +1,128 @@ +import { useState } from 'react' +import { Card, Button, Input, Modal, Badge, Tabs, TabsList, TabsTrigger, TabsContent } from '../components/ui' + +const integrations = { + connected: [ + { id: '1', name: 'JIRA AI Fixer', type: 'ai', icon: 'πŸ€–', status: 'active', url: 'https://jira-fixer.startdata.com.br' }, + ], + available: [ + { id: 'github', name: 'GitHub', type: 'repo', icon: 'πŸ™', description: 'Link tickets to commits and PRs' }, + { id: 'gitlab', name: 'GitLab', type: 'repo', icon: '🦊', description: 'Connect GitLab issues and MRs' }, + { id: 'slack', name: 'Slack', type: 'notify', icon: 'πŸ’¬', description: 'Get notifications in Slack' }, + { id: 'teams', name: 'Microsoft Teams', type: 'notify', icon: 'πŸ‘₯', description: 'Teams notifications' }, + { id: 'jira', name: 'Jira', type: 'sync', icon: 'πŸ”΅', description: 'Sync with Jira issues' }, + { id: 'servicenow', name: 'ServiceNow', type: 'sync', icon: '🟒', description: 'Sync incidents' }, + { id: 'email', name: 'Email', type: 'notify', icon: 'πŸ“§', description: 'Email notifications' }, + { id: 'zapier', name: 'Zapier', type: 'automation', icon: '⚑', description: 'Connect 5000+ apps' }, + ], +} + +export default function Integrations() { + const [showConnect, setShowConnect] = useState(null) + + return ( +
+
+
+

Integrations

+

Connect TicketHub with your tools

+
+
+ + + + Connected ({integrations.connected.length}) + Available ({integrations.available.length}) + + + + {integrations.connected.length === 0 ? ( + + πŸ”Œ +

No integrations yet

+

Connect your first integration to get started

+
+ ) : ( +
+ {integrations.connected.map(int => ( + +
+
+
+ {int.icon} +
+
+
+

{int.name}

+ Connected +
+

{int.url}

+
+
+
+ + +
+
+
+ ))} +
+ )} +
+ + +
+ {integrations.available.map(int => ( + +
+
+ {int.icon} +
+
+

{int.name}

+

{int.description}

+ +
+
+
+ ))} +
+
+
+ + {/* Webhook Section */} +
+

Webhooks

+ +

+ Send ticket events to external services via webhooks. Configure webhook URLs per project in Project Settings. +

+
+
+ POST + /api/webhooks/incoming + - Receive events from external services +
+
+

+ Events sent: ticket.created, ticket.updated, ticket.resolved, comment.added +

+
+
+ + {/* Connect Modal */} + setShowConnect(null)} title={`Connect ${integrations.available.find(i => i.id === showConnect)?.name || ''}`}> +
+ + +
+ + +
+
+
+
+ ) +} diff --git a/frontend/src/pages/NewTicket.tsx b/frontend/src/pages/NewTicket.tsx new file mode 100644 index 0000000..f4d5873 --- /dev/null +++ b/frontend/src/pages/NewTicket.tsx @@ -0,0 +1,80 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useQuery, useMutation } from '@tanstack/react-query' +import { Card, Button, Input, Select } from '../components/ui' +import { projectsApi, ticketsApi } from '../services/api' + +export default function NewTicket() { + const navigate = useNavigate() + const { data: projects } = useQuery({ queryKey: ['projects'], queryFn: projectsApi.list }) + + const [form, setForm] = useState({ + project_id: '', + title: '', + description: '', + priority: 'medium', + }) + + const createMutation = useMutation({ + mutationFn: ticketsApi.create, + onSuccess: (ticket) => navigate(`/tickets/${ticket.id}`), + }) + + return ( +
+
+

Create Ticket

+

Submit a new ticket for tracking

+
+ + +
{ e.preventDefault(); createMutation.mutate({ ...form, project_id: Number(form.project_id) }) }} className="space-y-6"> + setForm({ ...form, title: e.target.value })} + required + /> + +
+ +