feat: add classification-based filtering and fix company name display

- Added classification column to companies table
- Created excluded_classifications setting (default: Tools Only)
- Updated kiosk stats and activity APIs to filter by classification
- Added comprehensive classification filtering UI with checkboxes
- Support for common classifications (Tools Only, Wulff Consulting Client, Customer)
- Allow custom classification entry
- Fixed company name display in settings (convert string IDs properly)
- Classification filtering works alongside company ID exclusions
This commit is contained in:
root 2026-02-03 09:17:07 -05:00
parent 62ec703f81
commit 1d99a8f659
4 changed files with 176 additions and 10 deletions

View file

@ -14,15 +14,40 @@ async function getExcludedCompanyIds(): Promise<number[]> {
}
}
async function getExcludedClassifications(): Promise<string[]> {
try {
const result = await postgresClient.query(
`SELECT setting_value FROM kiosk_settings WHERE setting_key = 'excluded_classifications'`
);
const value = result.rows[0]?.setting_value || '';
return value ? value.split(',').map((c: string) => c.trim()).filter(Boolean) : [];
} catch (error) {
console.error('Error fetching excluded classifications:', error);
return [];
}
}
export async function GET(request: NextRequest) {
try {
// Get excluded company IDs (co-managed clients)
// Get excluded company IDs (co-managed clients) and classifications
const excludedCompanyIds = await getExcludedCompanyIds();
const excludeCompanyFilter = excludedCompanyIds.length > 0
? `AND t.company_id NOT IN (${excludedCompanyIds.join(',')})`
: '';
const excludedClassifications = await getExcludedClassifications();
// Build company exclusion filter
let excludeCompanyFilter = '';
if (excludedCompanyIds.length > 0 || excludedClassifications.length > 0) {
const conditions = [];
if (excludedCompanyIds.length > 0) {
conditions.push(`t.company_id NOT IN (${excludedCompanyIds.join(',')})`);
}
if (excludedClassifications.length > 0) {
const classificationList = excludedClassifications.map(c => `'${c.replace(/'/g, "''")}'`).join(',');
conditions.push(`t.company_id NOT IN (SELECT id FROM companies WHERE classification IN (${classificationList}))`);
}
excludeCompanyFilter = `AND (${conditions.join(' AND ')})`;
}
// Get recent ticket activity for ticker feed - exclude RMM alerts (source = 8) and co-managed clients
// Get recent ticket activity for ticker feed - exclude RMM alerts (source = 8), co-managed clients, and excluded classifications
const activityResult = await postgresClient.query(
`SELECT
t.ticket_number,

View file

@ -14,15 +14,40 @@ async function getExcludedCompanyIds(): Promise<number[]> {
}
}
async function getExcludedClassifications(): Promise<string[]> {
try {
const result = await postgresClient.query(
`SELECT setting_value FROM kiosk_settings WHERE setting_key = 'excluded_classifications'`
);
const value = result.rows[0]?.setting_value || '';
return value ? value.split(',').map((c: string) => c.trim()).filter(Boolean) : [];
} catch (error) {
console.error('Error fetching excluded classifications:', error);
return [];
}
}
export async function GET(request: NextRequest) {
try {
// Get excluded company IDs (co-managed clients)
const excludedCompanyIds = await getExcludedCompanyIds();
const excludeCompanyFilter = excludedCompanyIds.length > 0
? `AND company_id NOT IN (${excludedCompanyIds.join(',')})`
: '';
const excludedClassifications = await getExcludedClassifications();
// Build company exclusion filter
let excludeCompanyFilter = '';
if (excludedCompanyIds.length > 0 || excludedClassifications.length > 0) {
const conditions = [];
if (excludedCompanyIds.length > 0) {
conditions.push(`company_id NOT IN (${excludedCompanyIds.join(',')})`);
}
if (excludedClassifications.length > 0) {
const classificationList = excludedClassifications.map(c => `'${c.replace(/'/g, "''")}'`).join(',');
conditions.push(`company_id NOT IN (SELECT id FROM companies WHERE classification IN (${classificationList}))`);
}
excludeCompanyFilter = `AND (${conditions.join(' AND ')})`;
}
// Critical tickets (Priority 1-3) - exclude RMM alerts (source = 8) and co-managed clients
// Critical tickets (Priority 1-3) - exclude RMM alerts (source = 8), co-managed clients, and excluded classifications
const criticalTicketsResult = await postgresClient.query(
`SELECT COUNT(*) as count
FROM tickets

View file

@ -12,6 +12,7 @@ interface Company {
interface KioskSettings {
excluded_company_ids: number[];
excluded_classifications: string[];
cycle_interval: number;
refresh_interval: number;
show_rmm_alerts: boolean;
@ -21,6 +22,7 @@ export default function KioskSettingsPage() {
const router = useRouter();
const [settings, setSettings] = useState<KioskSettings>({
excluded_company_ids: [],
excluded_classifications: [],
cycle_interval: 7,
refresh_interval: 60,
show_rmm_alerts: false,
@ -99,6 +101,14 @@ export default function KioskSettingsPage() {
setting_value: settings.excluded_company_ids,
}),
}),
fetch('/api/kiosk/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
setting_key: 'excluded_classifications',
setting_value: settings.excluded_classifications,
}),
}),
fetch('/api/kiosk/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -135,7 +145,9 @@ export default function KioskSettingsPage() {
};
const getCompanyName = (companyId: number) => {
return companies.find(c => c.id === companyId)?.companyName || `Company ${companyId}`;
// API returns id as string, settings stores as number - convert for comparison
const idStr = companyId.toString();
return companies.find(c => c.id.toString() === idStr)?.companyName || `Company ${companyId}`;
};
if (loading) {
@ -232,6 +244,96 @@ export default function KioskSettingsPage() {
</div>
</div>
{/* Excluded Classifications Section */}
<div className="bg-gray-900 rounded-lg p-6 border border-gray-800">
<h2 className="text-xl font-semibold mb-4">Excluded Company Classifications</h2>
<p className="text-gray-400 mb-4">
Tickets from companies with these classifications will not appear in the kiosk display.
</p>
{/* Common Classifications */}
<div className="space-y-2 mb-4">
{['Tools Only', 'Wulff Consulting Client', 'Customer'].map(classification => (
<label key={classification} className="flex items-center gap-3 bg-gray-800 rounded px-4 py-3 border border-gray-700 cursor-pointer hover:bg-gray-750">
<input
type="checkbox"
checked={settings.excluded_classifications.includes(classification)}
onChange={(e) => {
if (e.target.checked) {
setSettings({
...settings,
excluded_classifications: [...settings.excluded_classifications, classification],
});
} else {
setSettings({
...settings,
excluded_classifications: settings.excluded_classifications.filter(c => c !== classification),
});
}
}}
className="w-5 h-5 rounded border-gray-600 text-blue-600 focus:ring-blue-500"
/>
<span className="flex-1">{classification}</span>
</label>
))}
</div>
{/* Custom Classification Input */}
<div className="mt-4">
<label className="block text-sm font-medium mb-2 text-gray-400">
Add custom classification:
</label>
<div className="flex gap-2">
<input
type="text"
placeholder="Enter classification name..."
className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white"
onKeyPress={(e) => {
if (e.key === 'Enter') {
const value = (e.target as HTMLInputElement).value.trim();
if (value && !settings.excluded_classifications.includes(value)) {
setSettings({
...settings,
excluded_classifications: [...settings.excluded_classifications, value],
});
(e.target as HTMLInputElement).value = '';
}
}
}}
/>
</div>
<p className="text-xs text-gray-500 mt-1">Press Enter to add</p>
</div>
{/* Custom Classifications List */}
{settings.excluded_classifications.filter(c => !['Tools Only', 'Wulff Consulting Client', 'Customer'].includes(c)).length > 0 && (
<div className="mt-4 space-y-2">
<div className="text-sm font-medium text-gray-400">Custom classifications:</div>
{settings.excluded_classifications
.filter(c => !['Tools Only', 'Wulff Consulting Client', 'Customer'].includes(c))
.map(classification => (
<div
key={classification}
className="flex items-center justify-between bg-gray-800 rounded px-4 py-3 border border-gray-700"
>
<span>{classification}</span>
<Button
variant="ghost"
size="sm"
onClick={() => setSettings({
...settings,
excluded_classifications: settings.excluded_classifications.filter(c => c !== classification),
})}
className="text-red-400 hover:text-red-300 hover:bg-red-900/20"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
))}
</div>
)}
</div>
{/* Display Settings */}
<div className="bg-gray-900 rounded-lg p-6 border border-gray-800">
<h2 className="text-xl font-semibold mb-4">Display Settings</h2>

View file

@ -0,0 +1,14 @@
-- Add classification column to companies table
-- This will store the classification name (e.g., "Tools Only", "Wulff Consulting Client", "Customer")
ALTER TABLE companies ADD COLUMN IF NOT EXISTS classification VARCHAR(255);
-- Create index for faster filtering
CREATE INDEX IF NOT EXISTS idx_companies_classification ON companies(classification);
-- Add classification to kiosk settings
INSERT INTO kiosk_settings (setting_key, setting_value, description) VALUES
('excluded_classifications', 'Tools Only', 'Comma-separated list of company classifications to exclude from kiosk')
ON CONFLICT (setting_key) DO NOTHING;
-- Add comment
COMMENT ON COLUMN companies.classification IS 'Company classification (e.g., Tools Only, Wulff Consulting Client, Customer)';