2026-03-27 05:35:18 +01:00
|
|
|
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
|
2026-04-02 13:09:42 +02:00
|
|
|
|
type CompanyRecord = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
companyCode: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
registrationNumber: string;
|
|
|
|
|
|
industry: string;
|
|
|
|
|
|
location: string;
|
|
|
|
|
|
joinedOn: string;
|
2026-04-08 22:12:38 +02:00
|
|
|
|
joinedAt: string;
|
|
|
|
|
|
accountStatus: 'ACTIVE' | 'PENDING' | 'SUSPENDED' | 'INACTIVE';
|
2026-04-02 13:09:42 +02:00
|
|
|
|
verificationStatus: string;
|
|
|
|
|
|
subscriptionType: string;
|
|
|
|
|
|
jobPostingsCount: number;
|
|
|
|
|
|
totalHires: number;
|
|
|
|
|
|
updatedAt: string;
|
|
|
|
|
|
};
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
|
2026-04-08 22:12:38 +02:00
|
|
|
|
function StatusBadge(props: { status: CompanyRecord['accountStatus'] }) {
|
|
|
|
|
|
const active = () => props.status === 'ACTIVE';
|
|
|
|
|
|
const pending = () => props.status === 'PENDING';
|
|
|
|
|
|
const suspended = () => props.status === 'SUSPENDED';
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span
|
|
|
|
|
|
style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${
|
|
|
|
|
|
active() ? '#FFD8C2' : pending() ? '#F6D78F' : suspended() ? '#FECACA' : '#D1D5DB'
|
|
|
|
|
|
};background:${
|
|
|
|
|
|
active() ? '#FFF1EB' : pending() ? '#FFF3D6' : suspended() ? '#FEF2F2' : '#F3F4F6'
|
|
|
|
|
|
};color:${active() ? '#FF5E13' : pending() ? '#B7791F' : suspended() ? '#B91C1C' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${
|
|
|
|
|
|
active() ? '#FF5E13' : pending() ? '#B7791F' : suspended() ? '#B91C1C' : '#9CA3AF'
|
|
|
|
|
|
};margin-right:5px;flex-shrink:0`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{props.status.charAt(0) + props.status.slice(1).toLowerCase()}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeStatus(value: unknown): CompanyRecord['accountStatus'] {
|
|
|
|
|
|
const key = String(value || '').toUpperCase();
|
|
|
|
|
|
if (key === 'APPROVED' || key === 'ACTIVE') return 'ACTIVE';
|
|
|
|
|
|
if (key === 'PENDING' || key === 'PENDING_REVIEW' || key === 'UNDER_REVIEW') return 'PENDING';
|
|
|
|
|
|
if (key === 'SUSPENDED') return 'SUSPENDED';
|
|
|
|
|
|
return 'INACTIVE';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
|
export default function CompanyManagementPage() {
|
2026-04-08 22:12:38 +02:00
|
|
|
|
const [listTab, setListTab] = createSignal<'all' | 'active' | 'pending' | 'view'>('all');
|
|
|
|
|
|
const [detailTab, setDetailTab] = createSignal<'overview' | 'verification' | 'metrics'>('overview');
|
2026-04-02 13:09:42 +02:00
|
|
|
|
const [rows, setRows] = createSignal<CompanyRecord[]>([]);
|
2026-04-08 22:12:38 +02:00
|
|
|
|
const [isLoading, setIsLoading] = createSignal(false);
|
|
|
|
|
|
const [loadError, setLoadError] = createSignal('');
|
|
|
|
|
|
const [selectedCompany, setSelectedCompany] = createSignal<CompanyRecord | null>(null);
|
|
|
|
|
|
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
|
|
|
|
|
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
const [search, setSearch] = createSignal('');
|
2026-04-08 22:12:38 +02:00
|
|
|
|
const [statusFilter, setStatusFilter] = createSignal<'all' | CompanyRecord['accountStatus']>('all');
|
|
|
|
|
|
const [sortBy, setSortBy] = createSignal<'name_asc' | 'name_desc' | 'joined_desc' | 'joined_asc'>('name_asc');
|
|
|
|
|
|
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
|
|
|
|
|
|
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
|
2026-03-27 05:35:18 +01:00
|
|
|
|
|
|
|
|
|
|
const load = async () => {
|
2026-04-08 22:12:38 +02:00
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
setLoadError('');
|
2026-03-27 05:35:18 +01:00
|
|
|
|
try {
|
2026-04-08 22:12:38 +02:00
|
|
|
|
const accessToken = typeof sessionStorage !== 'undefined'
|
|
|
|
|
|
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
|
|
|
|
|
: '';
|
2026-04-07 22:12:52 +02:00
|
|
|
|
const r = await fetch('/api/admin/companies', {
|
2026-04-08 22:12:38 +02:00
|
|
|
|
headers: {
|
|
|
|
|
|
Accept: 'application/json',
|
|
|
|
|
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
|
|
|
|
},
|
2026-04-03 02:49:14 +02:00
|
|
|
|
credentials: 'include',
|
|
|
|
|
|
});
|
2026-04-02 13:09:42 +02:00
|
|
|
|
if (!r.ok) throw new Error('Failed to fetch companies');
|
2026-04-08 22:12:38 +02:00
|
|
|
|
const data = await r.json().catch(() => []);
|
|
|
|
|
|
const mapped: CompanyRecord[] = (Array.isArray(data) ? data : []).map((c: any) => {
|
|
|
|
|
|
const joinedAt = String(c.created_at || '');
|
|
|
|
|
|
const status = normalizeStatus(c.status);
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: String(c.id || ''),
|
|
|
|
|
|
companyCode: String(c.id || '').slice(0, 8).toUpperCase() || '—',
|
|
|
|
|
|
name: String(c.company_name || c.name || 'Unnamed Company'),
|
|
|
|
|
|
registrationNumber: String(c.registration_number || c.gst_number || 'Pending Registration'),
|
|
|
|
|
|
industry: String(c.industry || 'Not Specified'),
|
|
|
|
|
|
location: String(c.location || c.city || 'Not Specified'),
|
|
|
|
|
|
joinedOn: joinedAt ? new Date(joinedAt).toLocaleDateString() : '—',
|
|
|
|
|
|
joinedAt,
|
|
|
|
|
|
accountStatus: status,
|
|
|
|
|
|
verificationStatus: status === 'ACTIVE' ? 'VERIFIED' : 'PENDING',
|
|
|
|
|
|
subscriptionType: String(c.subscription_type || 'STANDARD'),
|
|
|
|
|
|
jobPostingsCount: Number(c.job_postings_count || 0),
|
|
|
|
|
|
totalHires: Number(c.total_hires || 0),
|
|
|
|
|
|
updatedAt: String(c.updated_at || c.created_at || ''),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
2026-04-02 13:09:42 +02:00
|
|
|
|
setRows(mapped);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error(e);
|
2026-04-08 22:12:38 +02:00
|
|
|
|
setLoadError('Failed to load companies.');
|
2026-04-02 13:09:42 +02:00
|
|
|
|
setRows([]);
|
2026-04-08 22:12:38 +02:00
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoading(false);
|
2026-03-27 05:35:18 +01:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMount(() => void load());
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
|
const filteredRows = createMemo(() => {
|
2026-04-08 22:12:38 +02:00
|
|
|
|
let list = rows();
|
|
|
|
|
|
if (statusFilter() !== 'all') list = list.filter((row) => row.accountStatus === statusFilter());
|
|
|
|
|
|
|
|
|
|
|
|
const query = search().trim().toLowerCase();
|
|
|
|
|
|
if (query) {
|
|
|
|
|
|
list = list.filter((row) =>
|
|
|
|
|
|
row.name.toLowerCase().includes(query)
|
|
|
|
|
|
|| row.companyCode.toLowerCase().includes(query)
|
|
|
|
|
|
|| row.registrationNumber.toLowerCase().includes(query)
|
|
|
|
|
|
|| row.industry.toLowerCase().includes(query),
|
|
|
|
|
|
);
|
2026-03-27 05:35:18 +01:00
|
|
|
|
}
|
2026-04-08 22:12:38 +02:00
|
|
|
|
|
|
|
|
|
|
const sorted = [...list];
|
|
|
|
|
|
const mode = sortBy();
|
2026-03-27 05:35:18 +01:00
|
|
|
|
sorted.sort((a, b) => {
|
2026-04-08 22:12:38 +02:00
|
|
|
|
if (mode === 'name_desc') return b.name.localeCompare(a.name);
|
|
|
|
|
|
if (mode === 'joined_desc') return (Date.parse(b.joinedAt) || 0) - (Date.parse(a.joinedAt) || 0);
|
|
|
|
|
|
if (mode === 'joined_asc') return (Date.parse(a.joinedAt) || 0) - (Date.parse(b.joinedAt) || 0);
|
2026-03-27 05:35:18 +01:00
|
|
|
|
return a.name.localeCompare(b.name);
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
});
|
2026-04-08 22:12:38 +02:00
|
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
|
return sorted;
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-08 22:12:38 +02:00
|
|
|
|
const exportCsv = () => {
|
|
|
|
|
|
const headers = ['Company', 'Code', 'Industry', 'Location', 'Registration', 'Status', 'Joined'];
|
|
|
|
|
|
const lines = filteredRows().map((c) => [
|
|
|
|
|
|
c.name,
|
|
|
|
|
|
c.companyCode,
|
|
|
|
|
|
c.industry,
|
|
|
|
|
|
c.location,
|
|
|
|
|
|
c.registrationNumber,
|
|
|
|
|
|
c.accountStatus,
|
|
|
|
|
|
c.joinedOn,
|
|
|
|
|
|
]);
|
|
|
|
|
|
const csv = [headers, ...lines]
|
|
|
|
|
|
.map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
|
|
|
|
|
.join('\n');
|
|
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
|
link.href = url;
|
|
|
|
|
|
link.download = `companies-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
|
link.click();
|
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const openView = (company: CompanyRecord) => {
|
|
|
|
|
|
setSelectedCompany(company);
|
|
|
|
|
|
setListTab('view');
|
|
|
|
|
|
setOpenMenuId(null);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
return (
|
2026-04-08 22:12:38 +02:00
|
|
|
|
<div class="w-full space-y-6 pb-8">
|
|
|
|
|
|
<div style="margin-bottom: 1.5rem">
|
|
|
|
|
|
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Company Management</h1>
|
|
|
|
|
|
<p class="mt-1 text-[14px] text-[#6B7280]">Manage and monitor all registered companies and their account status.</p>
|
|
|
|
|
|
</div>
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
|
2026-04-08 22:12:38 +02:00
|
|
|
|
<div>
|
|
|
|
|
|
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
|
|
|
|
|
{([
|
|
|
|
|
|
{ key: 'all', label: 'All Companies', action: () => { setListTab('all'); setStatusFilter('all'); } },
|
|
|
|
|
|
{ key: 'active', label: 'Active', action: () => { setListTab('active'); setStatusFilter('ACTIVE'); } },
|
|
|
|
|
|
{ key: 'pending', label: 'Pending', action: () => { setListTab('pending'); setStatusFilter('PENDING'); } },
|
|
|
|
|
|
{ key: 'view', label: 'View Company', action: () => setListTab('view') },
|
|
|
|
|
|
] as const).map((tab) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={tab.action}
|
|
|
|
|
|
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{tab.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
2026-04-02 13:09:42 +02:00
|
|
|
|
</div>
|
2026-03-27 05:35:18 +01:00
|
|
|
|
|
2026-04-08 22:12:38 +02:00
|
|
|
|
<Show when={listTab() === 'view'}>
|
|
|
|
|
|
<Show when={!selectedCompany()}>
|
|
|
|
|
|
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;padding:48px 24px;text-align:center">
|
|
|
|
|
|
<p style="font-size:15px;font-weight:600;color:#111827">No company selected</p>
|
|
|
|
|
|
<p style="margin-top:6px;font-size:13px;color:#6B7280">Click the <strong>⋮</strong> menu on any company row and choose <strong>View Company</strong>.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
|
|
<Show when={selectedCompany()}>
|
|
|
|
|
|
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
|
|
|
|
|
<div style="padding:20px 24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 style="font-size:18px;font-weight:700;color:#111827">{selectedCompany()!.name}</h2>
|
|
|
|
|
|
<p style="margin-top:2px;font-size:13px;color:#6B7280">{selectedCompany()!.companyCode} • Joined {selectedCompany()!.joinedOn}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<StatusBadge status={selectedCompany()!.accountStatus} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
|
|
|
|
|
|
{(['overview', 'verification', 'metrics'] as const).map((tab, i) => {
|
|
|
|
|
|
const labels = ['Overview', 'Verification', 'Metrics'];
|
|
|
|
|
|
const active = () => detailTab() === tab;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button type="button" onClick={() => setDetailTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
|
|
|
|
|
|
{labels[i]}
|
|
|
|
|
|
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style="padding:24px">
|
|
|
|
|
|
<Show when={detailTab() === 'overview'}>
|
|
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
|
|
|
|
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
|
|
|
|
|
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Company Profile</h3>
|
|
|
|
|
|
<div style="display:flex;flex-direction:column;gap:12px">
|
|
|
|
|
|
{[
|
|
|
|
|
|
{ l: 'Industry', v: selectedCompany()!.industry },
|
|
|
|
|
|
{ l: 'Location', v: selectedCompany()!.location },
|
|
|
|
|
|
{ l: 'Registration', v: selectedCompany()!.registrationNumber },
|
|
|
|
|
|
{ l: 'Subscription', v: selectedCompany()!.subscriptionType },
|
|
|
|
|
|
].map((item) => (
|
|
|
|
|
|
<div style="display:flex;justify-content:space-between">
|
|
|
|
|
|
<span style="font-size:13px;color:#6B7280">{item.l}</span>
|
|
|
|
|
|
<span style="font-size:13px;font-weight:600;color:#111827">{item.v || '—'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
|
|
<Show when={detailTab() === 'verification'}>
|
|
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
|
|
|
|
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
|
|
|
|
|
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Verification Summary</h3>
|
|
|
|
|
|
<div style="display:flex;flex-direction:column;gap:12px">
|
|
|
|
|
|
{[
|
|
|
|
|
|
{ l: 'Verification Status', v: selectedCompany()!.verificationStatus },
|
|
|
|
|
|
{ l: 'Account Status', v: selectedCompany()!.accountStatus },
|
|
|
|
|
|
{ l: 'Last Updated', v: selectedCompany()!.updatedAt ? new Date(selectedCompany()!.updatedAt).toLocaleString() : '—' },
|
|
|
|
|
|
].map((item) => (
|
|
|
|
|
|
<div style="display:flex;justify-content:space-between">
|
|
|
|
|
|
<span style="font-size:13px;color:#6B7280">{item.l}</span>
|
|
|
|
|
|
<span style="font-size:13px;font-weight:600;color:#111827">{item.v || '—'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
|
|
<Show when={detailTab() === 'metrics'}>
|
|
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
|
|
|
|
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
|
|
|
|
|
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Performance Metrics</h3>
|
|
|
|
|
|
<div style="display:flex;flex-direction:column;gap:12px">
|
|
|
|
|
|
{[
|
|
|
|
|
|
{ l: 'Job Postings', v: String(selectedCompany()!.jobPostingsCount || 0) },
|
|
|
|
|
|
{ l: 'Total Hires', v: String(selectedCompany()!.totalHires || 0) },
|
|
|
|
|
|
].map((item) => (
|
|
|
|
|
|
<div style="display:flex;justify-content:space-between">
|
|
|
|
|
|
<span style="font-size:13px;color:#6B7280">{item.l}</span>
|
|
|
|
|
|
<span style="font-size:13px;font-weight:600;color:#111827">{item.v}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style="display:flex;align-items:center;gap:10px;padding:14px 24px;border-top:1px solid #E5E7EB">
|
|
|
|
|
|
<button type="button" onClick={() => { setSelectedCompany(null); setListTab('all'); }} style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Back to List</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ display: listTab() === 'view' ? 'none' : 'block' }}>
|
|
|
|
|
|
<div style="position:relative;margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:visible;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
|
|
|
|
|
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="Search companies..."
|
|
|
|
|
|
value={search()}
|
|
|
|
|
|
onInput={(e) => setSearch(e.currentTarget.value)}
|
|
|
|
|
|
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div style="position:relative">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setSortMenuOpen((v) => !v);
|
|
|
|
|
|
setFilterMenuOpen(false);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
|
|
|
|
|
|
Sort
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<Show when={sortMenuOpen()}>
|
|
|
|
|
|
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
|
|
|
|
|
{([
|
|
|
|
|
|
{ key: 'name_asc', label: 'Name (A-Z)' },
|
|
|
|
|
|
{ key: 'name_desc', label: 'Name (Z-A)' },
|
|
|
|
|
|
{ key: 'joined_desc', label: 'Joined (Newest)' },
|
|
|
|
|
|
{ key: 'joined_asc', label: 'Joined (Oldest)' },
|
|
|
|
|
|
] as const).map((item) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setSortBy(item.key);
|
|
|
|
|
|
setSortMenuOpen(false);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === item.key ? '#FF5E13' : '#374151'};background:${sortBy() === item.key ? '#FFF1EB' : 'transparent'}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style="position:relative">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setFilterMenuOpen((v) => !v);
|
|
|
|
|
|
setSortMenuOpen(false);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
|
|
|
|
|
Filters
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<Show when={filterMenuOpen()}>
|
|
|
|
|
|
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:180px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
|
|
|
|
|
{([
|
|
|
|
|
|
{ key: 'all', label: 'All Status' },
|
|
|
|
|
|
{ key: 'ACTIVE', label: 'Active' },
|
|
|
|
|
|
{ key: 'PENDING', label: 'Pending' },
|
|
|
|
|
|
{ key: 'SUSPENDED', label: 'Suspended' },
|
|
|
|
|
|
{ key: 'INACTIVE', label: 'Inactive' },
|
|
|
|
|
|
] as const).map((item) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setStatusFilter(item.key);
|
|
|
|
|
|
setFilterMenuOpen(false);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === item.key ? '#FF5E13' : '#374151'};background:${statusFilter() === item.key ? '#FFF1EB' : 'transparent'}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button type="button" onClick={exportCsv} style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
|
|
|
|
|
|
Export
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="overflow-x-auto">
|
|
|
|
|
|
<table class="min-w-full">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr style="background:#0D0D2A;text-align:left">
|
|
|
|
|
|
{['Company', 'Industry', 'Location', 'Status', 'Joined', 'Actions'].map((h) => (
|
|
|
|
|
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{h}</th>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<Show when={isLoading()}>
|
|
|
|
|
|
<tr><td colSpan={6} style="padding:32px;text-align:center;color:#64748b">Loading...</td></tr>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
<Show when={!isLoading() && !!loadError()}>
|
|
|
|
|
|
<tr><td colSpan={6} style="padding:32px;text-align:center;color:#b91c1c">{loadError()}</td></tr>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
<Show
|
|
|
|
|
|
when={!isLoading() && !loadError() && filteredRows().length > 0}
|
|
|
|
|
|
fallback={
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td colSpan={6} style="padding:32px;text-align:center">
|
|
|
|
|
|
<p style="font-size:15px;font-weight:600;color:#111827">No companies found</p>
|
|
|
|
|
|
<p style="margin-top:6px;font-size:13px;color:#6B7280">Try changing filters or search.</p>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<For each={filteredRows()}>
|
|
|
|
|
|
{(row) => (
|
|
|
|
|
|
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
|
|
|
|
|
|
<td style="padding:12px 20px">
|
|
|
|
|
|
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
|
|
|
|
|
|
<p style="font-size:12px;color:#6B7280">{row.companyCode}</p>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.industry}</td>
|
|
|
|
|
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.location}</td>
|
|
|
|
|
|
<td style="padding:12px 20px"><StatusBadge status={row.accountStatus} /></td>
|
|
|
|
|
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.joinedOn}</td>
|
|
|
|
|
|
<td style="padding:12px 20px;position:relative">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)}
|
|
|
|
|
|
style="display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;border-radius:8px;color:#9CA3AF;background:none;border:none;cursor:pointer"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<Show when={openMenuId() === row.id}>
|
|
|
|
|
|
<div style="position:absolute;right:20px;top:44px;z-index:20;width:180px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.12)">
|
|
|
|
|
|
<button type="button" onClick={() => openView(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">View Company</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</For>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Show when={!isLoading() && !loadError() && filteredRows().length > 0}>
|
|
|
|
|
|
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
|
|
|
|
|
|
<p style="font-size:13px;color:#6B7280">
|
|
|
|
|
|
Showing <strong style="font-weight:600;color:#111827">1–{filteredRows().length}</strong> of{' '}
|
|
|
|
|
|
<strong style="font-weight:600;color:#111827">{filteredRows().length}</strong> companies
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div style="display:flex;align-items:center;gap:4px">
|
|
|
|
|
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">‹</button>
|
|
|
|
|
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
|
|
|
|
|
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
|
|
|
|
|
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">3</button>
|
|
|
|
|
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">›</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
</div>
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
</div>
|
ui(step-2): apply reference layout to 8 management pages
- customer, candidate, photographer, makeup-artist, users, company,
developers, tutors: all get white sticky header, -mx-6/-mt-6 layout,
data-table/table-card CSS classes, navy primary buttons, orange tab
underlines, Tailwind classes replacing inline styles on all table cells
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 04:51:40 +01:00
|
|
|
|
</div>
|
2026-04-08 22:12:38 +02:00
|
|
|
|
</div>
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
);
|
|
|
|
|
|
}
|