feat: web sprint 250-262 — plans/clients/tickets/audit/profile/settings/leads/reports #26

Merged
kibin merged 11 commits from feat/web-sprint-250-262 into main 2026-04-01 08:09:45 +00:00
Showing only changes of commit 0d35d18b36 - Show all commits

View File

@@ -2,7 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { RefreshCw, Ticket, Plus, Send, RotateCcw, User } from "lucide-react"; import { RefreshCw, Ticket, Plus, Send, RotateCcw, User, MapPin, Zap } from "lucide-react";
import { useAuth } from "@/contexts/AuthContext"; import { useAuth } from "@/contexts/AuthContext";
import { Card, CardContent, CardHeader } from "@/components/ui/Card"; import { Card, CardContent, CardHeader } from "@/components/ui/Card";
import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table"; import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table";
@@ -120,6 +120,14 @@ export default function TicketsPage() {
onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to create ticket"), onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to create ticket"),
}); });
const activateClient = useMutation({
mutationFn: async (clientId: string) => {
await api.patch(`/api/v1/clients/${clientId}`, { isActive: true });
},
onSuccess: () => { toast.success("Client activated! 🎉"); refetch(); refetchDetail(); },
onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to activate client"),
});
const revertStatus = useMutation({ const revertStatus = useMutation({
mutationFn: async ({ id, status }: { id: string; status: string }) => { mutationFn: async ({ id, status }: { id: string; status: string }) => {
await api.patch(`/api/v1/tickets/${id}`, { status }); await api.patch(`/api/v1/tickets/${id}`, { status });
@@ -226,6 +234,21 @@ export default function TicketsPage() {
<div><span className="text-gray-500">Status</span><p><Badge variant={statusVariant[detail.status] ?? "muted"}>{detail.status}</Badge></p></div> <div><span className="text-gray-500">Status</span><p><Badge variant={statusVariant[detail.status] ?? "muted"}>{detail.status}</Badge></p></div>
<div className="col-span-2"><span className="text-gray-500">Created</span><p>{formatDate(detail.createdAt)}</p></div> <div className="col-span-2"><span className="text-gray-500">Created</span><p>{formatDate(detail.createdAt)}</p></div>
{detail.description && <div className="col-span-2"><span className="text-gray-500">Description</span><p className="mt-1 text-gray-800 whitespace-pre-wrap">{detail.description}</p></div>} {detail.description && <div className="col-span-2"><span className="text-gray-500">Description</span><p className="mt-1 text-gray-800 whitespace-pre-wrap">{detail.description}</p></div>}
{/* Installation: show Google Maps link using client address */}
{detail.type === "INSTALLATION" && detail.client && (
<div className="col-span-2">
<span className="text-gray-500">Map</span>
<p className="mt-0.5">
<a
href={`https://www.google.com/maps/search/${encodeURIComponent([(ticketDetail as any)?.client?.address, detail.client.firstName + " " + detail.client.lastName].filter(Boolean).join(", "))}`}
target="_blank" rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-blue-600 hover:underline text-sm"
>
<MapPin size={13} /> View on Google Maps
</a>
</p>
</div>
)}
</div> </div>
{/* Status actions */} {/* Status actions */}
@@ -240,6 +263,13 @@ export default function TicketsPage() {
<RotateCcw size={13} className="mr-1" /> Revert to {prevStatus[detail.status].replace("_", " ")} <RotateCcw size={13} className="mr-1" /> Revert to {prevStatus[detail.status].replace("_", " ")}
</Button> </Button>
)} )}
{/* Installation ticket RESOLVED → show Activate Client button */}
{detail.type === "INSTALLATION" && detail.status === "RESOLVED" && detail.clientId && (
<Button size="sm" variant="outline" onClick={() => activateClient.mutate(detail.clientId!)} isLoading={activateClient.isPending}
className="text-green-700 border-green-300 hover:bg-green-50">
<Zap size={13} className="mr-1" /> Activate Client
</Button>
)}
</div> </div>
{/* Comments */} {/* Comments */}