mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-05-07 20:32:26 +00:00
562ee234a8
fixes #423
487 lines
16 KiB
TypeScript
487 lines
16 KiB
TypeScript
import type { Component } from 'vue';
|
|
import {
|
|
HomeIcon,
|
|
ClockIcon,
|
|
CalendarIcon,
|
|
ChartBarIcon,
|
|
FolderIcon,
|
|
UserCircleIcon,
|
|
UserGroupIcon,
|
|
TagIcon,
|
|
DocumentTextIcon,
|
|
CreditCardIcon,
|
|
ArrowsRightLeftIcon,
|
|
Cog6ToothIcon,
|
|
UserIcon,
|
|
PlayIcon,
|
|
StopIcon,
|
|
PlusIcon,
|
|
ArrowPathIcon,
|
|
SunIcon,
|
|
MoonIcon,
|
|
ComputerDesktopIcon,
|
|
ClipboardDocumentListIcon,
|
|
BuildingOfficeIcon,
|
|
} from '@heroicons/vue/20/solid';
|
|
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
|
|
import type { Organization } from '@/types/models';
|
|
|
|
export type CommandGroup =
|
|
| 'timer'
|
|
| 'active-timer'
|
|
| 'navigation'
|
|
| 'create'
|
|
| 'theme'
|
|
| 'organization'
|
|
| 'entity';
|
|
|
|
export interface Command {
|
|
id: string;
|
|
label: string;
|
|
icon?: Component;
|
|
keywords: string[];
|
|
group: CommandGroup;
|
|
action: () => void | Promise<void>;
|
|
shortcut?: string;
|
|
permission?: () => boolean;
|
|
condition?: () => boolean;
|
|
priority: number;
|
|
}
|
|
|
|
export const GROUP_PRIORITIES: Record<CommandGroup, number> = {
|
|
timer: 1000,
|
|
'active-timer': 900,
|
|
navigation: 500,
|
|
create: 400,
|
|
organization: 300,
|
|
theme: 200,
|
|
entity: 100,
|
|
};
|
|
|
|
export function createNavigationCommands(
|
|
navigate: (route: string, params?: Record<string, string>) => void,
|
|
permissions: {
|
|
canViewProjects: () => boolean;
|
|
canViewClients: () => boolean;
|
|
canViewMembers: () => boolean;
|
|
canViewTags: () => boolean;
|
|
canViewReport: () => boolean;
|
|
canViewInvoices: () => boolean;
|
|
canManageBilling: () => boolean;
|
|
canUpdateOrganization: () => boolean;
|
|
},
|
|
features: {
|
|
isInvoicingActivated: () => boolean;
|
|
isBillingActivated: () => boolean;
|
|
},
|
|
currentTeamId: () => string
|
|
): Command[] {
|
|
return [
|
|
{
|
|
id: 'nav-dashboard',
|
|
label: 'Go to Dashboard',
|
|
icon: HomeIcon,
|
|
keywords: ['home', 'overview', 'dashboard'],
|
|
group: 'navigation',
|
|
action: () => navigate('dashboard'),
|
|
priority: GROUP_PRIORITIES.navigation + 10,
|
|
},
|
|
{
|
|
id: 'nav-time',
|
|
label: 'Go to Time',
|
|
icon: ClockIcon,
|
|
keywords: ['time', 'tracking', 'entries', 'timesheet'],
|
|
group: 'navigation',
|
|
action: () => navigate('time'),
|
|
priority: GROUP_PRIORITIES.navigation + 9,
|
|
},
|
|
{
|
|
id: 'nav-calendar',
|
|
label: 'Go to Calendar',
|
|
icon: CalendarIcon,
|
|
keywords: ['calendar', 'week', 'schedule'],
|
|
group: 'navigation',
|
|
action: () => navigate('calendar'),
|
|
priority: GROUP_PRIORITIES.navigation + 8,
|
|
},
|
|
{
|
|
id: 'nav-reporting',
|
|
label: 'Go to Reporting Overview',
|
|
icon: ChartBarIcon,
|
|
keywords: ['reports', 'analytics', 'overview', 'statistics'],
|
|
group: 'navigation',
|
|
action: () => navigate('reporting'),
|
|
priority: GROUP_PRIORITIES.navigation + 7,
|
|
},
|
|
{
|
|
id: 'nav-reporting-detailed',
|
|
label: 'Go to Reporting Detailed',
|
|
icon: ChartBarIcon,
|
|
keywords: ['detailed', 'reports', 'breakdown'],
|
|
group: 'navigation',
|
|
action: () => navigate('reporting.detailed'),
|
|
priority: GROUP_PRIORITIES.navigation + 6,
|
|
},
|
|
{
|
|
id: 'nav-reporting-shared',
|
|
label: 'Go to Shared Reports',
|
|
icon: ChartBarIcon,
|
|
keywords: ['shared', 'public', 'reports'],
|
|
group: 'navigation',
|
|
action: () => navigate('reporting.shared'),
|
|
permission: permissions.canViewReport,
|
|
priority: GROUP_PRIORITIES.navigation + 5,
|
|
},
|
|
{
|
|
id: 'nav-projects',
|
|
label: 'Go to Projects',
|
|
icon: FolderIcon,
|
|
keywords: ['projects', 'work'],
|
|
group: 'navigation',
|
|
action: () => navigate('projects'),
|
|
permission: permissions.canViewProjects,
|
|
priority: GROUP_PRIORITIES.navigation + 4,
|
|
},
|
|
{
|
|
id: 'nav-clients',
|
|
label: 'Go to Clients',
|
|
icon: UserCircleIcon,
|
|
keywords: ['clients', 'customers'],
|
|
group: 'navigation',
|
|
action: () => navigate('clients'),
|
|
permission: permissions.canViewClients,
|
|
priority: GROUP_PRIORITIES.navigation + 3,
|
|
},
|
|
{
|
|
id: 'nav-members',
|
|
label: 'Go to Members',
|
|
icon: UserGroupIcon,
|
|
keywords: ['members', 'team', 'users', 'employees'],
|
|
group: 'navigation',
|
|
action: () => navigate('members'),
|
|
permission: permissions.canViewMembers,
|
|
priority: GROUP_PRIORITIES.navigation + 2,
|
|
},
|
|
{
|
|
id: 'nav-tags',
|
|
label: 'Go to Tags',
|
|
icon: TagIcon,
|
|
keywords: ['tags', 'labels', 'categories'],
|
|
group: 'navigation',
|
|
action: () => navigate('tags'),
|
|
permission: permissions.canViewTags,
|
|
priority: GROUP_PRIORITIES.navigation + 1,
|
|
},
|
|
{
|
|
id: 'nav-invoices',
|
|
label: 'Go to Invoices',
|
|
icon: DocumentTextIcon,
|
|
keywords: ['invoices', 'billing', 'payments'],
|
|
group: 'navigation',
|
|
action: () => navigate('/invoices', {}),
|
|
permission: permissions.canViewInvoices,
|
|
condition: features.isInvoicingActivated,
|
|
priority: GROUP_PRIORITIES.navigation,
|
|
},
|
|
{
|
|
id: 'nav-billing',
|
|
label: 'Go to Billing',
|
|
icon: CreditCardIcon,
|
|
keywords: ['billing', 'subscription', 'plan'],
|
|
group: 'navigation',
|
|
action: () => navigate('/billing', {}),
|
|
permission: permissions.canManageBilling,
|
|
condition: features.isBillingActivated,
|
|
priority: GROUP_PRIORITIES.navigation - 1,
|
|
},
|
|
{
|
|
id: 'nav-import',
|
|
label: 'Go to Import / Export',
|
|
icon: ArrowsRightLeftIcon,
|
|
keywords: ['import', 'export', 'data', 'backup'],
|
|
group: 'navigation',
|
|
action: () => navigate('import'),
|
|
permission: permissions.canUpdateOrganization,
|
|
priority: GROUP_PRIORITIES.navigation - 2,
|
|
},
|
|
{
|
|
id: 'nav-settings',
|
|
label: 'Go to Settings',
|
|
icon: Cog6ToothIcon,
|
|
keywords: ['settings', 'organization', 'configuration'],
|
|
group: 'navigation',
|
|
action: () => navigate('teams.show', { team: currentTeamId() }),
|
|
permission: permissions.canUpdateOrganization,
|
|
priority: GROUP_PRIORITIES.navigation - 3,
|
|
},
|
|
{
|
|
id: 'nav-profile',
|
|
label: 'Go to Profile',
|
|
icon: UserIcon,
|
|
keywords: ['profile', 'account', 'user', 'personal'],
|
|
group: 'navigation',
|
|
action: () => navigate('profile.show'),
|
|
priority: GROUP_PRIORITIES.navigation - 4,
|
|
},
|
|
];
|
|
}
|
|
|
|
export function createTimerCommands(
|
|
timerActions: {
|
|
startTimer: () => Promise<void>;
|
|
stopTimer: () => Promise<void>;
|
|
openCreateTimeEntryModal: () => void;
|
|
continueLastEntry: () => Promise<void>;
|
|
},
|
|
conditions: {
|
|
isActive: () => boolean;
|
|
hasTimeEntries: () => boolean;
|
|
}
|
|
): Command[] {
|
|
return [
|
|
{
|
|
id: 'timer-start',
|
|
label: 'Start Timer',
|
|
icon: PlayIcon,
|
|
keywords: ['start', 'begin', 'track', 'timer'],
|
|
group: 'timer',
|
|
action: timerActions.startTimer,
|
|
condition: () => !conditions.isActive(),
|
|
priority: GROUP_PRIORITIES.timer + 10,
|
|
},
|
|
{
|
|
id: 'timer-stop',
|
|
label: 'Stop Timer',
|
|
icon: StopIcon,
|
|
keywords: ['stop', 'end', 'finish', 'timer'],
|
|
group: 'timer',
|
|
action: timerActions.stopTimer,
|
|
condition: conditions.isActive,
|
|
priority: GROUP_PRIORITIES.timer + 10,
|
|
},
|
|
{
|
|
id: 'timer-create',
|
|
label: 'Create Time Entry',
|
|
icon: PlusIcon,
|
|
keywords: ['create', 'manual', 'log', 'time', 'entry', 'new'],
|
|
group: 'timer',
|
|
action: timerActions.openCreateTimeEntryModal,
|
|
priority: GROUP_PRIORITIES.timer + 5,
|
|
},
|
|
{
|
|
id: 'timer-continue',
|
|
label: 'Continue Last Time Entry',
|
|
icon: ArrowPathIcon,
|
|
keywords: ['continue', 'repeat', 'restart', 'last', 'previous'],
|
|
group: 'timer',
|
|
action: timerActions.continueLastEntry,
|
|
condition: () => !conditions.isActive() && conditions.hasTimeEntries(),
|
|
priority: GROUP_PRIORITIES.timer + 4,
|
|
},
|
|
];
|
|
}
|
|
|
|
export function createActiveTimerCommands(
|
|
activeTimerActions: {
|
|
openProjectSelector: () => void;
|
|
openTaskSelector: () => void;
|
|
openTagsSelector: () => void;
|
|
toggleBillable: () => void;
|
|
addMinutes: (minutes: number) => void;
|
|
},
|
|
conditions: {
|
|
isActive: () => boolean;
|
|
}
|
|
): Command[] {
|
|
const minuteOptions = [5, 10, 15, 20, 25, 30, 45, 60];
|
|
|
|
const addMinutesCommands: Command[] = minuteOptions.map((minutes) => ({
|
|
id: `timer-add-${minutes}`,
|
|
label: `Add ${minutes} minutes to timer`,
|
|
icon: ClockIcon,
|
|
keywords: [`+${minutes}`, `add ${minutes}`, minutes === 60 ? 'add hour' : ''],
|
|
group: 'active-timer' as CommandGroup,
|
|
action: () => activeTimerActions.addMinutes(minutes),
|
|
condition: conditions.isActive,
|
|
priority: GROUP_PRIORITIES['active-timer'] - minutes,
|
|
}));
|
|
|
|
return [
|
|
{
|
|
id: 'timer-set-project',
|
|
label: 'Set Project',
|
|
icon: FolderIcon,
|
|
keywords: ['project', 'change project', 'select project'],
|
|
group: 'active-timer',
|
|
action: activeTimerActions.openProjectSelector,
|
|
condition: conditions.isActive,
|
|
priority: GROUP_PRIORITIES['active-timer'] + 10,
|
|
},
|
|
{
|
|
id: 'timer-set-task',
|
|
label: 'Set Task',
|
|
icon: ClipboardDocumentListIcon,
|
|
keywords: ['task', 'change task', 'select task'],
|
|
group: 'active-timer',
|
|
action: activeTimerActions.openTaskSelector,
|
|
condition: conditions.isActive,
|
|
priority: GROUP_PRIORITIES['active-timer'] + 9,
|
|
},
|
|
{
|
|
id: 'timer-set-tags',
|
|
label: 'Set Tags',
|
|
icon: TagIcon,
|
|
keywords: ['tags', 'add tags', 'labels'],
|
|
group: 'active-timer',
|
|
action: activeTimerActions.openTagsSelector,
|
|
condition: conditions.isActive,
|
|
priority: GROUP_PRIORITIES['active-timer'] + 8,
|
|
},
|
|
{
|
|
id: 'timer-toggle-billable',
|
|
label: 'Toggle Billable',
|
|
icon: BillableIcon,
|
|
keywords: ['billable', 'non-billable', 'money'],
|
|
group: 'active-timer',
|
|
action: activeTimerActions.toggleBillable,
|
|
condition: conditions.isActive,
|
|
priority: GROUP_PRIORITIES['active-timer'] + 7,
|
|
},
|
|
...addMinutesCommands,
|
|
];
|
|
}
|
|
|
|
export function createThemeCommands(
|
|
setTheme: (theme: 'light' | 'dark' | 'system') => void
|
|
): Command[] {
|
|
return [
|
|
{
|
|
id: 'theme-light',
|
|
label: 'Switch to Light Theme',
|
|
icon: SunIcon,
|
|
keywords: ['light', 'bright', 'day', 'theme'],
|
|
group: 'theme',
|
|
action: () => setTheme('light'),
|
|
priority: GROUP_PRIORITIES.theme + 3,
|
|
},
|
|
{
|
|
id: 'theme-dark',
|
|
label: 'Switch to Dark Theme',
|
|
icon: MoonIcon,
|
|
keywords: ['dark', 'night', 'theme'],
|
|
group: 'theme',
|
|
action: () => setTheme('dark'),
|
|
priority: GROUP_PRIORITIES.theme + 2,
|
|
},
|
|
{
|
|
id: 'theme-system',
|
|
label: 'Switch to System Theme',
|
|
icon: ComputerDesktopIcon,
|
|
keywords: ['system', 'auto', 'default', 'theme'],
|
|
group: 'theme',
|
|
action: () => setTheme('system'),
|
|
priority: GROUP_PRIORITIES.theme + 1,
|
|
},
|
|
];
|
|
}
|
|
|
|
export function createCreateCommands(
|
|
createActions: {
|
|
openProjectModal: () => void;
|
|
openClientModal: () => void;
|
|
openTaskModal: () => void;
|
|
openTagModal: () => void;
|
|
openInviteModal: () => void;
|
|
},
|
|
permissions: {
|
|
canCreateProjects: () => boolean;
|
|
canCreateClients: () => boolean;
|
|
canCreateTasks: () => boolean;
|
|
canCreateTags: () => boolean;
|
|
canCreateInvitations: () => boolean;
|
|
}
|
|
): Command[] {
|
|
return [
|
|
{
|
|
id: 'create-project',
|
|
label: 'Create Project',
|
|
icon: FolderIcon,
|
|
keywords: ['new project', 'add project', 'create'],
|
|
group: 'create',
|
|
action: createActions.openProjectModal,
|
|
permission: permissions.canCreateProjects,
|
|
priority: GROUP_PRIORITIES.create + 5,
|
|
},
|
|
{
|
|
id: 'create-client',
|
|
label: 'Create Client',
|
|
icon: UserCircleIcon,
|
|
keywords: ['new client', 'add client', 'create'],
|
|
group: 'create',
|
|
action: createActions.openClientModal,
|
|
permission: permissions.canCreateClients,
|
|
priority: GROUP_PRIORITIES.create + 4,
|
|
},
|
|
{
|
|
id: 'create-task',
|
|
label: 'Create Task',
|
|
icon: ClipboardDocumentListIcon,
|
|
keywords: ['new task', 'add task', 'create'],
|
|
group: 'create',
|
|
action: createActions.openTaskModal,
|
|
permission: permissions.canCreateTasks,
|
|
priority: GROUP_PRIORITIES.create + 3,
|
|
},
|
|
{
|
|
id: 'create-tag',
|
|
label: 'Create Tag',
|
|
icon: TagIcon,
|
|
keywords: ['new tag', 'add tag', 'create'],
|
|
group: 'create',
|
|
action: createActions.openTagModal,
|
|
permission: permissions.canCreateTags,
|
|
priority: GROUP_PRIORITIES.create + 2,
|
|
},
|
|
{
|
|
id: 'create-invite',
|
|
label: 'Invite Member',
|
|
icon: UserGroupIcon,
|
|
keywords: ['invite', 'add member', 'team'],
|
|
group: 'create',
|
|
action: createActions.openInviteModal,
|
|
permission: permissions.canCreateInvitations,
|
|
priority: GROUP_PRIORITIES.create + 1,
|
|
},
|
|
];
|
|
}
|
|
|
|
export function createOrganizationCommands(
|
|
organizations: Organization[],
|
|
currentOrgId: string,
|
|
switchOrganization: (orgId: string) => void
|
|
): Command[] {
|
|
if (organizations.length <= 1) return [];
|
|
|
|
return organizations
|
|
.filter((org) => org.id !== currentOrgId)
|
|
.map((org) => ({
|
|
id: `org-switch-${org.id}`,
|
|
label: `Switch to ${org.name}`,
|
|
icon: BuildingOfficeIcon,
|
|
keywords: ['switch', 'organization', 'workspace', org.name.toLowerCase()],
|
|
group: 'organization' as CommandGroup,
|
|
action: () => switchOrganization(org.id),
|
|
priority: GROUP_PRIORITIES.organization + 1,
|
|
}));
|
|
}
|
|
|
|
export function scoreEntity(name: string, query: string, baseScore: number): number {
|
|
const normalizedName = name.toLowerCase();
|
|
const normalizedQuery = query.toLowerCase().trim();
|
|
|
|
if (normalizedName === normalizedQuery) return baseScore + 50;
|
|
if (normalizedName.startsWith(normalizedQuery)) return baseScore + 30;
|
|
if (normalizedName.includes(normalizedQuery)) return baseScore + 10;
|
|
return baseScore;
|
|
}
|