chore: remove orphaned legacy dashboard routes; fix NotificationBell's broken API calls
All checks were successful
build-and-release / build (push) Successful in 1m28s

An entire route/component cluster was built on a legacy sibling of
DashboardShell (DashboardLayout.tsx) and called APIs via the bare
api.get/post/patch/delete helper, which never prefixes /api/ — so
every call 404s against the real ingress (which only routes /api/*
to the backend). Confirmed orphaned: nothing in the live dashboard
shell (DashboardShell.tsx / dashboard.tsx) links to any of it; the
only cross-references are within the cluster itself. Some of it also
targeted the apps/leads backend service removed in the companion
backend commit.

Removed:
- src/routes/dashboard/wallet/ (buy.tsx, payu-return.tsx, invoices/*)
- src/routes/dashboard/requests.tsx
- src/routes/dashboard/leads/accepted/*
- src/routes/dashboard/marketplace/*
- src/components/dashboard/AcceptedLeadsView.tsx
- src/components/DashboardLayout.tsx (only consumer was the above)
- the unused `api` object in src/lib/api.ts (the unprefixed-path
  footgun itself — `request()`, which it wrapped, stays; it's used
  correctly elsewhere with explicit /api/ paths)

Fixed rather than deleted: src/components/NotificationBell.tsx uses
the same broken convention but IS live (rendered on every dashboard
page via DashboardShell). Switched it to apiFetch with correct
/api/me/notifications/* paths, matching the routes that actually exist
in apps/users/src/handlers/notifications.rs.

`tsc --noEmit` shows no errors under src/ after these changes (pre-
existing node_modules/type-declaration noise unrelated to this change
remains, as it did before).
This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-21 04:25:36 +05:30
parent 142e185fc6
commit 18a9161e4f
13 changed files with 7 additions and 1495 deletions

View file

@ -1,134 +0,0 @@
import { type ParentProps, createMemo, createSignal, onMount } from "solid-js";
import { useLocation, useNavigate } from "@solidjs/router";
import DashboardShell from "~/components/DashboardShell";
const SIDEBAR_ITEMS = [
"My Dashboard",
"Leads",
"My Requests",
"Credits",
"Settings",
"Logout",
];
const ROUTE_BY_LABEL: Record<string, string> = {
"my dashboard": "/dashboard",
leads: "/dashboard/leads/accepted",
"my requests": "/dashboard/requests",
credits: "/dashboard?nav=credits",
settings: "/dashboard?nav=settings",
logout: "/dashboard?nav=logout",
};
function readUserName() {
if (typeof window === "undefined") return "User";
try {
const raw =
sessionStorage.getItem("nxtgauge_auth_user") ||
sessionStorage.getItem("nxtgauge_user") ||
localStorage.getItem("nxtgauge_auth_user") ||
localStorage.getItem("nxtgauge_user");
if (!raw) return "User";
const parsed = JSON.parse(raw);
return parsed?.full_name || parsed?.name || parsed?.email || "User";
} catch {
return "User";
}
}
export default function DashboardLayout(props: ParentProps) {
const location = useLocation();
const navigate = useNavigate();
const [roleKey, setRoleKey] = createSignal("DEVELOPER");
const [userName, setUserName] = createSignal("User");
const activeSidebar = createMemo(() => {
const path = location.pathname || "";
if (path.startsWith("/dashboard/requests")) return "My Requests";
if (path.startsWith("/dashboard/leads")) return "Leads";
if (path.startsWith("/dashboard/credits")) return "Credits";
if (path.startsWith("/dashboard/settings")) return "Settings";
return "My Dashboard";
});
const handleSidebarSelect = (item: string) => {
const target = ROUTE_BY_LABEL[item.toLowerCase()];
if (target) navigate(target);
};
onMount(async () => {
if (typeof window === "undefined") return;
const fromUrl = new URLSearchParams(window.location.search).get("role");
if (fromUrl && fromUrl.trim()) {
setRoleKey(fromUrl.trim().toUpperCase());
return;
}
const storageKeys: [string, Storage][] = [
["nxtgauge_signup_profile_v1", localStorage],
["nxtgauge_auth_user", localStorage],
["nxtgauge_user", localStorage],
["nxtgauge_signup_profile_v1", sessionStorage],
["nxtgauge_auth_user", sessionStorage],
["nxtgauge_user", sessionStorage],
];
for (const [key, storage] of storageKeys) {
try {
const raw = storage.getItem(key);
if (raw) {
const parsed = JSON.parse(raw);
const candidate = String(
parsed?.selectedProfessionalRole || parsed?.active_role || parsed?.roleKey || parsed?.role || ""
)
.trim()
.toUpperCase();
if (candidate && candidate !== "PROFESSIONAL") {
setRoleKey(candidate);
if (parsed?.full_name || parsed?.name || parsed?.email) {
setUserName(parsed.full_name || parsed.name || parsed.email || "User");
}
return;
}
}
} catch {
// continue
}
}
const token = sessionStorage.getItem("nxtgauge_access_token");
if (token) {
try {
const res = await fetch("/api/auth/session", {
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
},
credentials: "include",
});
if (res.ok) {
const data = await res.json();
const role = String(data?.active_role || data?.role || "").trim().toUpperCase();
setRoleKey(role && role !== "PROFESSIONAL" ? role : "DEVELOPER");
setUserName(data?.full_name || data?.name || data?.email || "User");
return;
}
} catch {
// fall through
}
}
});
return (
<DashboardShell
sidebarItems={SIDEBAR_ITEMS}
activeSidebar={activeSidebar()}
onSidebarSelect={handleSidebarSelect}
roleKey={roleKey()}
userName={userName()}
>
{props.children}
</DashboardShell>
);
}

View file

@ -1,5 +1,5 @@
import { createSignal, createEffect, onCleanup, Show } from "solid-js";
import { api } from "~/lib/api";
import { apiFetch } from "~/lib/api";
const ORANGE = "#FF5E13";
const NAVY = "#0D0D2A";
@ -13,8 +13,8 @@ export default function NotificationBell() {
createEffect(() => {
const fetchUnreadCount = async () => {
try {
const res = await api.get("/me/notifications/unread-count");
setUnreadCount(res.data?.unread_count || 0);
const res = await apiFetch("/api/me/notifications/unread-count");
setUnreadCount(res?.unread_count || 0);
} catch (e) {
// Silently fail
}
@ -32,8 +32,8 @@ export default function NotificationBell() {
// Fetch notifications when dropdown opens
const fetchNotifications = async () => {
try {
const res = await api.get("/me/notifications?limit=5");
setNotifications(res.data?.data || []);
const res = await apiFetch("/api/me/notifications?limit=5");
setNotifications(res?.data || []);
} catch (e) {
setNotifications([]);
}
@ -49,7 +49,7 @@ export default function NotificationBell() {
const markAsRead = async (id: string) => {
try {
await api.patch(`/me/notifications/${id}/read`);
await apiFetch(`/api/me/notifications/${id}/read`, { method: "PATCH" });
// Update local state
setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, is_read: true } : n)));
setUnreadCount((prev) => Math.max(0, prev - 1));
@ -60,7 +60,7 @@ export default function NotificationBell() {
const markAllAsRead = async () => {
try {
await api.patch("/me/notifications/read-all");
await apiFetch("/api/me/notifications/read-all", { method: "PATCH" });
setNotifications((prev) => prev.map((n) => ({ ...n, is_read: true })));
setUnreadCount(0);
} catch (e) {

View file

@ -1,274 +0,0 @@
import { createResource, createSignal, Show, For } from "solid-js";
import { useNavigate, useParams } from "@solidjs/router";
import DashboardLayout from "~/components/DashboardLayout";
import { RequireAuth } from "~/lib/auth";
import { api } from "~/lib/api";
interface AcceptedLead {
id: string;
requirement: {
id: string;
title: string;
profession_key: string;
location: string;
budget: number;
preferred_date: string;
description: string;
};
customer_contact: {
full_name: string;
phone: string;
email: string;
city: string;
};
tracecoins_deducted: number;
accepted_at: string;
}
export default function AcceptedLeadsView() {
const navigate = useNavigate();
const params = useParams();
// If ID param exists, show detail view
const isDetailView = () => !!params.id;
const [leads] = createResource(async () => {
const res = await api.get("/leads/accepted/me");
return res.data?.data || [];
});
const [selectedLead, setSelectedLead] = createSignal<AcceptedLead | null>(null);
// Fetch single lead if in detail view
createResource(async () => {
if (params.id) {
try {
const res = await api.get(`/leads/accepted/${params.id}`);
setSelectedLead(res.data);
} catch (e) {
navigate("/dashboard/leads/accepted");
}
}
});
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString("en-IN", {
day: "numeric",
month: "long",
year: "numeric",
});
};
const formatPhone = (phone: string) => {
return phone.replace(/(\d{5})(\d{5})/, "$1-$2");
};
// Detail View
if (isDetailView() && selectedLead()) {
const lead = selectedLead()!;
return (
<RequireAuth><DashboardLayout>
<div class="p-6 max-w-4xl mx-auto">
<button
onClick={() => navigate("/dashboard/leads/accepted")}
class="mb-4 text-gray-600 hover:text-gray-900 flex items-center gap-2"
>
Back to Accepted Leads
</button>
<div class="bg-white border rounded-xl overflow-hidden">
{/* Header */}
<div class="bg-green-50 p-6 border-b">
<div class="flex justify-between items-start">
<div>
<span class="inline-block px-3 py-1 bg-green-500 text-white text-sm rounded-full mb-2">
Lead Accepted
</span>
<h1 class="text-2xl font-bold text-gray-900">{lead.requirement.title}</h1>
<p class="text-gray-600 mt-1">Accepted on {formatDate(lead.accepted_at)}</p>
</div>
<div class="text-right">
<p class="text-sm text-gray-500">Tracecoins Deducted</p>
<p class="text-2xl font-bold text-orange-600">{lead.tracecoins_deducted} TC</p>
</div>
</div>
</div>
<div class="p-6 grid md:grid-cols-2 gap-8">
{/* Customer Contact Card */}
<div>
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
👤 Customer Contact
</h2>
<div class="bg-gray-50 rounded-xl p-6 space-y-4">
<div>
<p class="text-sm text-gray-500">Name</p>
<p class="text-lg font-semibold">{lead.customer_contact.full_name}</p>
</div>
<div>
<p class="text-sm text-gray-500">Phone</p>
<a
href={`tel:${lead.customer_contact.phone}`}
class="text-lg font-semibold text-orange-600 hover:underline"
>
{formatPhone(lead.customer_contact.phone)}
</a>
</div>
<div>
<p class="text-sm text-gray-500">Email</p>
<a
href={`mailto:${lead.customer_contact.email}`}
class="text-lg font-semibold text-orange-600 hover:underline"
>
{lead.customer_contact.email}
</a>
</div>
<div>
<p class="text-sm text-gray-500">Location</p>
<p class="font-medium">{lead.customer_contact.city}</p>
</div>
</div>
<div class="mt-4 flex gap-3">
<a
href={`tel:${lead.customer_contact.phone}`}
class="flex-1 px-4 py-3 bg-green-500 text-white rounded-lg text-center font-medium hover:bg-green-600 transition-colors"
>
📞 Call Customer
</a>
<a
href={`https://wa.me/${lead.customer_contact.phone.replace(/\D/g, "")}`}
target="_blank"
rel="noopener noreferrer"
class="flex-1 px-4 py-3 bg-green-600 text-white rounded-lg text-center font-medium hover:bg-green-700 transition-colors"
>
💬 WhatsApp
</a>
</div>
</div>
{/* Requirement Details */}
<div>
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
📋 Requirement Details
</h2>
<div class="space-y-4">
<div>
<p class="text-sm text-gray-500">Service Type</p>
<p class="font-medium capitalize">
{lead.requirement.profession_key.toLowerCase().replace(/_/g, " ")}
</p>
</div>
<div>
<p class="text-sm text-gray-500">Location</p>
<p class="font-medium">{lead.requirement.location}</p>
</div>
<div>
<p class="text-sm text-gray-500">Budget</p>
<p class="font-medium">
{lead.requirement.budget?.toLocaleString() || "Not specified"}
</p>
</div>
{lead.requirement.preferred_date && (
<div>
<p class="text-sm text-gray-500">Preferred Date</p>
<p class="font-medium">{formatDate(lead.requirement.preferred_date)}</p>
</div>
)}
<div>
<p class="text-sm text-gray-500">Description</p>
<p class="text-gray-700 mt-1">{lead.requirement.description}</p>
</div>
</div>
</div>
</div>
{/* Footer */}
<div class="bg-gray-50 p-6 border-t">
<p class="text-sm text-gray-500 text-center">
Contact the customer to discuss the project details and finalize the engagement.
</p>
</div>
</div>
</div>
</DashboardLayout></RequireAuth>
);
}
// List View
return (
<RequireAuth><DashboardLayout>
<div class="p-6">
<h1 class="text-2xl font-bold mb-6">Accepted Leads</h1>
{/* Loading State */}
<Show when={leads.loading}>
<div class="text-center py-12">
<div class="animate-spin w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full mx-auto mb-4" />
<p class="text-gray-500">Loading leads...</p>
</div>
</Show>
{/* Empty State */}
<Show when={!leads.loading && (leads() || []).length === 0}>
<div class="text-center py-12 bg-gray-50 rounded-xl">
<div class="text-6xl mb-4">🤝</div>
<h3 class="text-lg font-semibold text-gray-700 mb-2">No accepted leads yet</h3>
<p class="text-gray-500 mb-4">
Browse the marketplace and send requests to view customer contacts.
</p>
<button
onClick={() => navigate("/dashboard/marketplace")}
class="px-6 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors"
>
Browse Marketplace
</button>
</div>
</Show>
{/* Leads Grid */}
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={leads() || []}>
{(lead: AcceptedLead) => (
<div
onClick={() => navigate(`/dashboard/leads/accepted/${lead.id}`)}
class="bg-white border rounded-xl p-6 hover:shadow-lg transition-all cursor-pointer"
>
<div class="flex justify-between items-start mb-3">
<span class="inline-block px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
Accepted
</span>
<span class="text-orange-600 font-semibold text-sm">
-{lead.tracecoins_deducted} TC
</span>
</div>
<h3 class="font-semibold text-gray-900 mb-2 line-clamp-2">
{lead.requirement.title}
</h3>
<div class="space-y-2 text-sm">
<p class="text-gray-600 flex items-center gap-2">
<span>👤</span> {lead.customer_contact.full_name}
</p>
<p class="text-gray-600 flex items-center gap-2">
<span>📍</span> {lead.requirement.location}
</p>
{lead.requirement.budget && (
<p class="text-gray-600 flex items-center gap-2">
<span>💰</span> {lead.requirement.budget.toLocaleString()}
</p>
)}
</div>
<div class="mt-4 pt-4 border-t">
<p class="text-xs text-gray-500">Accepted {formatDate(lead.accepted_at)}</p>
</div>
</div>
)}
</For>
</div>
</div>
</DashboardLayout></RequireAuth>
);
}

