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
35 changes: 35 additions & 0 deletions apps/web/components/ui/sonner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client"

import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"

type ToasterProps = React.ComponentProps<typeof Sonner>

const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()

return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group z-100 bottom-20 sm:bottom-4"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-card group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
error: "group-[.toaster]:bg-destructive group-[.toaster]:text-destructive-foreground group-[.toaster]:border-destructive",
success: "group-[.toaster]:bg-green-600 group-[.toaster]:text-white group-[.toaster]:border-green-600",
},
}}
visibleToasts={3}
position="bottom-right"
{...props}
/>
)
}

export { Toaster }
23 changes: 10 additions & 13 deletions apps/web/features/tickets/components/TicketAssignment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { ticketService } from '../api/tickets.api';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { toast } from '@/lib/toast';
import { MESSAGES } from '@/lib/toast/messages';

interface Props {
ticketId: string;
Expand All @@ -16,31 +18,32 @@ export function TicketAssignment({ ticketId, currentAssignee }: Props) {
const router = useRouter();
const [isAssigning, setIsAssigning] = useState(false);
const [technicianId, setTechnicianId] = useState('');
const [error, setError] = useState<string | null>(null);

const handleAssign = async () => {
if (!technicianId) return;
setIsAssigning(true);
setError(null);
try {
await ticketService.assignTicket(ticketId, technicianId);
const apiCall = ticketService.assignTicket(ticketId, technicianId);
toast.promise(apiCall, MESSAGES.TICKETS.ASSIGN);
await apiCall;
setTechnicianId('');
router.refresh();
} catch (err: any) {
setError(err.message || 'Failed to assign ticket');
// Error feedback is handled by toast
} finally {
setIsAssigning(false);
}
};

const handleUnassign = async () => {
setIsAssigning(true);
setError(null);
try {
await ticketService.unassignTicket(ticketId);
const apiCall = ticketService.unassignTicket(ticketId);
toast.promise(apiCall, MESSAGES.TICKETS.UNASSIGN);
await apiCall;
router.refresh();
} catch (err: any) {
setError(err.message || 'Failed to unassign ticket');
// Error feedback is handled by toast
} finally {
setIsAssigning(false);
}
Expand All @@ -52,12 +55,6 @@ export function TicketAssignment({ ticketId, currentAssignee }: Props) {
<CardTitle className="text-sm font-medium">Assignment</CardTitle>
</CardHeader>
<CardContent>
{error && (
<div className="mb-4 text-sm text-destructive bg-destructive/10 p-2 rounded-md">
{error}
</div>
)}

{currentAssignee ? (
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Expand Down
16 changes: 6 additions & 10 deletions apps/web/features/tickets/components/TicketWorkflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { ticketService } from '../api/tickets.api';
import { TicketStatus } from '../types';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { toast } from '@/lib/toast';
import { MESSAGES } from '@/lib/toast/messages';

interface Props {
ticketId: string;
Expand All @@ -15,7 +17,6 @@ interface Props {
export function TicketWorkflow({ ticketId, currentStatus }: Props) {
const router = useRouter();
const [isUpdating, setIsUpdating] = useState(false);
const [error, setError] = useState<string | null>(null);

const getAvailableTransitions = () => {
// This replicates the backend ALLOWED_TRANSITIONS logic temporarily for UI purposes
Expand All @@ -33,12 +34,13 @@ export function TicketWorkflow({ ticketId, currentStatus }: Props) {

const handleStatusChange = async (newStatus: TicketStatus) => {
setIsUpdating(true);
setError(null);
try {
await ticketService.updateTicketStatus(ticketId, newStatus);
const apiCall = ticketService.updateTicketStatus(ticketId, newStatus);
toast.promise(apiCall, MESSAGES.TICKETS.STATUS_UPDATE);
await apiCall;
router.refresh();
} catch (err: any) {
setError(err.message || 'Failed to update status');
// Error feedback is handled by toast
} finally {
setIsUpdating(false);
}
Expand All @@ -54,12 +56,6 @@ export function TicketWorkflow({ ticketId, currentStatus }: Props) {
<CardTitle className="text-sm font-medium">Workflow Actions</CardTitle>
</CardHeader>
<CardContent>
{error && (
<div className="mb-4 text-sm text-destructive bg-destructive/10 p-2 rounded-md">
{error}
</div>
)}

<div className="flex flex-wrap gap-2">
{availableTransitions.map((status) => (
<Button
Expand Down
30 changes: 14 additions & 16 deletions apps/web/features/users/components/UserActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { User } from '../types';
import { UserService } from '../api/users.api';
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { toast } from '@/lib/toast';
import { MESSAGES } from '@/lib/toast/messages';

interface UserActionsProps {
user: User;
Expand All @@ -14,24 +16,23 @@ interface UserActionsProps {
export function UserActions({ user }: UserActionsProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleToggleStatus = async () => {
if (!confirm(`Are you sure you want to ${user.is_active ? 'deactivate' : 'activate'} this user?`)) {
return;
}

setLoading(true);
setError(null);
try {
if (user.is_active) {
await UserService.deactivateUser(user.id);
} else {
await UserService.activateUser(user.id);
}
const apiCall = user.is_active
? UserService.deactivateUser(user.id)
: UserService.activateUser(user.id);

toast.promise(apiCall, user.is_active ? MESSAGES.USERS.DEACTIVATE : MESSAGES.USERS.ACTIVATE);
await apiCall;
router.refresh();
} catch (err: any) {
setError(err.message || 'Action failed');
// Error feedback is handled by toast.promise
} finally {
setLoading(false);
}
Expand All @@ -43,13 +44,15 @@ export function UserActions({ user }: UserActionsProps) {
}

setLoading(true);
setError(null);
try {
await UserService.deleteUser(user.id);
const apiCall = UserService.deleteUser(user.id);
toast.promise(apiCall, MESSAGES.USERS.DELETE);
await apiCall;
router.push('/users');
router.refresh();
} catch (err: any) {
setError(err.message || 'Failed to delete user');
// Error feedback is handled by toast.promise
} finally {
setLoading(false);
}
};
Expand All @@ -61,11 +64,6 @@ export function UserActions({ user }: UserActionsProps) {
</CardHeader>

<CardContent>
{error && (
<div className="p-3 mb-4 bg-destructive/10 text-destructive rounded-md text-sm">
{error}
</div>
)}

<div className="flex flex-wrap gap-3">
<Button
Expand Down
37 changes: 19 additions & 18 deletions apps/web/features/users/components/UserForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { User, UserRole, CreateUserPayload, UpdateUserPayload } from '../types';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { toast } from '@/lib/toast';
import { MESSAGES } from '@/lib/toast/messages';

interface UserFormProps {
user?: User; // If provided, we are editing. If not, creating.
Expand All @@ -15,7 +17,6 @@ interface UserFormProps {
export function UserForm({ user }: UserFormProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const [formData, setFormData] = useState({
full_name: user?.full_name || '',
Expand All @@ -36,9 +37,9 @@ export function UserForm({ user }: UserFormProps) {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);

try {
let apiCall: Promise<any>;
if (user) {
const updatePayload: UpdateUserPayload = {
full_name: formData.full_name,
Expand All @@ -49,25 +50,30 @@ export function UserForm({ user }: UserFormProps) {
if (formData.password) {
updatePayload.password = formData.password;
}
await UserService.updateUser(user.id, updatePayload);
apiCall = UserService.updateUser(user.id, updatePayload);
} else {
if (!formData.password) {
throw new Error('Password is required for new users');
apiCall = Promise.reject(new Error('Password is required for new users'));
} else {
const createPayload: CreateUserPayload = {
full_name: formData.full_name,
email: formData.email,
password: formData.password,
role: formData.role as UserRole,
is_active: formData.is_active,
};
apiCall = UserService.createUser(createPayload);
}
const createPayload: CreateUserPayload = {
full_name: formData.full_name,
email: formData.email,
password: formData.password,
role: formData.role as UserRole,
is_active: formData.is_active,
};
await UserService.createUser(createPayload);
}

toast.promise(apiCall, user ? MESSAGES.USERS.UPDATE : MESSAGES.USERS.CREATE);
await apiCall;

router.push('/users');
router.refresh();
} catch (err: any) {
setError(err.message || 'An error occurred');
// Error feedback is handled by toast.promise
} finally {
setLoading(false);
}
};
Expand All @@ -76,11 +82,6 @@ export function UserForm({ user }: UserFormProps) {
<Card className="max-w-2xl border-border">
<CardContent className="p-6">
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="p-3 bg-destructive/10 text-destructive rounded-md text-sm">
{error}
</div>
)}

<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
Expand Down
11 changes: 11 additions & 0 deletions apps/web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"react": "latest",
"react-dom": "latest",
"react-hook-form": "latest",
"sonner": "^2.0.7",
"tailwind-merge": "latest",
"zod": "latest",
"zustand": "latest"
Expand Down
2 changes: 2 additions & 0 deletions apps/web/providers/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from 'react'
import { ThemeProvider } from './ThemeProvider'
import { QueryProvider } from './QueryProvider'
import { AuthProvider } from './AuthProvider'
import { Toaster } from '@/components/ui/sonner'

export function Providers({ children }: { children: React.ReactNode }) {
return (
Expand All @@ -18,6 +19,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
{children}
</AuthProvider>
</QueryProvider>
<Toaster />
</ThemeProvider>
)
}
Loading