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:
parent
62ec703f81
commit
1d99a8f659
4 changed files with 176 additions and 10 deletions
|
|
@ -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) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Get excluded company IDs (co-managed clients)
|
// Get excluded company IDs (co-managed clients) and classifications
|
||||||
const excludedCompanyIds = await getExcludedCompanyIds();
|
const excludedCompanyIds = await getExcludedCompanyIds();
|
||||||
const excludeCompanyFilter = excludedCompanyIds.length > 0
|
const excludedClassifications = await getExcludedClassifications();
|
||||||
? `AND t.company_id NOT IN (${excludedCompanyIds.join(',')})`
|
|
||||||
: '';
|
// 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(
|
const activityResult = await postgresClient.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
t.ticket_number,
|
t.ticket_number,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
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 excludeCompanyFilter = excludedCompanyIds.length > 0
|
const excludedClassifications = await getExcludedClassifications();
|
||||||
? `AND company_id NOT IN (${excludedCompanyIds.join(',')})`
|
|
||||||
: '';
|
// 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(
|
const criticalTicketsResult = await postgresClient.query(
|
||||||
`SELECT COUNT(*) as count
|
`SELECT COUNT(*) as count
|
||||||
FROM tickets
|
FROM tickets
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ interface Company {
|
||||||
|
|
||||||
interface KioskSettings {
|
interface KioskSettings {
|
||||||
excluded_company_ids: number[];
|
excluded_company_ids: number[];
|
||||||
|
excluded_classifications: string[];
|
||||||
cycle_interval: number;
|
cycle_interval: number;
|
||||||
refresh_interval: number;
|
refresh_interval: number;
|
||||||
show_rmm_alerts: boolean;
|
show_rmm_alerts: boolean;
|
||||||
|
|
@ -21,6 +22,7 @@ export default function KioskSettingsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [settings, setSettings] = useState<KioskSettings>({
|
const [settings, setSettings] = useState<KioskSettings>({
|
||||||
excluded_company_ids: [],
|
excluded_company_ids: [],
|
||||||
|
excluded_classifications: [],
|
||||||
cycle_interval: 7,
|
cycle_interval: 7,
|
||||||
refresh_interval: 60,
|
refresh_interval: 60,
|
||||||
show_rmm_alerts: false,
|
show_rmm_alerts: false,
|
||||||
|
|
@ -99,6 +101,14 @@ export default function KioskSettingsPage() {
|
||||||
setting_value: settings.excluded_company_ids,
|
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', {
|
fetch('/api/kiosk/settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|
@ -135,7 +145,9 @@ export default function KioskSettingsPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCompanyName = (companyId: number) => {
|
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) {
|
if (loading) {
|
||||||
|
|
@ -232,6 +244,96 @@ export default function KioskSettingsPage() {
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Display Settings */}
|
||||||
<div className="bg-gray-900 rounded-lg p-6 border border-gray-800">
|
<div className="bg-gray-900 rounded-lg p-6 border border-gray-800">
|
||||||
<h2 className="text-xl font-semibold mb-4">Display Settings</h2>
|
<h2 className="text-xl font-semibold mb-4">Display Settings</h2>
|
||||||
|
|
|
||||||
14
migrations/020_add_company_classification.sql
Normal file
14
migrations/020_add_company_classification.sql
Normal 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)';
|
||||||
Loading…
Add table
Add a link
Reference in a new issue