View file

@ -59,19 +59,6 @@ export async function request<T = any>(
return { data: data as T, status: res.status, headers: res.headers };
}
export const api = {
get: <T = any>(path: string, headers?: HeadersInit) =>
request<T>(path, { method: "GET", headers }),
post: <T = any>(path: string, body?: unknown, headers?: HeadersInit) =>
request<T>(path, { method: "POST", body, headers }),
put: <T = any>(path: string, body?: unknown, headers?: HeadersInit) =>
request<T>(path, { method: "PUT", body, headers }),
patch: <T = any>(path: string, body?: unknown, headers?: HeadersInit) =>
request<T>(path, { method: "PATCH", body, headers }),
delete: <T = any>(path: string, headers?: HeadersInit) =>
request<T>(path, { method: "DELETE", headers }),
};
export async function fetchProfile(rolePrefix: string): Promise<any> {
return apiFetch(`/api/${rolePrefix}/profile/me`);
}

View file

@ -1,5 +0,0 @@
import AcceptedLeadsView from "~/components/dashboard/AcceptedLeadsView";
export default function AcceptedLeadDetailRoute() {
return <AcceptedLeadsView />;
}

View file

@ -1,5 +0,0 @@
import AcceptedLeadsView from "~/components/dashboard/AcceptedLeadsView";
export default function AcceptedLeadsIndexRoute() {
return <AcceptedLeadsView />;
}

