Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added apps/__init__.py
Empty file.
Empty file added apps/api/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion apps/web/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"extends": "next/core-web-vitals"
"extends": ["next/core-web-vitals", "prettier"]
}
6 changes: 6 additions & 0 deletions apps/web/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}
11 changes: 11 additions & 0 deletions apps/web/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function AuthLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex min-h-screen w-full items-center justify-center">
{children}
</div>
)
}
25 changes: 25 additions & 0 deletions apps/web/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { LoginForm } from '@/components/auth/LoginForm';

export const metadata = {
title: 'Login - TicketFlow',
description: 'Sign in to your account',
};

export default function LoginPage() {
return (
<div className="flex min-h-screen flex-col items-center justify-center p-4">
<div className="w-full max-w-sm flex flex-col items-center space-y-6">
<div className="text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Welcome back
</h1>
<p className="text-sm text-gray-500 mt-2">
Enter your credentials to access your account
</p>
</div>

<LoginForm />
</div>
</div>
);
}
48 changes: 48 additions & 0 deletions apps/web/app/(dashboard)/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use client';

import { useEffect } from 'react';

export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log the error to an error reporting service safely (no sensitive backend details are exposed to the DOM)
console.error('Dashboard component boundary caught error:', error);
}, [error]);

return (
<div className="flex flex-col items-center justify-center min-h-[50vh] p-8 text-center">
<div className="rounded-full bg-red-100 p-4 mb-4">
<svg
className="h-8 w-8 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Unable to load dashboard
</h2>
<p className="text-gray-500 mb-6 max-w-md">
We encountered a problem while attempting to load your dashboard data. Please try again later.
</p>
<button
onClick={() => reset()}
className="px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
Try again
</button>
</div>
);
}
18 changes: 18 additions & 0 deletions apps/web/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { AuthGuard } from '@/components/auth/AuthGuard';

export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<AuthGuard>
<div className="flex min-h-screen w-full flex-col">
{/* Skeleton for future Sidebar/Header */}
<main className="flex-1 overflow-auto">
{children}
</main>
</div>
</AuthGuard>
)
}
44 changes: 44 additions & 0 deletions apps/web/app/(dashboard)/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export default function DashboardLoading() {
return (
<div className="p-8 max-w-7xl mx-auto w-full">
<header className="mb-8 animate-pulse">
<div className="h-8 w-64 bg-gray-200 rounded mb-2"></div>
<div className="h-4 w-96 bg-gray-200 rounded"></div>
</header>

<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-8">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="rounded-lg border bg-white p-6 shadow-sm animate-pulse">
<div className="h-4 w-24 bg-gray-200 rounded mb-4"></div>
<div className="h-8 w-16 bg-gray-200 rounded mb-2"></div>
<div className="h-3 w-32 bg-gray-200 rounded"></div>
</div>
))}
</div>

<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div className="rounded-lg border bg-white p-6 shadow-sm h-64 animate-pulse"></div>
<div className="rounded-lg border p-6 shadow-sm h-64 animate-pulse bg-gray-50"></div>
</div>

