From 19bacc5ab308f48635115df19fd44b569624e22f Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 19 Mar 2026 15:14:15 +0100 Subject: [PATCH] feat(admin-ui): align detail views with next layout patterns --- src/app.css | 121 ++++++++++- src/routes/admin/company/[id].tsx | 270 ++++++++++++++++++++---- src/routes/admin/leads/[id].tsx | 112 ++++++++-- src/routes/admin/photographer/[id].tsx | 86 +++++++- src/routes/admin/users/details/[id].tsx | 258 ++++++++++++++++++---- 5 files changed, 747 insertions(+), 100 deletions(-) diff --git a/src/app.css b/src/app.css index 07158ac..024ccaa 100644 --- a/src/app.css +++ b/src/app.css @@ -912,7 +912,9 @@ body { } .grid, - .list-grid { + .list-grid, + .field-grid-2, + .detail-layout { grid-template-columns: 1fr; } @@ -1105,6 +1107,113 @@ body { gap: 16px; } +.kv-item { + margin: 0; +} + +.kv-label { + margin: 0; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #64748b; +} + +.kv-value { + margin: 6px 0 0; + font-size: 14px; + color: #0f172a; + font-weight: 600; +} + +.status-pill { + display: inline-flex; + align-items: center; + border-radius: 999px; + border: 1px solid #cbd5e1; + padding: 6px 10px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.status-approved { + border-color: #86efac; + background: #f0fdf4; + color: #166534; +} + +.status-pending { + border-color: #fcd34d; + background: #fffbeb; + color: #92400e; +} + +.status-rejected { + border-color: #fca5a5; + background: #fef2f2; + color: #991b1b; +} + +.status-info { + border-color: #93c5fd; + background: #eff6ff; + color: #1d4ed8; +} + +.identity-avatar { + width: 62px; + height: 62px; + border-radius: 16px; + background: linear-gradient(135deg, #fd6216 0%, #050026 100%); + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 28px; + font-weight: 800; +} + +.mini-stat { + min-width: 90px; + text-align: center; +} + +.mini-stat-value { + margin: 0; + font-size: 26px; + font-weight: 800; + color: #0f172a; +} + +.mini-stat-label { + margin: 4px 0 0; + font-size: 11px; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.meta-chip { + display: inline-flex; + align-items: center; + border-radius: 999px; + border: 1px solid #e2e8f0; + background: #f8fafc; + color: #475569; + font-size: 11px; + font-weight: 600; + padding: 6px 10px; +} + +.detail-layout { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 16px; +} + .sub-card { border: 1px solid #e2e8f0; border-radius: 12px; @@ -1113,6 +1222,16 @@ body { margin-top: 12px; } +.sub-card-success { + border-color: #86efac; + background: #f0fdf4; +} + +.sub-card-warning { + border-color: #fcd34d; + background: #fffbeb; +} + .sub-card-header { display: flex; align-items: center; diff --git a/src/routes/admin/company/[id].tsx b/src/routes/admin/company/[id].tsx index f5fd66d..589fb79 100644 --- a/src/routes/admin/company/[id].tsx +++ b/src/routes/admin/company/[id].tsx @@ -4,7 +4,7 @@ import AdminShell from '~/components/AdminShell'; const API = '/api/gateway'; -type CompanyDetail = { +type CompanyCore = { id: string; company_name?: string; companyName?: string; @@ -18,21 +18,64 @@ type CompanyDetail = { city?: string; state?: string; address?: string; + description?: string; status?: string; is_verified?: boolean; isVerified?: boolean; - description?: string; pan?: string; gst?: string; tan?: string; + strength?: number; + gstVerified?: boolean; + mcaVerified?: boolean; + createdAt?: string; + updatedAt?: string; + updatedBy?: string; }; -async function fetchCompany(id: string): Promise { +type CompanyContact = { + city?: string; + state?: string; + pinCode?: string; + linkedin?: string; + instagram?: string; + twitter?: string; +}; + +type CompanyBanking = { + bankName?: string; + accountNumber?: string; + ifscCode?: string; + branchName?: string; + accountHolderName?: string; + isVerified?: boolean; +}; + +type CompanyBundle = { + company: CompanyCore; + contact: CompanyContact | null; + banking: CompanyBanking | null; +}; + +async function fetchCompanyBundle(id: string): Promise { try { const res = await fetch(`${API}/api/admin/companies/${id}`); if (!res.ok) return null; const data = await res.json(); - return data.company || data; + + if (data.company) { + return { + company: data.company, + contact: data.contact || null, + banking: data.banking || null, + }; + } + + return { + company: data, + contact: null, + banking: null, + }; } catch { return null; } @@ -40,10 +83,14 @@ async function fetchCompany(id: string): Promise { export default function CompanyDetailPage() { const params = useParams(); - const [company] = createResource(() => params.id, fetchCompany); + const [bundle] = createResource(() => params.id, fetchCompanyBundle); + + const company = createMemo(() => bundle()?.company || null); + const contact = createMemo(() => bundle()?.contact || null); + const banking = createMemo(() => bundle()?.banking || null); const name = createMemo(() => company()?.company_name || company()?.companyName || 'Company'); - const cid = createMemo(() => company()?.company_id || company()?.companyId || company()?.id || '—'); + const companyId = createMemo(() => company()?.company_id || company()?.companyId || company()?.id || '—'); const website = createMemo(() => company()?.website_url || company()?.websiteUrl || ''); const isVerified = createMemo(() => Boolean(company()?.is_verified ?? company()?.isVerified)); @@ -51,50 +98,201 @@ export default function CompanyDetailPage() {
-

Company Detail

-

Review company profile, contact details, and verification readiness.

+

{name()}

+

{companyId()}

+ + {isVerified() ? 'Verified - Can Post Jobs' : 'Not Verified'} + Back to Companies Open Approval Management
- +

Loading company...

- +

Company not found.

-
-
-

Company

-

Name: {name()}

-

Company ID: {cid()}

-

Industry: {company()!.industry || '—'}

-

Status: {company()!.status || '—'}

-

Verification: {isVerified() ? 'Verified' : 'Not Verified'}

-
-
-

Contact

-

Email: {company()!.email || '—'}

-

Phone: {company()!.phone || '—'}

-

City/State: {[company()!.city, company()!.state].filter(Boolean).join(', ') || '—'}

-

Address: {company()!.address || '—'}

-

Website: {website() ? {website()} : '—'}

-
-
- -
-

Compliance

-
-

PAN: {company()!.pan || '—'}

-

GST: {company()!.gst || '—'}

-

TAN: {company()!.tan || '—'}

+ <> +
+
+

Email

+

{company()!.email || '—'}

+
+
+

Phone

+

{company()!.phone || '—'}

+
+
+

Website

+

+ + {website()} + +

+
-
+ +
+
+
+

Company Information

+
+
+

Company Name

+

{name()}

+
+
+

Company ID

+

{companyId()}

+
+
+

Industry

+

{company()!.industry || '—'}

+
+
+

Company Size

+

{company()!.strength ? `${company()!.strength} employees` : '—'}

+
+
+

PAN

+

{company()!.pan || '—'}

+
+
+

GST

+

{company()!.gst || '—'}

+
+
+

TAN

+

{company()!.tan || '—'}

+
+
+

Status

+

{company()!.status || 'UNKNOWN'}

+
+
+

Address

+

{company()!.address || '—'}

+
+
+

Description

+

{company()!.description || '—'}

+
+
+
+ +
+

Contact Details

+
+
+

City

+

{contact()?.city || company()!.city || '—'}

+
+
+

State

+

{contact()?.state || company()!.state || '—'}

+
+
+

PIN Code

+

{contact()?.pinCode || '—'}

+
+
+

LinkedIn

+

{contact()?.linkedin || '—'}

+
+
+

Instagram

+

{contact()?.instagram || '—'}

+
+
+

Twitter

+

{contact()?.twitter || '—'}

+
+
+
+ +
+

Banking Details

+
+
+

Account Holder

+

{banking()?.accountHolderName || '—'}

+
+
+

Account Number

+

{banking()?.accountNumber || '—'}

+
+
+

Bank Name

+

{banking()?.bankName || '—'}

+
+
+

Branch

+

{banking()?.branchName || '—'}

+
+
+

IFSC Code

+

{banking()?.ifscCode || '—'}

+
+
+

Banking Verification

+

{banking()?.isVerified ? 'Verified' : 'Not Verified'}

+
+
+
+
+ +
+
+

Verification Status

+
+

Overall Status

+

{isVerified() ? 'Company Verified' : 'Pending Verification'}

+

+ {isVerified() ? 'This company can post job listings.' : 'Company cannot post jobs until verified.'} +

+
+
+
+

GST Verification

+

{company()!.gstVerified ? 'Verified' : 'Not Verified'}

+
+
+

MCA Verification

+

{company()!.mcaVerified ? 'Verified' : 'Not Verified'}

+
+
+

Banking Verification

+

{banking()?.isVerified ? 'Verified' : 'Not Verified'}

+
+
+
+ +
+

Metadata

+
+

Created At

+

{company()!.createdAt ? new Date(company()!.createdAt!).toLocaleString() : '—'}

+
+
+

Last Updated

+

{company()!.updatedAt ? new Date(company()!.updatedAt!).toLocaleString() : '—'}

+
+ +
+

Updated By

+

{company()!.updatedBy}

+
+
+
+
+
+
); diff --git a/src/routes/admin/leads/[id].tsx b/src/routes/admin/leads/[id].tsx index b0790ff..b25f054 100644 --- a/src/routes/admin/leads/[id].tsx +++ b/src/routes/admin/leads/[id].tsx @@ -20,12 +20,40 @@ type Lead = { updated_at?: string; }; -async function fetchLead(id: string): Promise { +type Requirement = { + id: string; + title?: string; + description?: string; + customerName?: string; + budgetMin?: number; + budgetMax?: number; + location?: string; + status?: string; +}; + +type LeadBundle = { + lead: Lead; + requirement: Requirement | null; +}; + +async function fetchLeadBundle(id: string): Promise { try { const res = await fetch(`${API}/api/leads/${id}`); if (!res.ok) return null; const data = await res.json(); - return data.lead || data; + const lead: Lead = data.lead || data; + + const requirementId = lead.requirementId || lead.requirement_id || ''; + if (!requirementId) return { lead, requirement: null }; + + try { + const reqRes = await fetch(`${API}/api/requirements/${requirementId}`); + if (!reqRes.ok) return { lead, requirement: null }; + const reqData = await reqRes.json(); + return { lead, requirement: reqData.requirement || reqData || null }; + } catch { + return { lead, requirement: null }; + } } catch { return null; } @@ -33,8 +61,10 @@ async function fetchLead(id: string): Promise { export default function LeadDetailPage() { const params = useParams(); - const [lead] = createResource(() => params.id, fetchLead); + const [bundle] = createResource(() => params.id, fetchLeadBundle); + const lead = createMemo(() => bundle()?.lead || null); + const requirement = createMemo(() => bundle()?.requirement || null); const requirementId = createMemo(() => lead()?.requirementId || lead()?.requirement_id || ''); const customerId = createMemo(() => lead()?.customerId || lead()?.customer_id || ''); const professionalId = createMemo(() => lead()?.professionalId || lead()?.professional_id || ''); @@ -51,30 +81,84 @@ export default function LeadDetailPage() { Back to Leads - +

Loading lead...

- +

Lead not found.

-
+
-

Lead ID: {lead()!.id}

-

Status: {lead()!.status || '—'}

-

Profession: {lead()!.profession || '—'}

-

Requirement ID: {requirementId() || '—'}

-

Customer ID: {customerId() || '—'}

-

Professional ID: {professionalId() || 'Unassigned'}

-

Created: {createdAt() ? new Date(createdAt()).toLocaleString() : '—'}

-

Updated: {updatedAt() ? new Date(updatedAt()).toLocaleString() : '—'}

+
+

Lead ID

+

{lead()!.id}

+
+
+

Status

+

{lead()!.status || '—'}

+
+
+

Profession

+

{lead()!.profession || '—'}

+
+
+

Professional ID

+

{professionalId() || 'Unassigned'}

+
+
+

Customer ID

+

{customerId() || '—'}

+
+
+

Created

+

{createdAt() ? new Date(createdAt()).toLocaleString() : '—'}

+
+ +
+

Updated

+

{new Date(updatedAt()!).toLocaleString()}

+
+
+
+

Requirement ID

+

{requirementId() || '—'}

+
+ + +
+
+

Linked Requirement

+
+

{requirement()!.title || 'Untitled Requirement'}

+

{requirement()!.description || 'No description available.'}

+
+
+

Customer

+

{requirement()!.customerName || 'Customer'}

+
+
+

Budget

+

{requirement()!.budgetMin || 0} - {requirement()!.budgetMax || 0}

+
+
+

Location

+

{requirement()!.location || '—'}

+
+
+

Requirement Status

+

{requirement()!.status || '—'}

+
+
+
+
diff --git a/src/routes/admin/photographer/[id].tsx b/src/routes/admin/photographer/[id].tsx index dd1ba67..90f4a3e 100644 --- a/src/routes/admin/photographer/[id].tsx +++ b/src/routes/admin/photographer/[id].tsx @@ -1,5 +1,5 @@ import { A, useParams } from '@solidjs/router'; -import { createMemo, createResource, Show } from 'solid-js'; +import { For, createMemo, createResource, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; const API = '/api/gateway'; @@ -17,6 +17,14 @@ type Photographer = { state?: string; experience?: string; portfolio_url?: string; + phoneNumber?: string; + location?: string; + gender?: string; + availability?: string; + permanentAddress?: string; + hometown?: string; + pincode?: string; + languages?: Array<{ language?: string; proficiency?: string }>; }; async function fetchPhotographer(id: string): Promise { @@ -24,8 +32,10 @@ async function fetchPhotographer(id: string): Promise { const res = await fetch(`${API}/api/admin/users/${id}`); if (res.ok) return res.json(); const fallback = await fetch(`${API}/api/users/${id}`); - if (!fallback.ok) return null; - return fallback.json(); + if (fallback.ok) return fallback.json(); + const photographerRes = await fetch(`${API}/api/photographers/${id}`); + if (!photographerRes.ok) return null; + return photographerRes.json(); } catch { return null; } @@ -61,14 +71,68 @@ export default function PhotographerDetailPage() {
-

Name: {name()}

-

Email: {profile()!.email || '—'}

-

Phone: {profile()!.phone || '—'}

-

Status: {profile()!.status || '—'}

-

City/State: {[profile()!.city, profile()!.state].filter(Boolean).join(', ') || '—'}

-

Experience: {profile()!.experience || '—'}

-

Created: {created() ? new Date(created()).toLocaleString() : '—'}

-

Portfolio: {profile()!.portfolio_url || '—'}

+
+

Name

+

{name()}

+
+
+

Email

+

{profile()!.email || '—'}

+
+
+

Phone

+

{profile()!.phone || profile()!.phoneNumber || '—'}

+
+
+

Location

+

{profile()!.location || [profile()!.city, profile()!.state].filter(Boolean).join(', ') || '—'}

+
+
+

Gender

+

{profile()!.gender || '—'}

+
+
+

Experience

+

{profile()!.experience || '—'}

+
+
+

Availability

+

{profile()!.availability || '—'}

+
+
+

Permanent Address

+

{profile()!.permanentAddress || '—'}

+
+
+

Hometown

+

{profile()!.hometown || '—'}

+
+
+

Pincode

+

{profile()!.pincode || '—'}

+
+
+

Status

+

{profile()!.status || '—'}

+
+
+

Created

+

{created() ? new Date(created()).toLocaleString() : '—'}

+
+
+

Portfolio

+

{profile()!.portfolio_url || '—'}

+
+ 0}> +
+

Languages

+
    + + {(item) =>
  • {item.language || 'Unknown'} - {item.proficiency || 'N/A'}
  • } +
    +
+
+
diff --git a/src/routes/admin/users/details/[id].tsx b/src/routes/admin/users/details/[id].tsx index ec02899..c71dd33 100644 --- a/src/routes/admin/users/details/[id].tsx +++ b/src/routes/admin/users/details/[id].tsx @@ -1,25 +1,49 @@ -import { A, useParams } from '@solidjs/router'; -import { createMemo, createResource, Show } from 'solid-js'; +import { A, useParams, useSearchParams } from '@solidjs/router'; +import { createMemo, createResource, For, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; const API = '/api/gateway'; -type User = { +type UserDetail = { id: string; + publicId?: string; name?: string; full_name?: string; - email: string; - role?: string; - role_name?: string; - roleId?: string; - status?: 'ACTIVE' | 'INACTIVE' | 'PENDING'; + email?: string; + status?: string; + profilePicture?: string; createdAt?: string; created_at?: string; - updatedAt?: string; - updated_at?: string; + lastLogin?: string; + accountType?: string; }; -async function fetchUser(id: string): Promise { +type RoleProfileSummary = { + id: string; + publicId?: string; + roleType?: string; + verificationStatus?: string; + submittedAt?: string | null; + updatedAt?: string | null; + isApproval?: boolean; +}; + +type UserBundle = { + user: UserDetail; + roleProfiles: RoleProfileSummary[]; +}; + +const ROLE_LABELS: Record = { + customer: 'Customer', + job_seeker: 'Job Seeker', + jobseeker: 'Job Seeker', + photographer: 'Photographer', + tutor: 'Tutor', + developer: 'Developer', + makeup_artist: 'Makeup Artist', +}; + +async function fetchUser(id: string): Promise { try { const adminRes = await fetch(`${API}/api/admin/users/${id}`); if (adminRes.ok) return adminRes.json(); @@ -32,21 +56,115 @@ async function fetchUser(id: string): Promise { } } +async function fetchRoleProfiles(userId: string): Promise { + const urls = [ + `${API}/api/role-profiles?userId=${encodeURIComponent(userId)}`, + `${API}/api/admin/role-profiles?userId=${encodeURIComponent(userId)}`, + ]; + + for (const url of urls) { + try { + const res = await fetch(url); + if (!res.ok) continue; + const data = await res.json(); + if (Array.isArray(data?.profiles)) return data.profiles; + if (Array.isArray(data)) return data; + } catch { + continue; + } + } + return []; +} + +async function fetchApprovals(userId: string): Promise { + try { + const res = await fetch(`${API}/api/approvals?requesterId=${encodeURIComponent(userId)}`); + if (!res.ok) return []; + const data = await res.json(); + const list = Array.isArray(data) ? data : data?.approvals || []; + if (!Array.isArray(list)) return []; + + return list.map((item: any) => ({ + id: item.id, + publicId: item.id, + roleType: item.requestType || item.entityType || 'Role Request', + verificationStatus: item.status || item.currentStatus || 'PENDING', + submittedAt: item.createdAt || null, + updatedAt: item.updatedAt || null, + isApproval: true, + })); + } catch { + return []; + } +} + +function normalizeRole(value: string | undefined): string { + return (value || '').toLowerCase().replace(/ /g, '_'); +} + +function formatDate(value: string | null | undefined): string { + if (!value) return '—'; + return new Date(value).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); +} + +function statusClass(status: string | undefined): string { + const normalized = (status || '').toUpperCase(); + if (normalized === 'APPROVED') return 'status-approved'; + if (normalized === 'REJECTED') return 'status-rejected'; + if (normalized === 'NEEDS_INFO') return 'status-info'; + return 'status-pending'; +} + +async function fetchUserBundle(id: string): Promise { + const user = await fetchUser(id); + if (!user) return null; + const userId = user.id || id; + + const [profiles, approvals] = await Promise.all([ + fetchRoleProfiles(userId), + fetchApprovals(userId), + ]); + + const roleProfiles = [...profiles, ...approvals].sort((a, b) => { + const ta = a.submittedAt ? new Date(a.submittedAt).getTime() : 0; + const tb = b.submittedAt ? new Date(b.submittedAt).getTime() : 0; + return tb - ta; + }); + + return { user, roleProfiles }; +} + export default function UserDetailPage() { const params = useParams(); - const [user] = createResource(() => params.id, fetchUser); + const [searchParams] = useSearchParams(); + const [bundle] = createResource(() => params.id, fetchUserBundle); + const user = createMemo(() => bundle()?.user || null); + const roleProfiles = createMemo(() => bundle()?.roleProfiles || []); + const roleFilter = createMemo(() => searchParams.roleFilter || ''); const displayName = createMemo(() => user()?.name || user()?.full_name || 'Unknown User'); - const roleName = createMemo(() => user()?.role_name || user()?.role || 'UNKNOWN'); - const createdAt = createMemo(() => user()?.createdAt || user()?.created_at || ''); - const updatedAt = createMemo(() => user()?.updatedAt || user()?.updated_at || ''); + const userCreatedAt = createMemo(() => user()?.createdAt || user()?.created_at || ''); + + const displayedProfiles = createMemo(() => { + const filter = roleFilter(); + if (!filter) return roleProfiles(); + const normalizedFilter = normalizeRole(filter); + return roleProfiles().filter((item) => normalizeRole(item.roleType) === normalizedFilter); + }); + + const verifiedCount = createMemo(() => displayedProfiles().filter((rp) => (rp.verificationStatus || '').toUpperCase() === 'APPROVED').length); + const roleTitleLabel = createMemo(() => { + const filter = roleFilter(); + if (!filter) return ''; + return ROLE_LABELS[filter] || filter.replace(/_/g, ' '); + }); return (
-

User Detail

-

Review account profile, role assignment, and account status.

+

{roleTitleLabel() ? `${roleTitleLabel()} Profile` : 'User Details'}

+

Review account profile and role registration data with approval status.

Back to Users @@ -54,35 +172,99 @@ export default function UserDetailPage() {
- +

Loading user...

- - +

User not found.

-
-
-

Profile

-

ID: {user()!.id}

-

Name: {displayName()}

-

Email: {user()!.email}

+ <> +
+
+
+
+ {displayName().charAt(0).toUpperCase() || '?'} +
+
+

{displayName()}

+

{user()!.email || '—'}

+

{user()!.publicId || user()!.id}

+
+
+
+
+

{roleFilter() ? displayedProfiles().length : roleProfiles().length}

+

{roleFilter() ? 'Profiles' : 'Total Roles'}

+
+
+

{verifiedCount()}

+

{roleFilter() ? 'Approved' : 'Verified'}

+
+
+
+
+ {user()!.status || 'unknown'} + Joined {formatDate(userCreatedAt())} + + Last seen {formatDate(user()!.lastLogin)} + +
-
-

Account

-

Role: {roleName()}

-

Status: {user()!.status || 'PENDING'}

-

Created: {createdAt() ? new Date(createdAt()).toLocaleString() : '—'}

-

Updated: {updatedAt() ? new Date(updatedAt()).toLocaleString() : '—'}

-
-
-
-

Raw Data

-
{JSON.stringify(user(), null, 2)}
-
+
+
+

{roleTitleLabel() ? `${roleTitleLabel()} Registration Data` : 'Registered Roles & Profiles'}

+
+ 0} fallback={ +
+

This user has no registered role profiles yet.

+
+ }> +
+ + + + + + + + + + + + + + + + {(item) => ( + + + + + + + + + + + )} + + +
RoleProfile IDVerificationSubmittedLast UpdatedAction
{ROLE_LABELS[normalizeRole(item.roleType)] || item.roleType || 'Role'}{item.publicId || item.id} + + {(item.verificationStatus || 'UNKNOWN').replace(/_/g, ' ')} + + {formatDate(item.submittedAt)}{formatDate(item.updatedAt)} + + {item.isApproval ? 'View Request' : 'Open Profile'} + +
+
+
+
+
);