View file

@ -1,94 +0,0 @@
import { createResource, Show } from "solid-js";
import { useNavigate, useParams } from "@solidjs/router";
import DashboardLayout from "~/components/DashboardLayout";
import { RequireAuth } from "~/lib/auth";
import { apiFetch } from "~/lib/api";
export default function MarketplaceRequirementDetailRoute() {
const navigate = useNavigate();
const params = useParams();
const [requirement] = createResource(() => params.id, async (id) => {
return apiFetch(`/api/customers/requirements/${id}`);
});
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString("en-IN", {
day: "numeric",
month: "long",
year: "numeric",
});
};
return (
<RequireAuth>
<DashboardLayout>
<div class="p-6 max-w-4xl mx-auto">
<button
onClick={() => navigate("/dashboard/marketplace")}
class="mb-4 text-gray-600 hover:text-gray-900 flex items-center gap-2"
>
Back to Marketplace
</button>
<Show when={requirement.loading}>
<div class="text-center py-12">
<div class="animate-spin w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full mx-auto mb-4" />
<p class="text-gray-500">Loading requirement...</p>
</div>
</Show>
<Show when={requirement.error}>
<div class="text-center py-12 bg-gray-50 rounded-xl">
<h3 class="text-lg font-semibold text-gray-700 mb-2">Requirement not found</h3>
<p class="text-gray-500">It may have been closed or removed.</p>
</div>
</Show>
<Show when={!requirement.loading && requirement()}>
{(req: any) => {
const r = req().data || req();
return (
<div class="bg-white border rounded-xl overflow-hidden">
<div class="bg-orange-50 p-6 border-b">
<h1 class="text-2xl font-bold text-gray-900">{r.title}</h1>
<p class="text-gray-600 mt-1">Posted on {r.created_at ? formatDate(r.created_at) : "—"}</p>
</div>
<div class="p-6 space-y-4">
<div>
<p class="text-sm text-gray-500">Service Type</p>
<p class="font-medium capitalize">
{(r.profession_key || "").toLowerCase().replace(/_/g, " ")}
</p>
</div>
<div>
<p class="text-sm text-gray-500">Location</p>
<p class="font-medium">{r.location}</p>
</div>
<div>
<p class="text-sm text-gray-500">Budget</p>
<p class="font-medium">
{r.budget?.toLocaleString() || "Not specified"}
</p>
</div>
<Show when={r.preferred_date}>
<div>
<p class="text-sm text-gray-500">Preferred Date</p>
<p class="font-medium">{formatDate(r.preferred_date)}</p>
</div>
</Show>
<div>
<p class="text-sm text-gray-500">Description</p>
<p class="text-gray-700 mt-1">{r.description}</p>
</div>
</div>
</div>
);
}}
</Show>
</div>
</DashboardLayout>
</RequireAuth>
);
}