<div className="rounded-lg border bg-white shadow-sm overflow-hidden animate-pulse">
<div className="p-6 border-b border-gray-100">
<div className="h-5 w-32 bg-gray-200 rounded"></div>
</div>
<div className="divide-y divide-gray-100">
{[1, 2, 3].map((i) => (
<div key={i} className="p-6 flex items-center justify-between">
<div className="flex gap-4 items-center">
<div className="h-10 w-10 bg-gray-200 rounded-full"></div>
<div className="space-y-2">
<div className="h-4 w-48 bg-gray-200 rounded"></div>
<div className="h-3 w-24 bg-gray-200 rounded"></div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
35 changes: 35 additions & 0 deletions apps/web/app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { dashboardService } from '@/services/dashboard.service';
import {
DashboardHeaderWrapper,
SummaryCards,
DashboardCharts,
RecentActivity,
} from '@/components/dashboard';

export const metadata = {
title: 'Dashboard - TicketFlow',
description: 'TicketFlow Dashboard Overview',
};

export default async function DashboardPage() {
// Fetch initial dashboard data securely on the server
// This interacts securely through our dashboard.service.ts
const summary = await dashboardService.getSummary();

// For presentation logic in charts, we'll map the metrics and activity appropriately
// or just pass them down if they match the expected shape.
// The service already returned the expected types.

return (
<div className="p-8 max-w-7xl mx-auto w-full">
<DashboardHeaderWrapper />
<SummaryCards metrics={summary.metrics} />
{/*
DashboardCharts currently uses fallback data internally for presentation,
but we can pass transformed data here in the future.
*/}
<DashboardCharts />
<RecentActivity activities={summary.recentActivity} />
</div>
);
}
110 changes: 110 additions & 0 deletions apps/web/app/(dashboard)/tickets/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { ticketService } from '@/services/ticket.service';
import { TicketStatusBadge, TicketPriorityBadge, TicketAssignment, TicketWorkflow } from '@/components/tickets';
import { cookies } from 'next/headers';
import Link from 'next/link';

export const metadata = {
title: 'Ticket Details - TicketFlow',
};

export default async function TicketDetailPage({
params,
}: {
params: { id: string }
}) {
const cookieStore = await cookies();
const allCookies = cookieStore.getAll().map((c: { name: string; value: string }) => `${c.name}=${c.value}`).join('; ');
const headers = { Cookie: allCookies };

const [ticket, history] = await Promise.all([
ticketService.getTicket(params.id, headers),
ticketService.getAssignmentHistory(params.id, headers).catch(() => []),
]);

return (
<div className="p-8 max-w-5xl mx-auto w-full">
<div className="mb-6">
<Link href="/tickets" className="text-sm text-blue-600 hover:text-blue-900 flex items-center">
&larr; Back to tickets
</Link>
</div>

<div className="bg-white shadow-sm ring-1 ring-black/5 rounded-lg overflow-hidden">
<div className="p-6 sm:p-8 border-b border-gray-200">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900">{ticket.title}</h1>
<p className="text-sm text-gray-500 mt-1">Ticket ID: {ticket.id}</p>
</div>
<div className="flex items-center gap-2">
<TicketStatusBadge status={ticket.status} />
<TicketPriorityBadge priority={ticket.priority} />
</div>
</div>
</div>

<div className="p-6 sm:p-8">
<h3 className="text-lg font-medium text-gray-900 mb-2">Description</h3>
<div className="prose max-w-none text-gray-700">
{ticket.description || 'No description provided.'}
</div>

<div className="mt-8 grid grid-cols-1 sm:grid-cols-2 gap-6">
<div>
<h4 className="text-sm font-medium text-gray-500">Category</h4>
<p className="mt-1 text-sm text-gray-900">{ticket.category || 'None'}</p>
</div>
<div>
<h4 className="text-sm font-medium text-gray-500">Created At</h4>
<p className="mt-1 text-sm text-gray-900">{new Date(ticket.created_at).toLocaleString()}</p>
</div>
</div>
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<TicketAssignment ticketId={ticket.id} currentAssignee={ticket.assigned_to} />
<TicketWorkflow ticketId={ticket.id} currentStatus={ticket.status} />
</div>

{history.length > 0 && (
<div className="bg-white p-6 sm:p-8 rounded-lg shadow-sm ring-1 ring-black/5 mt-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Assignment History</h3>
<div className="flow-root">
<ul role="list" className="-mb-8">
{history.map((event, eventIdx) => (
<li key={event.id}>
<div className="relative pb-8">
{eventIdx !== history.length - 1 ? (
<span className="absolute left-4 top-4 -ml-px h-full w-0.5 bg-gray-200" aria-hidden="true" />
) : null}
<div className="relative flex space-x-3">
<div>
<span className="h-8 w-8 rounded-full bg-gray-100 flex items-center justify-center ring-8 ring-white">
<svg className="h-5 w-5 text-gray-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fillRule="evenodd" d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 10a1 1 0 112 0v2a1 1 0 11-2 0v-2zm1-4a1 1 0 100 2 1 1 0 000-2z" clipRule="evenodd" />
</svg>
</span>
</div>
<div className="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div>
<p className="text-sm text-gray-500">
{event.action_type.replace('_', ' ')}
{event.assigned_to && <span className="font-medium text-gray-900"> to {event.assigned_to}</span>}
</p>
</div>
<div className="whitespace-nowrap text-right text-sm text-gray-500">
<time dateTime={event.created_at}>{new Date(event.created_at).toLocaleString()}</time>
</div>
</div>
</div>
</div>
</li>
))}
</ul>
</div>
</div>
)}
</div>
);
}
28 changes: 28 additions & 0 deletions apps/web/app/(dashboard)/tickets/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

import { useEffect } from 'react';

export default function TicketsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);

return (
<div className="p-8 max-w-7xl mx-auto w-full text-center py-20">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Something went wrong!</h2>
<p className="text-gray-500 mb-8">{error.message || 'Failed to load tickets.'}</p>
<button
onClick={() => reset()}
className="rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Try again
</button>
</div>
);
}
11 changes: 11 additions & 0 deletions apps/web/app/(dashboard)/tickets/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function TicketsLoading() {
return (
<div className="p-8 max-w-7xl mx-auto w-full animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2 mb-8"></div>

<div className="h-16 bg-gray-200 rounded w-full mb-6"></div>
<div className="h-64 bg-gray-200 rounded w-full"></div>
</div>
);
}
Loading
Loading