264 lines
10 KiB
TypeScript
264 lines
10 KiB
TypeScript
|
|
/* Centralized status / priority / classification color registry.
|
||
|
|
*
|
||
|
|
* Lifts the hard-coded maps from components/admin/DetailModal.tsx so the
|
||
|
|
* same labels and colors can be rendered anywhere via <StatusBadge />.
|
||
|
|
*
|
||
|
|
* Two axes:
|
||
|
|
* • TONE — semantic state (ok / warn / error / info / accent / neutral / pending / inactive)
|
||
|
|
* • PALETTE — arbitrary categorical hues used when there is no semantic
|
||
|
|
* intent (e.g. classifications, sources, queue colors)
|
||
|
|
*
|
||
|
|
* Components consume `BadgeProps` returned by the helpers below.
|
||
|
|
*
|
||
|
|
* The class strings here use Tailwind v4 named-palette colors at /15
|
||
|
|
* background and -600/-700 text; the recipe matches the existing app
|
||
|
|
* style and reads correctly on both light and dark surfaces. */
|
||
|
|
|
||
|
|
export type StatusTone =
|
||
|
|
| 'ok'
|
||
|
|
| 'warn'
|
||
|
|
| 'error'
|
||
|
|
| 'info'
|
||
|
|
| 'accent'
|
||
|
|
| 'neutral'
|
||
|
|
| 'pending'
|
||
|
|
| 'inactive';
|
||
|
|
|
||
|
|
export type PaletteHue =
|
||
|
|
| 'red'
|
||
|
|
| 'orange'
|
||
|
|
| 'amber'
|
||
|
|
| 'yellow'
|
||
|
|
| 'green'
|
||
|
|
| 'emerald'
|
||
|
|
| 'sky'
|
||
|
|
| 'blue'
|
||
|
|
| 'cyan'
|
||
|
|
| 'teal'
|
||
|
|
| 'indigo'
|
||
|
|
| 'violet'
|
||
|
|
| 'purple'
|
||
|
|
| 'slate'
|
||
|
|
| 'zinc';
|
||
|
|
|
||
|
|
export interface BadgeProps {
|
||
|
|
label: string;
|
||
|
|
/** className snippet for the badge body — pass through to <StatusBadge /> */
|
||
|
|
variantClass: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Tone → Tailwind classes ───────────────────────────────────────── */
|
||
|
|
|
||
|
|
export const TONE_CLASS: Record<StatusTone, string> = {
|
||
|
|
ok: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400',
|
||
|
|
warn: 'bg-amber-500/15 text-amber-700 dark:text-amber-400',
|
||
|
|
error: 'bg-destructive/15 text-destructive',
|
||
|
|
info: 'bg-primary/15 text-primary',
|
||
|
|
accent: 'bg-primary/15 text-primary',
|
||
|
|
neutral: 'bg-slate-500/15 text-slate-700 dark:text-slate-300',
|
||
|
|
pending: 'bg-sky-500/15 text-sky-700 dark:text-sky-400',
|
||
|
|
inactive: 'bg-muted text-muted-foreground',
|
||
|
|
};
|
||
|
|
|
||
|
|
export const PALETTE_CLASS: Record<PaletteHue, string> = {
|
||
|
|
red: 'bg-red-500/15 text-red-700 dark:text-red-400',
|
||
|
|
orange: 'bg-orange-500/15 text-orange-700 dark:text-orange-400',
|
||
|
|
amber: 'bg-amber-500/15 text-amber-700 dark:text-amber-400',
|
||
|
|
yellow: 'bg-yellow-500/15 text-yellow-700 dark:text-yellow-400',
|
||
|
|
green: 'bg-green-500/15 text-green-700 dark:text-green-400',
|
||
|
|
emerald: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400',
|
||
|
|
sky: 'bg-sky-500/15 text-sky-700 dark:text-sky-400',
|
||
|
|
blue: 'bg-blue-500/15 text-blue-700 dark:text-blue-400',
|
||
|
|
cyan: 'bg-cyan-500/15 text-cyan-700 dark:text-cyan-400',
|
||
|
|
teal: 'bg-teal-500/15 text-teal-700 dark:text-teal-400',
|
||
|
|
indigo: 'bg-indigo-500/15 text-indigo-700 dark:text-indigo-400',
|
||
|
|
violet: 'bg-violet-500/15 text-violet-700 dark:text-violet-400',
|
||
|
|
purple: 'bg-purple-500/15 text-purple-700 dark:text-purple-400',
|
||
|
|
slate: 'bg-slate-500/15 text-slate-700 dark:text-slate-300',
|
||
|
|
zinc: 'bg-zinc-500/15 text-zinc-700 dark:text-zinc-300',
|
||
|
|
};
|
||
|
|
|
||
|
|
const NEUTRAL_BADGE: BadgeProps = {
|
||
|
|
label: '—',
|
||
|
|
variantClass: TONE_CLASS.inactive,
|
||
|
|
};
|
||
|
|
|
||
|
|
/* ── Priorities (Autotask numeric IDs) ────────────────────────────── */
|
||
|
|
|
||
|
|
const PRIORITY_REGISTRY: Record<number, { label: string; tone: StatusTone }> = {
|
||
|
|
2: { label: 'Critical', tone: 'error' },
|
||
|
|
3: { label: 'High', tone: 'warn' },
|
||
|
|
4: { label: 'Medium', tone: 'pending' },
|
||
|
|
6: { label: 'Low', tone: 'info' },
|
||
|
|
7: { label: 'Very Low', tone: 'neutral' },
|
||
|
|
// Mirrored entries from the older picklist (Autotask renumbered)
|
||
|
|
8: { label: 'Critical', tone: 'error' },
|
||
|
|
9: { label: 'High', tone: 'warn' },
|
||
|
|
10: { label: 'Medium', tone: 'pending' },
|
||
|
|
11: { label: 'Low', tone: 'info' },
|
||
|
|
};
|
||
|
|
|
||
|
|
export function priorityBadge(id: number | null | undefined): BadgeProps {
|
||
|
|
if (id == null) return NEUTRAL_BADGE;
|
||
|
|
const hit = PRIORITY_REGISTRY[Number(id)];
|
||
|
|
if (!hit) return { label: `Priority ${id}`, variantClass: TONE_CLASS.inactive };
|
||
|
|
return { label: hit.label, variantClass: TONE_CLASS[hit.tone] };
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Ticket statuses (string label keyed) ─────────────────────────── */
|
||
|
|
|
||
|
|
const TICKET_STATUS_REGISTRY: Record<string, StatusTone> = {
|
||
|
|
'New': 'info',
|
||
|
|
'In Progress': 'pending',
|
||
|
|
'Complete': 'ok',
|
||
|
|
'Resolved <CSAT Survey>': 'ok',
|
||
|
|
'Waiting Customer': 'warn',
|
||
|
|
'Waiting Materials': 'warn',
|
||
|
|
'Waiting Vendor': 'warn',
|
||
|
|
'Waiting Approval': 'warn',
|
||
|
|
'On Hold': 'neutral',
|
||
|
|
'Escalate': 'error',
|
||
|
|
'Escalate to Wulf': 'error',
|
||
|
|
'Escalate to MC': 'error',
|
||
|
|
'Resource Assigned': 'pending',
|
||
|
|
'Service Call Scheduled': 'pending',
|
||
|
|
'Dispatched': 'pending',
|
||
|
|
};
|
||
|
|
|
||
|
|
export function ticketStatusBadge(label: string | null | undefined): BadgeProps {
|
||
|
|
if (!label) return NEUTRAL_BADGE;
|
||
|
|
const tone = TICKET_STATUS_REGISTRY[label] ?? 'inactive';
|
||
|
|
return { label, variantClass: TONE_CLASS[tone] };
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Sources (Autotask) ───────────────────────────────────────────── */
|
||
|
|
|
||
|
|
const SOURCE_REGISTRY: Record<number, string> = {
|
||
|
|
[-2]: 'System',
|
||
|
|
[-1]: 'Internal',
|
||
|
|
1: 'Phone',
|
||
|
|
2: 'Email',
|
||
|
|
4: 'Web Portal',
|
||
|
|
6: 'Monitoring Alert',
|
||
|
|
8: 'RMM Alert',
|
||
|
|
17: 'Chat',
|
||
|
|
21: 'API',
|
||
|
|
22: 'Automation',
|
||
|
|
27: 'In Person',
|
||
|
|
29: 'Client Portal',
|
||
|
|
30: 'Microsoft Teams',
|
||
|
|
31: 'Webhook',
|
||
|
|
33: 'Datto RMM',
|
||
|
|
34: 'Rewst',
|
||
|
|
35: 'TimeZest',
|
||
|
|
36: 'DeskDirector',
|
||
|
|
38: 'Huntress',
|
||
|
|
39: 'Blumira',
|
||
|
|
40: 'SentinelOne',
|
||
|
|
};
|
||
|
|
|
||
|
|
export function sourceBadge(id: number | null | undefined): BadgeProps {
|
||
|
|
if (id == null) return NEUTRAL_BADGE;
|
||
|
|
const label = SOURCE_REGISTRY[Number(id)] ?? `Source ${id}`;
|
||
|
|
return { label, variantClass: PALETTE_CLASS.violet };
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Classification (Autotask) ────────────────────────────────────── */
|
||
|
|
|
||
|
|
const CLASSIFICATION_REGISTRY: Record<number, { label: string; hue: PaletteHue }> = {
|
||
|
|
5: { label: 'Block Hour', hue: 'sky' },
|
||
|
|
9: { label: 'Canceled', hue: 'slate' },
|
||
|
|
202: { label: 'Co-Managed', hue: 'cyan' },
|
||
|
|
16: { label: 'Gold (Legacy)', hue: 'yellow' },
|
||
|
|
206: { label: 'IT Complete w/ Gold Security', hue: 'amber' },
|
||
|
|
207: { label: 'IT Core / Silver Security', hue: 'blue' },
|
||
|
|
203: { label: 'IT Foundation / Bronze Security', hue: 'orange' },
|
||
|
|
205: { label: 'IT Premier / Platinum Security', hue: 'violet' },
|
||
|
|
14: { label: 'Jeopardy Company', hue: 'red' },
|
||
|
|
201: { label: 'Partner', hue: 'purple' },
|
||
|
|
15: { label: 'Platinum (Legacy)', hue: 'violet' },
|
||
|
|
13: { label: 'Residential (no-pay)', hue: 'slate' },
|
||
|
|
17: { label: 'Silver (Legacy)', hue: 'zinc' },
|
||
|
|
12: { label: 'T&M', hue: 'teal' },
|
||
|
|
7: { label: 'Target', hue: 'emerald' },
|
||
|
|
200: { label: 'Tools Only', hue: 'slate' },
|
||
|
|
18: { label: 'Bronze (Legacy)', hue: 'orange' },
|
||
|
|
};
|
||
|
|
|
||
|
|
export function classificationBadge(id: number | null | undefined): BadgeProps {
|
||
|
|
if (id == null) return NEUTRAL_BADGE;
|
||
|
|
const hit = CLASSIFICATION_REGISTRY[Number(id)];
|
||
|
|
if (!hit) return { label: `Classification ${id}`, variantClass: TONE_CLASS.inactive };
|
||
|
|
return { label: hit.label, variantClass: PALETTE_CLASS[hit.hue] };
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Company type (Autotask) ──────────────────────────────────────── */
|
||
|
|
|
||
|
|
const COMPANY_TYPE_REGISTRY: Record<number, { label: string; hue: PaletteHue }> = {
|
||
|
|
1: { label: 'Customer', hue: 'green' },
|
||
|
|
2: { label: 'Lead', hue: 'blue' },
|
||
|
|
3: { label: 'Prospect', hue: 'purple' },
|
||
|
|
4: { label: 'Dead', hue: 'slate' },
|
||
|
|
6: { label: 'Cancelation', hue: 'red' },
|
||
|
|
7: { label: 'Vendor', hue: 'orange' },
|
||
|
|
8: { label: 'Partner', hue: 'cyan' },
|
||
|
|
};
|
||
|
|
|
||
|
|
export function companyTypeBadge(id: number | null | undefined): BadgeProps {
|
||
|
|
if (id == null) return NEUTRAL_BADGE;
|
||
|
|
const hit = COMPANY_TYPE_REGISTRY[Number(id)];
|
||
|
|
if (!hit) return { label: `Type ${id}`, variantClass: TONE_CLASS.inactive };
|
||
|
|
return { label: hit.label, variantClass: PALETTE_CLASS[hit.hue] };
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Note publish levels (Autotask) ──────────────────────────────── */
|
||
|
|
|
||
|
|
const PUBLISH_REGISTRY: Record<number, { label: string; tone: StatusTone }> = {
|
||
|
|
1: { label: 'All Users', tone: 'ok' },
|
||
|
|
2: { label: 'Internal', tone: 'warn' },
|
||
|
|
4: { label: 'Internal Only', tone: 'neutral' },
|
||
|
|
};
|
||
|
|
|
||
|
|
export function publishBadge(id: number | null | undefined): BadgeProps {
|
||
|
|
if (id == null) return NEUTRAL_BADGE;
|
||
|
|
const hit = PUBLISH_REGISTRY[Number(id)];
|
||
|
|
if (!hit) return { label: `Publish ${id}`, variantClass: TONE_CLASS.inactive };
|
||
|
|
return { label: hit.label, variantClass: TONE_CLASS[hit.tone] };
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Boolean badges (active / billable / approved / yes / no) ────── */
|
||
|
|
|
||
|
|
export function activeBadge(active: boolean | null | undefined): BadgeProps {
|
||
|
|
if (active == null) return NEUTRAL_BADGE;
|
||
|
|
return active
|
||
|
|
? { label: 'Active', variantClass: TONE_CLASS.ok }
|
||
|
|
: { label: 'Inactive', variantClass: TONE_CLASS.inactive };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function yesNoBadge(value: boolean | null | undefined): BadgeProps {
|
||
|
|
if (value == null) return NEUTRAL_BADGE;
|
||
|
|
return value
|
||
|
|
? { label: 'Yes', variantClass: TONE_CLASS.ok }
|
||
|
|
: { label: 'No', variantClass: TONE_CLASS.inactive };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function billableBadge(value: boolean | null | undefined): BadgeProps {
|
||
|
|
if (!value) return NEUTRAL_BADGE;
|
||
|
|
return { label: 'Billable', variantClass: TONE_CLASS.ok };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function approvedBadge(value: boolean | null | undefined): BadgeProps {
|
||
|
|
if (!value) return NEUTRAL_BADGE;
|
||
|
|
return { label: 'Approved', variantClass: TONE_CLASS.info };
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Generic helpers (for inline use without registry entry) ─────── */
|
||
|
|
|
||
|
|
export function toneClass(tone: StatusTone): string {
|
||
|
|
return TONE_CLASS[tone];
|
||
|
|
}
|
||
|
|
|
||
|
|
export function paletteClass(hue: PaletteHue): string {
|
||
|
|
return PALETTE_CLASS[hue];
|
||
|
|
}
|