feat: classification labels in data browser + kiosk recurring revenue filter
- data-browser/companies: resolve classification picklist IDs to labels in table column and detail modal; also added to DetailModal COMPANY_GROUPS - DetailModal: add 'classification' FieldType with color-coded badge map - kiosk stats + activity: switch from label-based exclusion to ID-based allowlist (included_classifications). Only shows companies with classification IN (15,16,17,18,203,205,206,207,202,5,12) = managed service / recurring revenue tiers only
This commit is contained in:
parent
5f0fbb4734
commit
a98c0daf15
4 changed files with 84 additions and 34 deletions
|
|
@ -9,6 +9,32 @@ import { Badge } from '@/components/ui/badge';
|
||||||
import { ArrowLeft, Users } from 'lucide-react';
|
import { ArrowLeft, Users } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
const CLASSIFICATION_LABELS: Record<number, string> = {
|
||||||
|
5: 'Block Hour',
|
||||||
|
9: 'Canceled',
|
||||||
|
202: 'Co-Managed',
|
||||||
|
16: 'Gold (Legacy)',
|
||||||
|
206: 'IT Complete w/ Gold Security',
|
||||||
|
207: 'IT Core / Silver Security',
|
||||||
|
203: 'IT Foundation / Bronze Security',
|
||||||
|
205: 'IT Premier / Platinum Security',
|
||||||
|
14: 'Jeopardy Company',
|
||||||
|
201: 'Partner',
|
||||||
|
15: 'Platinum (Legacy)',
|
||||||
|
13: 'Residential (no-pay)',
|
||||||
|
17: 'Silver (Legacy)',
|
||||||
|
12: 'T&M',
|
||||||
|
7: 'Target',
|
||||||
|
200: 'Tools Only',
|
||||||
|
18: 'Bronze (Legacy)',
|
||||||
|
};
|
||||||
|
|
||||||
|
function classificationLabel(val: any): string {
|
||||||
|
if (val === null || val === undefined || val === '') return '—';
|
||||||
|
const num = parseInt(String(val), 10);
|
||||||
|
return CLASSIFICATION_LABELS[num] ?? String(val);
|
||||||
|
}
|
||||||
|
|
||||||
export default function CompaniesBrowserPage() {
|
export default function CompaniesBrowserPage() {
|
||||||
const [companies, setCompanies] = useState([]);
|
const [companies, setCompanies] = useState([]);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
|
@ -87,6 +113,7 @@ export default function CompaniesBrowserPage() {
|
||||||
key: 'classification',
|
key: 'classification',
|
||||||
label: 'Classification',
|
label: 'Classification',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
render: (value: any) => classificationLabel(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'is_active',
|
key: 'is_active',
|
||||||
|
|
@ -124,7 +151,7 @@ export default function CompaniesBrowserPage() {
|
||||||
{ key: 'state', label: 'State' },
|
{ key: 'state', label: 'State' },
|
||||||
{ key: 'postal_code', label: 'Postal Code' },
|
{ key: 'postal_code', label: 'Postal Code' },
|
||||||
{ key: 'country', label: 'Country' },
|
{ key: 'country', label: 'Country' },
|
||||||
{ key: 'classification', label: 'Classification' },
|
{ key: 'classification', label: 'Classification', render: (v: any) => classificationLabel(v) },
|
||||||
{ key: 'company_type', label: 'Company Type' },
|
{ key: 'company_type', label: 'Company Type' },
|
||||||
{ key: 'company_category_id', label: 'Company Category ID' },
|
{ key: 'company_category_id', label: 'Company Category ID' },
|
||||||
{ key: 'owner_resource_id', label: 'Owner Resource ID' },
|
{ key: 'owner_resource_id', label: 'Owner Resource ID' },
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,15 @@ async function getExcludedCompanyIds(): Promise<number[]> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getExcludedClassifications(): Promise<string[]> {
|
async function getIncludedClassificationIds(): Promise<number[]> {
|
||||||
try {
|
try {
|
||||||
const result = await postgresClient.query(
|
const result = await postgresClient.query(
|
||||||
`SELECT setting_value FROM kiosk_settings WHERE setting_key = 'excluded_classifications'`
|
`SELECT setting_value FROM kiosk_settings WHERE setting_key = 'included_classifications'`
|
||||||
);
|
);
|
||||||
const value = result.rows[0]?.setting_value || '';
|
const value = result.rows[0]?.setting_value || '';
|
||||||
return value ? value.split(',').map((c: string) => c.trim()).filter(Boolean) : [];
|
return value ? value.split(',').map((c: string) => parseInt(c.trim(), 10)).filter((n: number) => !isNaN(n)) : [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching excluded classifications:', error);
|
console.error('Error fetching included classifications:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -31,19 +31,18 @@ export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Get excluded company IDs (co-managed clients) and classifications
|
// Get excluded company IDs (co-managed clients) and classifications
|
||||||
const excludedCompanyIds = await getExcludedCompanyIds();
|
const excludedCompanyIds = await getExcludedCompanyIds();
|
||||||
const excludedClassifications = await getExcludedClassifications();
|
const includedClassificationIds = await getIncludedClassificationIds();
|
||||||
|
|
||||||
// Build company exclusion filter
|
// Build company filter: only show recurring revenue classification clients
|
||||||
let excludeCompanyFilter = '';
|
let excludeCompanyFilter = '';
|
||||||
if (excludedCompanyIds.length > 0 || excludedClassifications.length > 0) {
|
const conditions: string[] = [];
|
||||||
const conditions = [];
|
if (excludedCompanyIds.length > 0) {
|
||||||
if (excludedCompanyIds.length > 0) {
|
conditions.push(`t.company_id NOT IN (${excludedCompanyIds.join(',')})`);
|
||||||
conditions.push(`t.company_id NOT IN (${excludedCompanyIds.join(',')})`);
|
}
|
||||||
}
|
if (includedClassificationIds.length > 0) {
|
||||||
if (excludedClassifications.length > 0) {
|
conditions.push(`t.company_id IN (SELECT id FROM companies WHERE classification::integer IN (${includedClassificationIds.join(',')}))`);
|
||||||
const classificationList = excludedClassifications.map(c => `'${c.replace(/'/g, "''")}'`).join(',');
|
}
|
||||||
conditions.push(`t.company_id NOT IN (SELECT id FROM companies WHERE classification IN (${classificationList}))`);
|
if (conditions.length > 0) {
|
||||||
}
|
|
||||||
excludeCompanyFilter = `AND (${conditions.join(' AND ')})`;
|
excludeCompanyFilter = `AND (${conditions.join(' AND ')})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,15 @@ async function getExcludedCompanyIds(): Promise<number[]> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getExcludedClassifications(): Promise<string[]> {
|
async function getIncludedClassificationIds(): Promise<number[]> {
|
||||||
try {
|
try {
|
||||||
const result = await postgresClient.query(
|
const result = await postgresClient.query(
|
||||||
`SELECT setting_value FROM kiosk_settings WHERE setting_key = 'excluded_classifications'`
|
`SELECT setting_value FROM kiosk_settings WHERE setting_key = 'included_classifications'`
|
||||||
);
|
);
|
||||||
const value = result.rows[0]?.setting_value || '';
|
const value = result.rows[0]?.setting_value || '';
|
||||||
return value ? value.split(',').map((c: string) => c.trim()).filter(Boolean) : [];
|
return value ? value.split(',').map((c: string) => parseInt(c.trim(), 10)).filter((n: number) => !isNaN(n)) : [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching excluded classifications:', error);
|
console.error('Error fetching included classifications:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -31,19 +31,18 @@ export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Get excluded company IDs (co-managed clients)
|
// Get excluded company IDs (co-managed clients)
|
||||||
const excludedCompanyIds = await getExcludedCompanyIds();
|
const excludedCompanyIds = await getExcludedCompanyIds();
|
||||||
const excludedClassifications = await getExcludedClassifications();
|
const includedClassificationIds = await getIncludedClassificationIds();
|
||||||
|
|
||||||
// Build company exclusion filter
|
// Build company filter: only show recurring revenue classification clients
|
||||||
let excludeCompanyFilter = '';
|
let excludeCompanyFilter = '';
|
||||||
if (excludedCompanyIds.length > 0 || excludedClassifications.length > 0) {
|
const conditions: string[] = [];
|
||||||
const conditions = [];
|
if (excludedCompanyIds.length > 0) {
|
||||||
if (excludedCompanyIds.length > 0) {
|
conditions.push(`company_id NOT IN (${excludedCompanyIds.join(',')})`);
|
||||||
conditions.push(`company_id NOT IN (${excludedCompanyIds.join(',')})`);
|
}
|
||||||
}
|
if (includedClassificationIds.length > 0) {
|
||||||
if (excludedClassifications.length > 0) {
|
conditions.push(`company_id IN (SELECT id FROM companies WHERE classification::integer IN (${includedClassificationIds.join(',')}))`);
|
||||||
const classificationList = excludedClassifications.map(c => `'${c.replace(/'/g, "''")}'`).join(',');
|
}
|
||||||
conditions.push(`company_id NOT IN (SELECT id FROM companies WHERE classification IN (${classificationList}))`);
|
if (conditions.length > 0) {
|
||||||
}
|
|
||||||
excludeCompanyFilter = `AND (${conditions.join(' AND ')})`;
|
excludeCompanyFilter = `AND (${conditions.join(' AND ')})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,26 @@ const SOURCE_MAP: Record<number, string> = {
|
||||||
36: 'DeskDirector', 38: 'Huntress', 39: 'Blumira', 40: 'SentinelOne',
|
36: 'DeskDirector', 38: 'Huntress', 39: 'Blumira', 40: 'SentinelOne',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CLASSIFICATION_MAP: Record<number, { label: string; cls: string }> = {
|
||||||
|
5: { label: 'Block Hour', cls: 'bg-sky-500/15 text-sky-600 border border-sky-500/30' },
|
||||||
|
9: { label: 'Canceled', cls: 'bg-slate-500/15 text-slate-500 border border-slate-500/30' },
|
||||||
|
202: { label: 'Co-Managed', cls: 'bg-cyan-500/15 text-cyan-600 border border-cyan-500/30' },
|
||||||
|
16: { label: 'Gold (Legacy)', cls: 'bg-yellow-500/15 text-yellow-600 border border-yellow-500/30' },
|
||||||
|
206: { label: 'IT Complete w/ Gold Security', cls: 'bg-amber-500/15 text-amber-600 border border-amber-500/30' },
|
||||||
|
207: { label: 'IT Core / Silver Security', cls: 'bg-blue-500/15 text-blue-600 border border-blue-500/30' },
|
||||||
|
203: { label: 'IT Foundation / Bronze Security', cls: 'bg-orange-500/15 text-orange-600 border border-orange-500/30' },
|
||||||
|
205: { label: 'IT Premier / Platinum Security', cls: 'bg-violet-500/15 text-violet-600 border border-violet-500/30' },
|
||||||
|
14: { label: 'Jeopardy Company', cls: 'bg-red-500/15 text-red-600 border border-red-500/30' },
|
||||||
|
201: { label: 'Partner', cls: 'bg-purple-500/15 text-purple-600 border border-purple-500/30' },
|
||||||
|
15: { label: 'Platinum (Legacy)', cls: 'bg-violet-500/15 text-violet-600 border border-violet-500/30' },
|
||||||
|
13: { label: 'Residential (no-pay)', cls: 'bg-slate-500/15 text-slate-500 border border-slate-500/30' },
|
||||||
|
17: { label: 'Silver (Legacy)', cls: 'bg-zinc-500/15 text-zinc-600 border border-zinc-500/30' },
|
||||||
|
12: { label: 'T&M', cls: 'bg-teal-500/15 text-teal-600 border border-teal-500/30' },
|
||||||
|
7: { label: 'Target', cls: 'bg-emerald-500/15 text-emerald-600 border border-emerald-500/30' },
|
||||||
|
200: { label: 'Tools Only', cls: 'bg-slate-500/15 text-slate-500 border border-slate-500/30' },
|
||||||
|
18: { label: 'Bronze (Legacy)', cls: 'bg-orange-500/15 text-orange-600 border border-orange-500/30' },
|
||||||
|
};
|
||||||
|
|
||||||
const COMPANY_TYPE_MAP: Record<number, { label: string; cls: string }> = {
|
const COMPANY_TYPE_MAP: Record<number, { label: string; cls: string }> = {
|
||||||
1: { label: 'Customer', cls: 'bg-green-500/15 text-green-600 border border-green-500/30' },
|
1: { label: 'Customer', cls: 'bg-green-500/15 text-green-600 border border-green-500/30' },
|
||||||
2: { label: 'Lead', cls: 'bg-blue-500/15 text-blue-600 border border-blue-500/30' },
|
2: { label: 'Lead', cls: 'bg-blue-500/15 text-blue-600 border border-blue-500/30' },
|
||||||
|
|
@ -73,7 +93,7 @@ interface Lookups {
|
||||||
|
|
||||||
// ── Field metadata for formatted view ─────────────────────────────────────────
|
// ── Field metadata for formatted view ─────────────────────────────────────────
|
||||||
|
|
||||||
type FieldType = 'date' | 'bool' | 'status' | 'priority' | 'source' | 'queue' | 'company_type' | 'url' | 'phone' | 'hours' | 'id' | 'resource' | 'company' | 'issue_type' | 'sub_issue_type' | 'config_item';
|
type FieldType = 'date' | 'bool' | 'status' | 'priority' | 'source' | 'queue' | 'company_type' | 'classification' | 'url' | 'phone' | 'hours' | 'id' | 'resource' | 'company' | 'issue_type' | 'sub_issue_type' | 'config_item';
|
||||||
|
|
||||||
type FieldGroup = {
|
type FieldGroup = {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -130,6 +150,7 @@ const COMPANY_GROUPS: FieldGroup[] = [
|
||||||
{ key: 'company_name', label: 'Company Name' },
|
{ key: 'company_name', label: 'Company Name' },
|
||||||
{ key: 'company_number', label: 'Company #' },
|
{ key: 'company_number', label: 'Company #' },
|
||||||
{ key: 'company_type', label: 'Type', type: 'company_type' },
|
{ key: 'company_type', label: 'Type', type: 'company_type' },
|
||||||
|
{ key: 'classification', label: 'Classification', type: 'classification' },
|
||||||
{ key: 'is_active', label: 'Active', type: 'bool' },
|
{ key: 'is_active', label: 'Active', type: 'bool' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -219,6 +240,10 @@ function resolveLabel(key: string, value: any, type: FieldType | undefined, look
|
||||||
const ct = COMPANY_TYPE_MAP[Number(value)];
|
const ct = COMPANY_TYPE_MAP[Number(value)];
|
||||||
return { display: <ColorBadge cls={ct?.cls ?? 'bg-muted text-muted-foreground border border-border'}>{ct?.label ?? `Type ${value}`}</ColorBadge>, isEmpty: false };
|
return { display: <ColorBadge cls={ct?.cls ?? 'bg-muted text-muted-foreground border border-border'}>{ct?.label ?? `Type ${value}`}</ColorBadge>, isEmpty: false };
|
||||||
}
|
}
|
||||||
|
case 'classification': {
|
||||||
|
const cl = CLASSIFICATION_MAP[Number(value)];
|
||||||
|
return { display: <ColorBadge cls={cl?.cls ?? 'bg-muted text-muted-foreground border border-border'}>{cl?.label ?? `Classification ${value}`}</ColorBadge>, isEmpty: false };
|
||||||
|
}
|
||||||
case 'resource': {
|
case 'resource': {
|
||||||
const name = lookups.resources[Number(value)];
|
const name = lookups.resources[Number(value)];
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue