166 lines
5.1 KiB
TypeScript
166 lines
5.1 KiB
TypeScript
/**
|
|
* QuickActionBoard — reusable card grid for feature dashboards.
|
|
*
|
|
* Renders a set of Quick Action cards grouped by category.
|
|
* Each card dispatches via the onDispatch callback; the parent
|
|
* decides what happens (navigate to workspace, trigger workflow, etc.).
|
|
*/
|
|
|
|
import React from 'react';
|
|
import styles from './QuickActionBoard.module.css';
|
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
|
|
|
// ============================================================================
|
|
// TYPES
|
|
// ============================================================================
|
|
|
|
export interface QuickAction {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
icon: string;
|
|
color: string;
|
|
category: string;
|
|
actionType: 'agentPrompt' | 'workflow' | 'link';
|
|
config: Record<string, any>;
|
|
sortOrder: number;
|
|
}
|
|
|
|
export interface QuickActionCategory {
|
|
id: string;
|
|
label: string;
|
|
sortOrder: number;
|
|
}
|
|
|
|
export interface QuickActionBoardProps {
|
|
actions: QuickAction[];
|
|
categories?: QuickActionCategory[];
|
|
onDispatch: (action: QuickAction) => void;
|
|
loading?: boolean;
|
|
grouped?: boolean;
|
|
}
|
|
|
|
// ============================================================================
|
|
// ICON MAP (mdi name → unicode/emoji fallback)
|
|
// ============================================================================
|
|
|
|
const _ICON_MAP: Record<string, string> = {
|
|
'mdi-file-document-check-outline': '\uD83D\uDCCB',
|
|
'mdi-sync': '\uD83D\uDD04',
|
|
'mdi-chart-bar': '\uD83D\uDCCA',
|
|
'mdi-view-dashboard-outline': '\uD83D\uDCF0',
|
|
'mdi-cash-multiple': '\uD83D\uDCB0',
|
|
'mdi-clipboard-check-outline': '\u2705',
|
|
'mdi-chart-timeline-variant': '\uD83D\uDCC8',
|
|
'mdi-camera-document-outline': '\uD83D\uDCF7',
|
|
};
|
|
|
|
function _renderIcon(icon: string, color: string): React.ReactNode {
|
|
const fallback = _ICON_MAP[icon] || '\u26A1';
|
|
return (
|
|
<span className={styles.actionIcon} style={{ color }}>
|
|
{fallback}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// COMPONENT
|
|
// ============================================================================
|
|
|
|
export const QuickActionBoard: React.FC<QuickActionBoardProps> = ({
|
|
actions,
|
|
categories,
|
|
onDispatch,
|
|
loading = false,
|
|
grouped = true,
|
|
}) => {
|
|
const { t } = useLanguage();
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className={styles.board}>
|
|
<h3 className={styles.boardTitle}>{t('Schnellaktionen')}</h3>
|
|
<div className={styles.grid}>
|
|
{[1, 2, 3, 4].map((i) => (
|
|
<div key={i} className={`${styles.card} ${styles.cardSkeleton}`}>
|
|
<div className={styles.skeletonIcon} />
|
|
<div className={styles.skeletonText} />
|
|
<div className={styles.skeletonTextShort} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!actions || actions.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const _handleClick = (action: QuickAction) => (e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
onDispatch(action);
|
|
};
|
|
|
|
if (!grouped || !categories || categories.length === 0) {
|
|
return (
|
|
<div className={styles.board}>
|
|
<h3 className={styles.boardTitle}>{t('Schnellaktionen')}</h3>
|
|
<div className={styles.grid}>
|
|
{actions.map((action) => (
|
|
<button
|
|
key={action.id}
|
|
className={styles.card}
|
|
onClick={_handleClick(action)}
|
|
title={action.description}
|
|
>
|
|
{_renderIcon(action.icon, action.color)}
|
|
<span className={styles.actionLabel}>{action.label}</span>
|
|
<span className={styles.actionDescription}>{action.description}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const sortedCategories = [...categories].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
const actionsByCategory = new Map<string, QuickAction[]>();
|
|
for (const action of actions) {
|
|
const cat = action.category || '_uncategorized';
|
|
if (!actionsByCategory.has(cat)) actionsByCategory.set(cat, []);
|
|
actionsByCategory.get(cat)!.push(action);
|
|
}
|
|
|
|
return (
|
|
<div className={styles.board}>
|
|
<h3 className={styles.boardTitle}>{t('Schnellaktionen')}</h3>
|
|
{sortedCategories.map((cat) => {
|
|
const catActions = actionsByCategory.get(cat.id);
|
|
if (!catActions || catActions.length === 0) return null;
|
|
return (
|
|
<div key={cat.id} className={styles.categorySection}>
|
|
<h4 className={styles.categoryTitle}>{cat.label}</h4>
|
|
<div className={styles.grid}>
|
|
{catActions.map((action) => (
|
|
<button
|
|
key={action.id}
|
|
className={styles.card}
|
|
onClick={_handleClick(action)}
|
|
title={action.description}
|
|
>
|
|
{_renderIcon(action.icon, action.color)}
|
|
<span className={styles.actionLabel}>{action.label}</span>
|
|
<span className={styles.actionDescription}>{action.description}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default QuickActionBoard;
|