View file

@ -1,13 +0,0 @@
import DashboardLayout from "~/components/DashboardLayout";
import { RequireAuth } from "~/lib/auth";
import ExploreServicesPage from "~/components/dashboard/ExploreServicesPage";
export default function MarketplacePage() {
return (
<RequireAuth>
<DashboardLayout>
<ExploreServicesPage />
</DashboardLayout>
</RequireAuth>
);
}

View file

@ -1,204 +0,0 @@
import { createResource, createSignal, Show, For } from "solid-js";
import { useNavigate } from "@solidjs/router";
import DashboardLayout from "~/components/DashboardLayout";
import { RequireAuth } from "~/lib/auth";
import { api } from "~/lib/api";
interface LeadRequest {
id: string;
requirement: {
id: string;
title: string;
location: string;
budget: number;
};
status: "PENDING" | "ACCEPTED" | "REJECTED" | "EXPIRED";
tracecoins_reserved: number;
expires_at: string;
requested_at: string;
}
export default function MyRequestsPage() {
const navigate = useNavigate();
const [activeTab, setActiveTab] = createSignal("ALL");
const [requests, { refetch }] = createResource(async () => {
const res = await api.get("/leads/requests/me");
return res.data?.data || [];
});
const filteredRequests = () => {
const all = requests() || [];
if (activeTab() === "ALL") return all;
return all.filter((r: LeadRequest) => r.status === activeTab());
};
const cancelRequest = async (id: string) => {
if (!confirm("Are you sure you want to cancel this request?")) return;
try {
await api.delete(`/leads/requests/${id}`);
refetch();
} catch (e) {
alert("Failed to cancel request");
}
};
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString("en-IN", {
day: "numeric",
month: "short",
hour: "2-digit",
minute: "2-digit",
});
};
const getTimeRemaining = (expiresAt: string) => {
const now = new Date();
const expiry = new Date(expiresAt);
const diff = expiry.getTime() - now.getTime();
if (diff <= 0) return "Expired";
const hours = Math.floor(diff / (1000 * 60 * 60));
if (hours < 1) {
const mins = Math.floor(diff / (1000 * 60));
return `${mins}m remaining`;
}
return `${hours}h remaining`;
};
const getStatusBadge = (status: string) => {
const classes = {
PENDING: "bg-yellow-100 text-yellow-800",
ACCEPTED: "bg-green-100 text-green-800",
REJECTED: "bg-red-100 text-red-800",
EXPIRED: "bg-gray-100 text-gray-800",
};
return classes[status as keyof typeof classes] || classes.EXPIRED;
};
return (
<RequireAuth>
<DashboardLayout>
<div class="p-6">
<h1 class="text-2xl font-bold mb-6">My Lead Requests</h1>
{/* Filter Tabs */}
<div class="flex gap-2 mb-6">
{["ALL", "PENDING", "ACCEPTED", "REJECTED", "EXPIRED", "CANCELLED"].map((tab) => (
<button
onClick={() => setActiveTab(tab)}
class={`px-4 py-2 rounded-lg font-medium transition-colors ${
activeTab() === tab
? "bg-orange-500 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
{tab === "ALL" ? "All Requests" : tab.charAt(0) + tab.slice(1).toLowerCase()}
</button>
))}
</div>
{/* Loading State */}
<Show when={requests.loading}>
<div class="text-center py-12">
<div class="animate-spin w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full mx-auto mb-4" />
<p class="text-gray-500">Loading requests...</p>
</div>
</Show>
{/* Empty State */}
<Show when={!requests.loading && filteredRequests().length === 0}>
<div class="text-center py-12 bg-gray-50 rounded-xl">
<div class="text-6xl mb-4">📋</div>
<h3 class="text-lg font-semibold text-gray-700 mb-2">No requests found</h3>
<p class="text-gray-500 mb-4">
{activeTab() === "ALL"
? "You haven't sent any lead requests yet."
: `No ${activeTab().toLowerCase()} requests.`}
</p>
<button
onClick={() => navigate("/dashboard/marketplace")}
class="px-6 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors"
>
Browse Marketplace
</button>
</div>
</Show>
{/* Requests List */}
<div class="space-y-4">
<For each={filteredRequests()}>
{(request: LeadRequest) => (
<div class="bg-white border rounded-xl p-6 hover:shadow-md transition-shadow">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-900">{request.requirement.title}</h3>
<p class="text-gray-500 text-sm">{request.requirement.location}</p>
</div>
<span
class={`px-3 py-1 rounded-full text-sm font-medium ${getStatusBadge(request.status)}`}
>
{request.status}
</span>
</div>
<div class="grid grid-cols-3 gap-4 mb-4 text-sm">
<div>
<p class="text-gray-500">Budget</p>
<p class="font-semibold">
{request.requirement.budget?.toLocaleString() || "Not specified"}
</p>
</div>
<div>
<p class="text-gray-500">Reserved</p>
<p class="font-semibold text-orange-600">{request.tracecoins_reserved} TC</p>
</div>
<div>
<p class="text-gray-500">
{request.status === "PENDING" ? "Expires" : "Requested"}
</p>
<p class="font-semibold">
{request.status === "PENDING"
? getTimeRemaining(request.expires_at)
: formatDate(request.requested_at)}
</p>
</div>
</div>
<div class="flex gap-3">
<button
onClick={() => navigate(`/dashboard/marketplace/${request.requirement.id}`)}
class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
View Requirement
</button>
<Show when={request.status === "PENDING"}>
<button
onClick={() => cancelRequest(request.id)}
class="px-4 py-2 border border-red-300 text-red-600 rounded-lg hover:bg-red-50 transition-colors"
>
Cancel Request
</button>
</Show>
<Show when={request.status === "ACCEPTED"}>
<button
onClick={() => navigate(`/dashboard/leads/accepted/${request.id}`)}
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
>
View Contact Details
</button>
</Show>
</div>
</div>
)}
</For>
</div>
</div>
</DashboardLayout>
</RequireAuth>
);
}

View file

@ -1,198 +0,0 @@
import { createResource, createSignal, Show, For } from "solid-js";
import { useNavigate } from "@solidjs/router";
import DashboardLayout from "~/components/DashboardLayout";
import { RequireAuth } from "~/lib/auth";
import { api } from "~/lib/api";
import { openPayuCheckout } from "~/lib/payu";
interface Package {
id: string;
name: string;
tracecoins_amount: number;
price_inr: number;
description: string;
}
export default function BuyTracecoinsPage() {
const navigate = useNavigate();
const [selectedPackage, setSelectedPackage] = createSignal<Package | null>(null);
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal("");
const [success, setSuccess] = createSignal(false);
const resolveRoleKey = () => {
if (typeof window === "undefined") return "DEVELOPER";
const fromUrl = new URLSearchParams(window.location.search).get("role");
if (fromUrl && fromUrl.trim()) return fromUrl.trim().toUpperCase();
const keys = ["nxtgauge_signup_profile_v1", "nxtgauge_auth_user", "nxtgauge_user"];
for (const key of keys) {
try {
const raw = window.localStorage.getItem(key) || window.sessionStorage.getItem(key);
if (!raw) continue;
const parsed = JSON.parse(raw);
const candidate = String(
parsed?.selectedProfessionalRole || parsed?.active_role || parsed?.roleKey || parsed?.role || ""
)
.trim()
.toUpperCase();
if (candidate && candidate !== "PROFESSIONAL") return candidate;
} catch {
// ignore malformed storage payloads
}
}
return "DEVELOPER";
};
const [packages] = createResource(async () => {
const roleKey = resolveRoleKey();
const res = await api.get(`/packages?role=${encodeURIComponent(roleKey)}&roleKey=${encodeURIComponent(roleKey)}`);
return res.data?.packages || [];
});
const handlePurchase = async () => {
const pkg = selectedPackage();
if (!pkg) return;
setLoading(true);
setError("");
try {
const paymentData = await openPayuCheckout({
amount: pkg.price_inr,
currency: "INR",
txnId: pkg.id,
description: `${pkg.name} - ${pkg.tracecoins_amount} Tracecoins`,
});
await api.post("/payments/verify", {
txnid: paymentData.txnid,
mihpayid: paymentData.mihpayid,
status: paymentData.status,
hash: paymentData.hash,
amount: paymentData.amount,
productinfo: paymentData.productinfo,
firstname: paymentData.firstname,
email: paymentData.email,
phone: paymentData.phone,
udf1: paymentData.udf1,
udf2: paymentData.udf2,
});
setSuccess(true);
setTimeout(() => {
navigate("/dashboard?nav=credits");
}, 2000);
} catch (e: any) {
setError(e.message || "Payment failed. Please try again.");
} finally {
setLoading(false);
}
};
const formatPrice = (paise: number) => {
return `${(paise / 100).toLocaleString()}`;
};
return (
<RequireAuth>
<DashboardLayout>
<div class="p-6 max-w-4xl mx-auto">
<h1 class="text-2xl font-bold mb-2">Buy Tracecoins</h1>
<p class="text-gray-600 mb-6">Purchase Tracecoins to send lead requests to customers</p>
<Show when={success()}>
<div class="bg-green-50 border border-green-200 rounded-xl p-6 mb-6 text-center">
<div class="text-5xl mb-3">🎉</div>
<h2 class="text-xl font-bold text-green-800 mb-2">Payment Successful!</h2>
<p class="text-green-700">Tracecoins have been added to your wallet.</p>
</div>
</Show>
<Show when={error()}>
<div class="bg-red-50 border border-red-200 rounded-xl p-4 mb-6">
<p class="text-red-700">{error()}</p>
</div>
</Show>
<Show when={!success()}>
<Show when={packages.loading}>
<div class="text-center py-12">
<div class="animate-spin w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full mx-auto mb-4" />
<p class="text-gray-500">Loading packages...</p>
</div>
</Show>
<Show when={!packages.loading}>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<For each={packages() || []}>
{(pkg: Package) => (
<div
onClick={() => setSelectedPackage(pkg)}
class={`border-2 rounded-xl p-6 cursor-pointer transition-all ${
selectedPackage()?.id === pkg.id
? "border-orange-500 bg-orange-50"
: "border-gray-200 hover:border-orange-300"
}`}
>
<div class="flex justify-between items-start mb-4">
<h3 class="font-bold text-lg">{pkg.name}</h3>
<Show when={selectedPackage()?.id === pkg.id}>
<span class="text-orange-500"></span>
</Show>
</div>
<div class="mb-4">
<span class="text-3xl font-bold text-gray-900">{pkg.tracecoins_amount}</span>
<span class="text-gray-500 ml-1">TC</span>
</div>
<p class="text-2xl font-bold text-orange-600 mb-3">
{formatPrice(pkg.price_inr)}
</p>
<p class="text-sm text-gray-600">{pkg.description}</p>
</div>
)}
</For>
</div>
<Show when={selectedPackage()}>
<div class="bg-gray-50 rounded-xl p-6 mb-6">
<h3 class="font-semibold mb-4">Order Summary</h3>
<div class="flex justify-between mb-2">
<span>{selectedPackage()?.name}</span>
<span>{formatPrice(selectedPackage()?.price_inr || 0)}</span>
</div>
<div class="border-t pt-2 mt-2">
<div class="flex justify-between font-bold text-lg">
<span>Total</span>
<span>{formatPrice(selectedPackage()?.price_inr || 0)}</span>
</div>
</div>
</div>
<div class="flex gap-4">
<button
onClick={() => navigate("/dashboard?nav=credits")}
class="flex-1 px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={handlePurchase}
disabled={loading()}
class="flex-1 px-6 py-3 bg-orange-500 text-white rounded-lg hover:bg-orange-600 disabled:opacity-50"
>
<Show when={loading()} fallback="Pay Now">
Processing...
</Show>
</button>
</div>
</Show>
</Show>
</Show>
</div>
</DashboardLayout>
</RequireAuth>
);
}

View file

@ -1,284 +0,0 @@
import { createResource, Show, For } from "solid-js";
import { useParams, useNavigate } from "@solidjs/router";
import DashboardLayout from "~/components/DashboardLayout";
import { api } from "~/lib/api";
interface Invoice {
id: string;
invoice_number: string;
payment_id: string;
user_id: string;
status: string;
currency: string;
invoice_type: string;
subtotal: number;
discount_amount: number;
cgst_rate: number;
cgst_amount: number;
sgst_rate: number;
sgst_amount: number;
igst_rate: number;
igst_amount: number;
total: number;
reverse_charge: boolean;
seller_name: string;
seller_address: string;
seller_gstin: string | null;
seller_pan: string | null;
seller_state_code: string | null;
place_of_supply_state: string | null;
customer_name: string | null;
customer_email: string | null;
customer_phone: string | null;
customer_billing_address: string | null;
customer_gstin: string | null;
customer_state_code: string | null;
discount_label: string | null;
notes: string | null;
pdf_object_key: string | null;
issued_at: string;
paid_at: string | null;
voided_at: string | null;
void_reason: string | null;
}
interface LineItem {
line_number: number;
description: string;
hsn_sac_code: string | null;
quantity: number;
unit_price_paise: number;
tax_rate_percent: number;
}
interface InvoiceResponse {
invoice: Invoice;
lines: LineItem[];
totals: {
subtotal: number;
discount: number;
taxable_value: number;
cgst: number;
sgst: number;
igst: number;
total_tax: number;
total: number;
};
}
export default function InvoiceDetailPage() {
const params = useParams();
const navigate = useNavigate();
const [data] = createResource(async () => {
const res = await api.get(`/wallet/me/invoices/${params.id}`);
return res.data as InvoiceResponse;
});
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString("en-IN", {
day: "numeric",
month: "long",
year: "numeric",
});
};
const formatPrice = (paise: number) => {
const rupees = paise / 100;
return new Intl.NumberFormat("en-IN", {
style: "currency",
currency: "INR",
minimumFractionDigits: 2,
}).format(rupees);
};
const downloadPdf = () => {
window.open(`/api/wallet/me/invoices/${params.id}/html`, "_blank");
};
return (
<DashboardLayout>
<div class="p-6 max-w-4xl mx-auto">
<button
onClick={() => navigate("/dashboard/wallet/invoices")}
class="mb-4 text-gray-600 hover:text-gray-900 flex items-center gap-2"
>
Back to Invoices
</button>
<Show when={data.loading}>
<div class="text-center py-12">
<div class="animate-spin w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full mx-auto mb-4" />
<p class="text-gray-500">Loading invoice...</p>
</div>
</Show>
<Show when={data.error}>
<div class="bg-red-50 border border-red-200 rounded-xl p-6 text-center">
<p class="text-red-700">Failed to load invoice: {String(data.error)}</p>
</div>
</Show>
<Show when={data()} keyed>
{(d: InvoiceResponse) => (
<div class="bg-white border rounded-xl overflow-hidden shadow-sm">
<div class="bg-gray-900 text-white p-6">
<div class="flex justify-between items-start">
<div>
<h1 class="text-2xl font-bold">TAX INVOICE</h1>
<p class="text-gray-400 mt-1 font-mono">{d.invoice.invoice_number}</p>
</div>
<div class="text-right">
<span
class={`inline-block px-3 py-1 rounded-full text-sm font-medium ${
d.invoice.status === "PAID"
? "bg-green-500 text-white"
: d.invoice.status === "ISSUED"
? "bg-blue-500 text-white"
: d.invoice.status === "VOID"
? "bg-red-500 text-white"
: "bg-yellow-500 text-white"
}`}
>
{d.invoice.status}
</span>
<p class="text-gray-400 text-sm mt-2">Issued {formatDate(d.invoice.issued_at)}</p>
</div>
</div>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="border rounded-lg p-4">
<p class="text-xs uppercase text-gray-500 mb-2">From</p>
<p class="font-semibold">{d.invoice.seller_name}</p>
<p class="text-sm whitespace-pre-line mt-1">{d.invoice.seller_address}</p>
<p class="text-xs mt-2">
{d.invoice.seller_gstin && (
<>GSTIN: {d.invoice.seller_gstin}<br /></>
)}
{d.invoice.seller_pan && <>PAN: {d.invoice.seller_pan}</>}
</p>
</div>
<div class="border rounded-lg p-4">
<p class="text-xs uppercase text-gray-500 mb-2">Bill To</p>
<p class="font-semibold">{d.invoice.customer_name ?? "—"}</p>
<p class="text-sm whitespace-pre-line mt-1">
{d.invoice.customer_billing_address ?? "—"}
</p>
<p class="text-xs mt-2">
{d.invoice.customer_gstin && (
<>GSTIN: {d.invoice.customer_gstin}<br /></>
)}
{d.invoice.place_of_supply_state && (
<>Place of Supply: {d.invoice.place_of_supply_state}</>
)}
</p>
</div>
</div>
<table class="w-full text-sm mb-6">
<thead>
<tr class="text-left text-xs uppercase text-gray-500 border-b">
<th class="py-2">#</th>
<th class="py-2">Description</th>
<th class="py-2">HSN/SAC</th>
<th class="py-2 text-right">Qty</th>
<th class="py-2 text-right">Rate</th>
<th class="py-2 text-right">Amount</th>
</tr>
</thead>
<tbody>
<For each={d.lines}>
{(line) => (
<tr class="border-b">
<td class="py-2">{line.line_number}</td>
<td class="py-2">{line.description}</td>
<td class="py-2">{line.hsn_sac_code ?? "—"}</td>
<td class="py-2 text-right">{line.quantity}</td>
<td class="py-2 text-right">{formatPrice(line.unit_price_paise)}</td>
<td class="py-2 text-right">
{formatPrice(line.unit_price_paise * line.quantity)}
</td>
</tr>
)}
</For>
</tbody>
</table>
<div class="border-t pt-4 space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">Subtotal</span>
<span>{formatPrice(d.totals.subtotal)}</span>
</div>
<Show when={d.totals.discount > 0}>
<div class="flex justify-between">
<span class="text-gray-600">Discount</span>
<span>-{formatPrice(d.totals.discount)}</span>
</div>
</Show>
<div class="flex justify-between">
<span class="text-gray-600">Taxable value</span>
<span>{formatPrice(d.totals.taxable_value)}</span>
</div>
<Show when={d.totals.cgst > 0}>
<div class="flex justify-between">
<span class="text-gray-600">CGST ({(d.invoice.cgst_rate).toFixed(2)}%)</span>
<span>{formatPrice(d.totals.cgst)}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">SGST ({(d.invoice.sgst_rate).toFixed(2)}%)</span>
<span>{formatPrice(d.totals.sgst)}</span>
</div>
</Show>
<Show when={d.totals.igst > 0}>
<div class="flex justify-between">
<span class="text-gray-600">IGST ({(d.invoice.igst_rate).toFixed(2)}%)</span>
<span>{formatPrice(d.totals.igst)}</span>
</div>
</Show>
<div class="flex justify-between text-lg font-bold pt-2 border-t">
<span>Total ({d.invoice.currency})</span>
<span class="text-orange-600">{formatPrice(d.totals.total)}</span>
</div>
</div>
<Show when={d.invoice.paid_at}>
<div class="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg text-sm">
<p class="text-green-800 font-semibold">
Paid on {formatDate(d.invoice.paid_at!)}
</p>
</div>
</Show>
<Show when={d.invoice.voided_at}>
<div class="mt-6 p-4 bg-red-50 border border-red-200 rounded-lg text-sm">
<p class="text-red-800 font-semibold"> Invoice voided</p>
<Show when={d.invoice.void_reason}>
<p class="text-red-700 mt-1">Reason: {d.invoice.void_reason}</p>
</Show>
</div>
</Show>
<div class="mt-6 flex gap-3">
<button
onClick={downloadPdf}
class="flex-1 px-4 py-3 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors"
>
Open Printable View
</button>
<button
onClick={() => window.print()}
class="px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Print
</button>
</div>
</div>
</div>
)}
</Show>
</div>
</DashboardLayout>
);
}

View file

@ -1,134 +0,0 @@
import { createResource, Show, For } from "solid-js";
import { A } from "@solidjs/router";
import DashboardLayout from "~/components/DashboardLayout";
import { api } from "~/lib/api";
interface InvoiceSummary {
id: string;
invoice_number: string;
payment_id: string;
status: string;
currency: string;
subtotal: number;
cgst_amount: number;
sgst_amount: number;
igst_amount: number;
total: number;
issued_at: string;
paid_at: string | null;
pdf_object_key: string | null;
package_name: string | null;
}
interface InvoicesResponse {
invoices: InvoiceSummary[];
page: number;
limit: number;
}
export default function InvoicesPage() {
const [data] = createResource(async () => {
const res = await api.get<InvoicesResponse>("/wallet/me/invoices");
return res.data;
});
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString("en-IN", {
day: "numeric",
month: "short",
year: "numeric",
});
};
const formatPrice = (paise: number) => {
return new Intl.NumberFormat("en-IN", {
style: "currency",
currency: "INR",
minimumFractionDigits: 0,
}).format(paise / 100);
};
return (
<DashboardLayout>
<div class="p-6 max-w-5xl mx-auto">
<h1 class="text-2xl font-bold mb-2">Invoices</h1>
<p class="text-gray-600 mb-6">All your Tracecoin purchase invoices</p>
<Show when={data.loading}>
<div class="text-center py-12">
<div class="animate-spin w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full mx-auto mb-4" />
<p class="text-gray-500">Loading invoices...</p>
</div>
</Show>
<Show when={data()} keyed>
{(d) => (
<Show
when={d.invoices.length > 0}
fallback={
<div class="bg-white border rounded-xl p-12 text-center">
<p class="text-gray-500 text-lg">No invoices yet.</p>
<p class="text-gray-400 text-sm mt-2">
Invoices are generated automatically when you purchase a Tracecoin package.
</p>
</div>
}
>
<div class="bg-white border rounded-xl overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs uppercase text-gray-500 border-b bg-gray-50">
<th class="px-4 py-3">Invoice #</th>
<th class="px-4 py-3">Package</th>
<th class="px-4 py-3">Date</th>
<th class="px-4 py-3 text-right">Total</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody>
<For each={d.invoices}>
{(inv) => (
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3 font-mono text-sm">{inv.invoice_number}</td>
<td class="px-4 py-3">{inv.package_name ?? "Tracecoin purchase"}</td>
<td class="px-4 py-3 text-gray-600">{formatDate(inv.issued_at)}</td>
<td class="px-4 py-3 text-right font-medium">
{formatPrice(inv.total)}
</td>
<td class="px-4 py-3">
<span
class={`inline-block px-2 py-1 rounded-full text-xs font-medium ${
inv.status === "PAID"
? "bg-green-100 text-green-700"
: inv.status === "ISSUED"
? "bg-blue-100 text-blue-700"
: inv.status === "VOID"
? "bg-red-100 text-red-700"
: "bg-gray-100 text-gray-700"
}`}
>
{inv.status}
</span>
</td>
<td class="px-4 py-3 text-right">
<A
href={`/dashboard/wallet/invoices/${inv.id}`}
class="text-orange-600 hover:underline text-sm"
>
View
</A>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</Show>
)}
</Show>
</div>
</DashboardLayout>
);
}

View file

@ -1,130 +0,0 @@
import { createSignal, onMount, Show } from "solid-js";
import { useNavigate } from "@solidjs/router";
import DashboardLayout from "~/components/DashboardLayout";
import { RequireAuth } from "~/lib/auth";
import { api } from "~/lib/api";
type PayuResponseFields = {
status: string;
txnid: string;
amount: string;
mihpayid: string;
hash: string;
firstname: string;
email: string;
phone: string;
productinfo: string;
udf1?: string;
udf2?: string;
udf3?: string;
udf4?: string;
udf5?: string;
};
function readFormFields(): PayuResponseFields {
if (typeof window === "undefined") {
return emptyFields();
}
const search = window.location.search.replace(/^\?/, "");
const params = new URLSearchParams(search);
return {
status: params.get("status") || "",
txnid: params.get("txnid") || "",
amount: params.get("amount") || "",
mihpayid: params.get("mihpayid") || "",
hash: params.get("hash") || "",
firstname: params.get("firstname") || "",
email: params.get("email") || "",
phone: params.get("phone") || "",
productinfo: params.get("productinfo") || "",
udf1: params.get("udf1") || undefined,
udf2: params.get("udf2") || undefined,
udf3: params.get("udf3") || undefined,
udf4: params.get("udf4") || undefined,
udf5: params.get("udf5") || undefined,
};
}
function emptyFields(): PayuResponseFields {
return {
status: "",
txnid: "",
amount: "",
mihpayid: "",
hash: "",
firstname: "",
email: "",
phone: "",
productinfo: "",
};
}
export default function PayuReturnPage() {
const navigate = useNavigate();
const [status, setStatus] = createSignal<"verifying" | "success" | "failure">("verifying");
const [error, setError] = createSignal("");
onMount(async () => {
const fields = readFormFields();
if (!fields.txnid) {
setStatus("failure");
setError("Missing PayU transaction id. Please try again from your wallet.");
return;
}
try {
const res = await api.post("/payments/verify", fields);
if (res?.data?.verified) {
setStatus("success");
setTimeout(() => {
navigate("/dashboard?nav=credits");
}, 2500);
} else {
setStatus("failure");
setError(res?.data?.message || "PayU reported a non-success status.");
}
} catch (e: any) {
setStatus("failure");
setError(e?.message || "Failed to verify PayU payment.");
}
});
return (
<RequireAuth>
<DashboardLayout>
<div class="p-6 max-w-2xl mx-auto">
<Show when={status() === "verifying"}>
<div class="text-center py-16">
<div class="animate-spin w-10 h-10 border-2 border-orange-500 border-t-transparent rounded-full mx-auto mb-4" />
<h2 class="text-xl font-semibold mb-2">Verifying your PayU payment</h2>
<p class="text-gray-500">Please do not refresh this page.</p>
</div>
</Show>
<Show when={status() === "success"}>
<div class="bg-green-50 border border-green-200 rounded-xl p-8 text-center">
<div class="text-5xl mb-3">🎉</div>
<h2 class="text-2xl font-bold text-green-800 mb-2">Payment Successful</h2>
<p class="text-green-700">Your Tracecoins have been credited. Redirecting to your wallet</p>
</div>
</Show>
<Show when={status() === "failure"}>
<div class="bg-red-50 border border-red-200 rounded-xl p-6">
<h2 class="text-xl font-bold text-red-800 mb-2">Payment Failed</h2>
<p class="text-red-700 mb-4">{error()}</p>
<button
onClick={() => navigate("/dashboard/wallet/buy")}
class="px-5 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600"
>
Try Again
</button>
</div>
</Show>
</div>
</DashboardLayout>
</RequireAuth>
);
}