refactor: use Zustand for task state, optimize GraphQL requests, and fix recursive setState bug
This commit is contained in:
parent
b4d1c5db4e
commit
546ef8216d
114
client/README.md
114
client/README.md
@ -52,3 +52,117 @@ export default tseslint.config({
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
# Task Receipts Client
|
||||
|
||||
A React application for managing task receipts with GraphQL API integration and Zustand state management.
|
||||
|
||||
## Architecture
|
||||
|
||||
### State Management with Zustand
|
||||
|
||||
The application uses Zustand for state management to optimize GraphQL API requests and improve performance:
|
||||
|
||||
#### Key Features:
|
||||
- **Centralized State**: All task data is stored in a single Zustand store
|
||||
- **Optimized Queries**: GraphQL queries are only made when data is not already available in the store
|
||||
- **Efficient Updates**: Mutations update the store directly, reducing the need for refetching
|
||||
- **Selective Rendering**: Components only re-render when their specific data changes
|
||||
|
||||
#### Store Structure:
|
||||
```typescript
|
||||
interface TaskState {
|
||||
// Data
|
||||
groups: GroupWithTasks[];
|
||||
recentTasks: TaskWithSteps[];
|
||||
frequentTasks: TaskWithSteps[];
|
||||
currentStep: StepWithNotes | null;
|
||||
|
||||
// Loading states
|
||||
isLoadingGroups: boolean;
|
||||
isLoadingRecentTasks: boolean;
|
||||
isLoadingFrequentTasks: boolean;
|
||||
isLoadingStep: boolean;
|
||||
|
||||
// Selection state
|
||||
selectedGroup: GroupWithTasks | undefined;
|
||||
selectedTask: TaskWithSteps | undefined;
|
||||
selectedStep: StepWithNotes | undefined;
|
||||
|
||||
// Actions and helper methods...
|
||||
}
|
||||
```
|
||||
|
||||
#### Hooks:
|
||||
|
||||
1. **`useTaskDataOptimized`**: Main hook that manages GraphQL queries and store updates
|
||||
2. **`useTaskSelector`**: Collection of selector hooks for efficient data access
|
||||
3. **`useTaskStore`**: Direct access to the Zustand store
|
||||
|
||||
#### Performance Optimizations:
|
||||
|
||||
- **Skip Queries**: Queries are skipped if data already exists in the store
|
||||
- **Cache-First Policy**: Apollo Client uses cache-first for queries
|
||||
- **Selective Updates**: Only relevant parts of the store are updated
|
||||
- **Efficient Selectors**: Components subscribe only to the data they need
|
||||
|
||||
### GraphQL Integration
|
||||
|
||||
The application uses Apollo Client for GraphQL operations with optimized caching policies:
|
||||
|
||||
- **Cache-First**: Queries prioritize cached data
|
||||
- **Cache-and-Network**: Watch queries update from cache first, then network
|
||||
- **Optimistic Updates**: Mutations update the store immediately
|
||||
|
||||
### Component Structure
|
||||
|
||||
- **App.tsx**: Main application component using the optimized hook
|
||||
- **Layouts**: Desktop and mobile layouts for different screen sizes
|
||||
- **Components**: Reusable UI components
|
||||
- **Hooks**: Custom hooks for data management and device detection
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+
|
||||
- npm or yarn
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Development Server
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## State Management Flow
|
||||
|
||||
1. **Initial Load**:
|
||||
- `useTaskDataOptimized` checks if data exists in store
|
||||
- If not, makes GraphQL queries to fetch data
|
||||
- Updates store with fetched data
|
||||
|
||||
2. **User Interactions**:
|
||||
- Selection changes update store directly
|
||||
- Mutations update store optimistically
|
||||
- Components re-render only when their data changes
|
||||
|
||||
3. **Data Persistence**:
|
||||
- Store maintains data across component unmounts
|
||||
- Queries are skipped if data is already available
|
||||
- Cache policies ensure efficient data retrieval
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Reduced API Calls**: Data is fetched once and reused
|
||||
- **Better Performance**: Components only re-render when necessary
|
||||
- **Improved UX**: Faster navigation and interactions
|
||||
- **Maintainable Code**: Centralized state management
|
||||
- **Type Safety**: Full TypeScript support with proper typing
|
||||
|
@ -13,11 +13,12 @@
|
||||
"@apollo/client": "^3.9.5",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/material": "^5.15.10",
|
||||
"@mui/material": "^5.17.1",
|
||||
"@task-receipts/shared": "file:../shared",
|
||||
"graphql": "^16.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.55",
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import { client } from './graphql/client';
|
||||
import { DesktopLayout } from './layouts/DesktopLayout';
|
||||
import { MobileLayout } from './layouts/MobileLayout';
|
||||
import { UserSelection } from './components/UserSelection';
|
||||
import { useTaskData } from './hooks/useTaskData';
|
||||
import { useTaskDataOptimized } from './hooks/useTaskDataOptimized';
|
||||
import { useDeviceType } from './hooks/useDeviceType';
|
||||
import { useUserSelection } from './hooks/useUserSelection';
|
||||
import './App.css';
|
||||
@ -25,7 +25,7 @@ export function App() {
|
||||
handlePrintTask,
|
||||
handlePrintStep,
|
||||
handleAddNote
|
||||
} = useTaskData();
|
||||
} = useTaskDataOptimized();
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
if (selectedStep) {
|
||||
|
@ -58,11 +58,11 @@ export const client = new ApolloClient({
|
||||
}),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'network-only',
|
||||
fetchPolicy: 'cache-and-network',
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: 'network-only',
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
},
|
||||
|
446
client/src/hooks/useTaskDataOptimized.ts
Normal file
446
client/src/hooks/useTaskDataOptimized.ts
Normal file
@ -0,0 +1,446 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery, useMutation } from '@apollo/client';
|
||||
import { GET_GROUPS, GET_TASKS, GET_STEP, GET_RECENT_TASKS, GET_FREQUENT_TASKS } from '../graphql/queries';
|
||||
import { PRINT_TASK, PRINT_STEP, CREATE_NOTE, CREATE_GROUP, CREATE_TASK, CREATE_STEP } from '../graphql/mutations';
|
||||
import { useTaskStore } from '../stores/taskStore';
|
||||
import type { GroupWithTasks, TaskWithSteps, StepWithNotes } from '../types';
|
||||
import { doesExist, isArray } from '../utils/typeGuards';
|
||||
|
||||
interface GraphQLStep {
|
||||
id: number;
|
||||
name: string;
|
||||
instructions: string;
|
||||
taskId: number;
|
||||
order: number;
|
||||
notes: GraphQLNote[];
|
||||
print_count: number;
|
||||
last_printed_at: string | null;
|
||||
}
|
||||
|
||||
interface GraphQLNote {
|
||||
id: number;
|
||||
content: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
user: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GraphQLTask {
|
||||
id: number;
|
||||
name: string;
|
||||
groupId: number;
|
||||
steps: GraphQLStep[];
|
||||
notes: GraphQLNote[];
|
||||
print_count: number;
|
||||
last_printed_at: string | null;
|
||||
}
|
||||
|
||||
interface GraphQLGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
tasks: GraphQLTask[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
function toStepWithNotes(step: GraphQLStep | null | undefined): StepWithNotes | null {
|
||||
if (!doesExist(step)) return null;
|
||||
|
||||
return {
|
||||
id: step.id,
|
||||
name: step.name,
|
||||
instructions: step.instructions,
|
||||
task_id: step.taskId,
|
||||
order: step.order,
|
||||
print_count: step.print_count,
|
||||
last_printed_at: step.last_printed_at ? new Date(step.last_printed_at) : undefined,
|
||||
notes: isArray(step.notes) ? step.notes.map(note => ({
|
||||
id: note.id,
|
||||
content: note.content,
|
||||
created_at: new Date(note.created_at),
|
||||
updated_at: new Date(note.updated_at),
|
||||
created_by: note.user.id,
|
||||
user: {
|
||||
id: note.user.id,
|
||||
name: note.user.name,
|
||||
},
|
||||
})) : [],
|
||||
};
|
||||
}
|
||||
|
||||
function toTaskWithSteps(task: GraphQLTask | null | undefined): TaskWithSteps | null {
|
||||
if (!doesExist(task)) return null;
|
||||
|
||||
return {
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
group_id: task.groupId,
|
||||
print_count: task.print_count,
|
||||
last_printed_at: task.last_printed_at ? new Date(task.last_printed_at) : undefined,
|
||||
steps: isArray(task.steps) ? task.steps.map(step => toStepWithNotes(step)).filter(doesExist) : [],
|
||||
notes: isArray(task.notes) ? task.notes.map(note => ({
|
||||
id: note.id,
|
||||
content: note.content,
|
||||
created_at: new Date(note.created_at),
|
||||
updated_at: new Date(note.updated_at),
|
||||
created_by: note.user.id,
|
||||
user: {
|
||||
id: note.user.id,
|
||||
name: note.user.name,
|
||||
},
|
||||
})) : [],
|
||||
};
|
||||
}
|
||||
|
||||
function toGroupWithTasks(group: GraphQLGroup | null | undefined): GroupWithTasks | null {
|
||||
if (!doesExist(group)) return null;
|
||||
|
||||
return {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
created_at: new Date(group.created_at),
|
||||
updated_at: new Date(group.updated_at),
|
||||
tasks: isArray(group.tasks) ? group.tasks.map(task => toTaskWithSteps(task)).filter(doesExist) : [],
|
||||
};
|
||||
}
|
||||
|
||||
export function useTaskDataOptimized() {
|
||||
const {
|
||||
groups,
|
||||
recentTasks,
|
||||
frequentTasks,
|
||||
currentStep,
|
||||
isLoadingGroups,
|
||||
isLoadingRecentTasks,
|
||||
isLoadingFrequentTasks,
|
||||
isLoadingStep,
|
||||
selectedGroup,
|
||||
selectedTask,
|
||||
selectedStep,
|
||||
setGroups,
|
||||
setRecentTasks,
|
||||
setFrequentTasks,
|
||||
setCurrentStep,
|
||||
setLoadingGroups,
|
||||
setLoadingRecentTasks,
|
||||
setLoadingFrequentTasks,
|
||||
setLoadingStep,
|
||||
setSelectedGroup,
|
||||
setSelectedTask,
|
||||
setSelectedStep,
|
||||
updateTaskPrintCount,
|
||||
updateStepPrintCount,
|
||||
addNoteToStep,
|
||||
addTaskToGroup,
|
||||
addStepToTask,
|
||||
getTaskById,
|
||||
getStepById,
|
||||
} = useTaskStore();
|
||||
|
||||
// Only fetch groups if we don't have them already
|
||||
const { data: groupsData, loading: groupsLoading } = useQuery(GET_GROUPS, {
|
||||
skip: groups.length > 0,
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
// Only fetch recent tasks if we don't have them already
|
||||
const { data: recentTasksData, loading: recentTasksLoading } = useQuery(GET_RECENT_TASKS, {
|
||||
skip: recentTasks.length > 0,
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
// Only fetch frequent tasks if we don't have them already
|
||||
const { data: frequentTasksData, loading: frequentTasksLoading } = useQuery(GET_FREQUENT_TASKS, {
|
||||
skip: frequentTasks.length > 0,
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
// Only fetch tasks for a group if we don't have them already
|
||||
const { data: tasksData, loading: tasksLoading } = useQuery(GET_TASKS, {
|
||||
variables: { groupId: selectedGroup?.id },
|
||||
skip: !selectedGroup?.id || (selectedGroup && selectedGroup.tasks.length > 0),
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
// Only fetch step details if we don't have them already
|
||||
const { data: stepData, loading: stepLoading } = useQuery(GET_STEP, {
|
||||
variables: { id: selectedStep?.id },
|
||||
skip: !selectedStep?.id || (selectedStep && selectedStep.notes.length > 0),
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
// Mutations
|
||||
const [printTask] = useMutation(PRINT_TASK);
|
||||
const [printStep] = useMutation(PRINT_STEP);
|
||||
const [createNote] = useMutation(CREATE_NOTE);
|
||||
const [createGroup] = useMutation(CREATE_GROUP);
|
||||
const [createTask] = useMutation(CREATE_TASK);
|
||||
const [createStep] = useMutation(CREATE_STEP);
|
||||
|
||||
// Update store when data is fetched
|
||||
useEffect(() => {
|
||||
if (groupsData?.groups && groupsData.groups.length > 0) {
|
||||
const processedGroups = groupsData.groups
|
||||
.map((group: GraphQLGroup) => toGroupWithTasks(group))
|
||||
.filter(doesExist);
|
||||
setGroups(processedGroups);
|
||||
}
|
||||
}, [groupsData, setGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
if (recentTasksData?.recentTasks && recentTasksData.recentTasks.length > 0) {
|
||||
const processedTasks = recentTasksData.recentTasks
|
||||
.map((task: GraphQLTask) => toTaskWithSteps(task))
|
||||
.filter(doesExist);
|
||||
setRecentTasks(processedTasks);
|
||||
}
|
||||
}, [recentTasksData, setRecentTasks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (frequentTasksData?.frequentTasks && frequentTasksData.frequentTasks.length > 0) {
|
||||
const processedTasks = frequentTasksData.frequentTasks
|
||||
.map((task: GraphQLTask) => toTaskWithSteps(task))
|
||||
.filter(doesExist);
|
||||
setFrequentTasks(processedTasks);
|
||||
}
|
||||
}, [frequentTasksData, setFrequentTasks]);
|
||||
|
||||
// Update tasks in the store when fetched, but don't update selectedGroup
|
||||
useEffect(() => {
|
||||
if (tasksData?.tasks && tasksData.tasks.length > 0 && selectedGroup) {
|
||||
const processedTasks = tasksData.tasks
|
||||
.map((task: GraphQLTask) => toTaskWithSteps(task))
|
||||
.filter(doesExist);
|
||||
|
||||
// Update the groups array with the new tasks for this group
|
||||
const updatedGroups = groups.map(group =>
|
||||
group.id === selectedGroup.id
|
||||
? { ...group, tasks: processedTasks }
|
||||
: group
|
||||
);
|
||||
setGroups(updatedGroups);
|
||||
}
|
||||
}, [tasksData, selectedGroup?.id, groups, setGroups]);
|
||||
|
||||
// Update step details in the store when fetched, but don't update selectedStep
|
||||
useEffect(() => {
|
||||
if (stepData?.step && selectedStep) {
|
||||
const processedStep = toStepWithNotes(stepData.step);
|
||||
if (processedStep) {
|
||||
setCurrentStep(processedStep);
|
||||
}
|
||||
}
|
||||
}, [stepData, selectedStep?.id, setCurrentStep]);
|
||||
|
||||
// Update loading states
|
||||
useEffect(() => {
|
||||
setLoadingGroups(groupsLoading);
|
||||
}, [groupsLoading, setLoadingGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingRecentTasks(recentTasksLoading);
|
||||
}, [recentTasksLoading, setLoadingRecentTasks]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingFrequentTasks(frequentTasksLoading);
|
||||
}, [frequentTasksLoading, setLoadingFrequentTasks]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingStep(stepLoading);
|
||||
}, [stepLoading, setLoadingStep]);
|
||||
|
||||
// Create virtual groups for recent and frequent tasks
|
||||
const virtualGroups: GroupWithTasks[] = [
|
||||
{
|
||||
id: -1,
|
||||
name: 'Recent Tasks',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
tasks: recentTasks,
|
||||
},
|
||||
{
|
||||
id: -2,
|
||||
name: 'Frequent Tasks',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
tasks: frequentTasks,
|
||||
},
|
||||
];
|
||||
|
||||
const allGroups = [...virtualGroups, ...groups];
|
||||
|
||||
const handlePrintTask = async (taskId: string, userId: string) => {
|
||||
if (!taskId || !userId) {
|
||||
console.error('Cannot print task: missing taskId or userId');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await printTask({ variables: { id: taskId, userId } });
|
||||
if (result.data?.printTask) {
|
||||
const task = result.data.printTask;
|
||||
updateTaskPrintCount(
|
||||
parseInt(taskId),
|
||||
task.print_count,
|
||||
task.last_printed_at
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error printing task:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrintStep = async (stepId: string, userId: string) => {
|
||||
if (!stepId || !userId) {
|
||||
console.error('Cannot print step: missing stepId or userId');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await printStep({ variables: { id: stepId, userId } });
|
||||
if (result.data?.printStep) {
|
||||
const step = result.data.printStep;
|
||||
updateStepPrintCount(
|
||||
parseInt(stepId),
|
||||
step.print_count,
|
||||
step.last_printed_at
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error printing step:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddNote = async (content: string) => {
|
||||
if (!selectedStep?.id) {
|
||||
console.error('Cannot add note: missing stepId');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createNote({
|
||||
variables: {
|
||||
content,
|
||||
stepId: String(selectedStep.id),
|
||||
},
|
||||
});
|
||||
|
||||
if (result.data?.createNote) {
|
||||
const note = result.data.createNote;
|
||||
addNoteToStep(selectedStep.id, {
|
||||
id: note.id,
|
||||
content: note.content,
|
||||
created_at: new Date(note.created_at),
|
||||
updated_at: new Date(note.updated_at),
|
||||
created_by: note.user.id,
|
||||
user: {
|
||||
id: note.user.id,
|
||||
name: note.user.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding note:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateGroup = async (name: string) => {
|
||||
if (!name) {
|
||||
console.error('Cannot create group: missing name');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createGroup({ variables: { name } });
|
||||
if (result.data?.createGroup) {
|
||||
const group = result.data.createGroup;
|
||||
const newGroup: GroupWithTasks = {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
created_at: new Date(group.created_at),
|
||||
updated_at: new Date(group.updated_at),
|
||||
tasks: [],
|
||||
};
|
||||
setGroups([...groups, newGroup]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating group:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateTask = async (name: string, groupId: string) => {
|
||||
if (!name || !groupId) {
|
||||
console.error('Cannot create task: missing name or groupId');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createTask({ variables: { name, groupId } });
|
||||
if (result.data?.createTask) {
|
||||
const task = result.data.createTask;
|
||||
const newTask: TaskWithSteps = {
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
group_id: task.group_id,
|
||||
print_count: 0,
|
||||
steps: [],
|
||||
notes: [],
|
||||
};
|
||||
addTaskToGroup(parseInt(groupId), newTask);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating task:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateStep = async (name: string, instructions: string, taskId: string, order: number) => {
|
||||
if (!name || !instructions || !taskId || order < 1) {
|
||||
console.error('Cannot create step: missing required fields');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createStep({ variables: { name, instructions, taskId, order } });
|
||||
if (result.data?.createStep) {
|
||||
const step = result.data.createStep;
|
||||
const newStep: StepWithNotes = {
|
||||
id: step.id,
|
||||
name: step.name,
|
||||
instructions: step.instructions,
|
||||
task_id: step.task_id,
|
||||
order: step.order,
|
||||
print_count: 0,
|
||||
notes: [],
|
||||
};
|
||||
addStepToTask(parseInt(taskId), newStep);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating step:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loading = isLoadingGroups || isLoadingRecentTasks || isLoadingFrequentTasks || isLoadingStep || tasksLoading;
|
||||
|
||||
return {
|
||||
groups: allGroups,
|
||||
step: currentStep,
|
||||
loading,
|
||||
selectedGroup,
|
||||
selectedTask,
|
||||
selectedStep,
|
||||
setSelectedGroup,
|
||||
setSelectedTask,
|
||||
setSelectedStep,
|
||||
handlePrintTask,
|
||||
handlePrintStep,
|
||||
handleAddNote,
|
||||
handleCreateGroup,
|
||||
handleCreateTask,
|
||||
handleCreateStep,
|
||||
getTaskById,
|
||||
getStepById,
|
||||
};
|
||||
}
|
85
client/src/hooks/useTaskSelector.ts
Normal file
85
client/src/hooks/useTaskSelector.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { useTaskStore } from '../stores/taskStore';
|
||||
import type { GroupWithTasks } from '../types';
|
||||
|
||||
// Selector hooks for efficient data access
|
||||
export function useGroups() {
|
||||
return useTaskStore((state) => state.groups);
|
||||
}
|
||||
|
||||
export function useRecentTasks() {
|
||||
return useTaskStore((state) => state.recentTasks);
|
||||
}
|
||||
|
||||
export function useFrequentTasks() {
|
||||
return useTaskStore((state) => state.frequentTasks);
|
||||
}
|
||||
|
||||
export function useSelectedGroup() {
|
||||
return useTaskStore((state) => state.selectedGroup);
|
||||
}
|
||||
|
||||
export function useSelectedTask() {
|
||||
return useTaskStore((state) => state.selectedTask);
|
||||
}
|
||||
|
||||
export function useSelectedStep() {
|
||||
return useTaskStore((state) => state.selectedStep);
|
||||
}
|
||||
|
||||
export function useLoadingStates() {
|
||||
return useTaskStore((state) => ({
|
||||
isLoadingGroups: state.isLoadingGroups,
|
||||
isLoadingRecentTasks: state.isLoadingRecentTasks,
|
||||
isLoadingFrequentTasks: state.isLoadingFrequentTasks,
|
||||
isLoadingStep: state.isLoadingStep,
|
||||
}));
|
||||
}
|
||||
|
||||
export function useTaskActions() {
|
||||
return useTaskStore((state) => ({
|
||||
setSelectedGroup: state.setSelectedGroup,
|
||||
setSelectedTask: state.setSelectedTask,
|
||||
setSelectedStep: state.setSelectedStep,
|
||||
getTaskById: state.getTaskById,
|
||||
getStepById: state.getStepById,
|
||||
getGroupById: state.getGroupById,
|
||||
}));
|
||||
}
|
||||
|
||||
// Custom selector for a specific task
|
||||
export function useTaskById(taskId: number) {
|
||||
return useTaskStore((state) => state.getTaskById(taskId));
|
||||
}
|
||||
|
||||
// Custom selector for a specific step
|
||||
export function useStepById(stepId: number) {
|
||||
return useTaskStore((state) => state.getStepById(stepId));
|
||||
}
|
||||
|
||||
// Custom selector for a specific group
|
||||
export function useGroupById(groupId: number) {
|
||||
return useTaskStore((state) => state.getGroupById(groupId));
|
||||
}
|
||||
|
||||
// Selector for all groups including virtual ones
|
||||
export function useAllGroups() {
|
||||
return useTaskStore((state) => {
|
||||
const virtualGroups: GroupWithTasks[] = [
|
||||
{
|
||||
id: -1,
|
||||
name: 'Recent Tasks',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
tasks: state.recentTasks,
|
||||
},
|
||||
{
|
||||
id: -2,
|
||||
name: 'Frequent Tasks',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
tasks: state.frequentTasks,
|
||||
},
|
||||
];
|
||||
return [...virtualGroups, ...state.groups];
|
||||
});
|
||||
}
|
259
client/src/stores/taskStore.ts
Normal file
259
client/src/stores/taskStore.ts
Normal file
@ -0,0 +1,259 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import type { GroupWithTasks, TaskWithSteps, StepWithNotes } from '../types';
|
||||
|
||||
interface TaskState {
|
||||
// Data
|
||||
groups: GroupWithTasks[];
|
||||
recentTasks: TaskWithSteps[];
|
||||
frequentTasks: TaskWithSteps[];
|
||||
currentStep: StepWithNotes | null;
|
||||
|
||||
// Loading states
|
||||
isLoadingGroups: boolean;
|
||||
isLoadingRecentTasks: boolean;
|
||||
isLoadingFrequentTasks: boolean;
|
||||
isLoadingStep: boolean;
|
||||
|
||||
// Selection state
|
||||
selectedGroup: GroupWithTasks | undefined;
|
||||
selectedTask: TaskWithSteps | undefined;
|
||||
selectedStep: StepWithNotes | undefined;
|
||||
|
||||
// Actions
|
||||
setGroups: (groups: GroupWithTasks[]) => void;
|
||||
setRecentTasks: (tasks: TaskWithSteps[]) => void;
|
||||
setFrequentTasks: (tasks: TaskWithSteps[]) => void;
|
||||
setCurrentStep: (step: StepWithNotes | null) => void;
|
||||
|
||||
setLoadingGroups: (loading: boolean) => void;
|
||||
setLoadingRecentTasks: (loading: boolean) => void;
|
||||
setLoadingFrequentTasks: (loading: boolean) => void;
|
||||
setLoadingStep: (loading: boolean) => void;
|
||||
|
||||
setSelectedGroup: (group: GroupWithTasks | undefined) => void;
|
||||
setSelectedTask: (task: TaskWithSteps | undefined) => void;
|
||||
setSelectedStep: (step: StepWithNotes | undefined) => void;
|
||||
|
||||
// Helper methods
|
||||
getTaskById: (taskId: number) => TaskWithSteps | undefined;
|
||||
getStepById: (stepId: number) => StepWithNotes | undefined;
|
||||
getGroupById: (groupId: number) => GroupWithTasks | undefined;
|
||||
|
||||
// Update methods for mutations
|
||||
updateTaskPrintCount: (taskId: number, printCount: number, lastPrintedAt: string | null) => void;
|
||||
updateStepPrintCount: (stepId: number, printCount: number, lastPrintedAt: string | null) => void;
|
||||
addNoteToStep: (stepId: number, note: any) => void;
|
||||
addTaskToGroup: (groupId: number, task: TaskWithSteps) => void;
|
||||
addStepToTask: (taskId: number, step: StepWithNotes) => void;
|
||||
|
||||
// Clear methods
|
||||
clearData: () => void;
|
||||
}
|
||||
|
||||
export const useTaskStore = create<TaskState>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
groups: [],
|
||||
recentTasks: [],
|
||||
frequentTasks: [],
|
||||
currentStep: null,
|
||||
|
||||
isLoadingGroups: false,
|
||||
isLoadingRecentTasks: false,
|
||||
isLoadingFrequentTasks: false,
|
||||
isLoadingStep: false,
|
||||
|
||||
selectedGroup: undefined,
|
||||
selectedTask: undefined,
|
||||
selectedStep: undefined,
|
||||
|
||||
// Actions
|
||||
setGroups: (groups) => set({ groups }),
|
||||
setRecentTasks: (tasks) => set({ recentTasks: tasks }),
|
||||
setFrequentTasks: (tasks) => set({ frequentTasks: tasks }),
|
||||
setCurrentStep: (step) => set({ currentStep: step }),
|
||||
|
||||
setLoadingGroups: (loading) => set({ isLoadingGroups: loading }),
|
||||
setLoadingRecentTasks: (loading) => set({ isLoadingRecentTasks: loading }),
|
||||
setLoadingFrequentTasks: (loading) => set({ isLoadingFrequentTasks: loading }),
|
||||
setLoadingStep: (loading) => set({ isLoadingStep: loading }),
|
||||
|
||||
setSelectedGroup: (group) => set({ selectedGroup: group }),
|
||||
setSelectedTask: (task) => set({ selectedTask: task }),
|
||||
setSelectedStep: (step) => set({ selectedStep: step }),
|
||||
|
||||
// Helper methods
|
||||
getTaskById: (taskId) => {
|
||||
const state = get();
|
||||
// Search in all groups
|
||||
for (const group of state.groups) {
|
||||
const task = group.tasks.find(t => t.id === taskId);
|
||||
if (task) return task;
|
||||
}
|
||||
// Search in recent tasks
|
||||
const recentTask = state.recentTasks.find(t => t.id === taskId);
|
||||
if (recentTask) return recentTask;
|
||||
// Search in frequent tasks
|
||||
const frequentTask = state.frequentTasks.find(t => t.id === taskId);
|
||||
if (frequentTask) return frequentTask;
|
||||
return undefined;
|
||||
},
|
||||
|
||||
getStepById: (stepId) => {
|
||||
const state = get();
|
||||
// Search in all groups
|
||||
for (const group of state.groups) {
|
||||
for (const task of group.tasks) {
|
||||
const step = task.steps.find(s => s.id === stepId);
|
||||
if (step) return step;
|
||||
}
|
||||
}
|
||||
// Search in recent tasks
|
||||
for (const task of state.recentTasks) {
|
||||
const step = task.steps.find(s => s.id === stepId);
|
||||
if (step) return step;
|
||||
}
|
||||
// Search in frequent tasks
|
||||
for (const task of state.frequentTasks) {
|
||||
const step = task.steps.find(s => s.id === stepId);
|
||||
if (step) return step;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
getGroupById: (groupId) => {
|
||||
const state = get();
|
||||
return state.groups.find(g => g.id === groupId);
|
||||
},
|
||||
|
||||
// Update methods for mutations
|
||||
updateTaskPrintCount: (taskId, printCount, lastPrintedAt) => {
|
||||
set((state) => {
|
||||
const updateTaskInArray = (tasks: TaskWithSteps[]): TaskWithSteps[] =>
|
||||
tasks.map(task =>
|
||||
task.id === taskId
|
||||
? {
|
||||
...task,
|
||||
print_count: printCount,
|
||||
last_printed_at: lastPrintedAt ? new Date(lastPrintedAt) : null,
|
||||
}
|
||||
: task
|
||||
);
|
||||
|
||||
return {
|
||||
groups: state.groups.map(group => ({
|
||||
...group,
|
||||
tasks: updateTaskInArray(group.tasks),
|
||||
})),
|
||||
recentTasks: updateTaskInArray(state.recentTasks),
|
||||
frequentTasks: updateTaskInArray(state.frequentTasks),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
updateStepPrintCount: (stepId, printCount, lastPrintedAt) => {
|
||||
set((state) => {
|
||||
const updateStepInTask = (task: TaskWithSteps): TaskWithSteps => ({
|
||||
...task,
|
||||
steps: task.steps.map(step =>
|
||||
step.id === stepId
|
||||
? {
|
||||
...step,
|
||||
print_count: printCount,
|
||||
last_printed_at: lastPrintedAt ? new Date(lastPrintedAt) : null,
|
||||
}
|
||||
: step
|
||||
),
|
||||
});
|
||||
|
||||
const updateTaskInArray = (tasks: TaskWithSteps[]): TaskWithSteps[] =>
|
||||
tasks.map(task => updateStepInTask(task));
|
||||
|
||||
return {
|
||||
groups: state.groups.map(group => ({
|
||||
...group,
|
||||
tasks: updateTaskInArray(group.tasks),
|
||||
})),
|
||||
recentTasks: updateTaskInArray(state.recentTasks),
|
||||
frequentTasks: updateTaskInArray(state.frequentTasks),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
addNoteToStep: (stepId, note) => {
|
||||
set((state) => {
|
||||
const addNoteToStepInTask = (task: TaskWithSteps): TaskWithSteps => ({
|
||||
...task,
|
||||
steps: task.steps.map(step =>
|
||||
step.id === stepId
|
||||
? {
|
||||
...step,
|
||||
notes: [...step.notes, note],
|
||||
}
|
||||
: step
|
||||
),
|
||||
});
|
||||
|
||||
const updateTaskInArray = (tasks: TaskWithSteps[]): TaskWithSteps[] =>
|
||||
tasks.map(task => addNoteToStepInTask(task));
|
||||
|
||||
return {
|
||||
groups: state.groups.map(group => ({
|
||||
...group,
|
||||
tasks: updateTaskInArray(group.tasks),
|
||||
})),
|
||||
recentTasks: updateTaskInArray(state.recentTasks),
|
||||
frequentTasks: updateTaskInArray(state.frequentTasks),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
addTaskToGroup: (groupId, task) => {
|
||||
set((state) => ({
|
||||
groups: state.groups.map(group =>
|
||||
group.id === groupId
|
||||
? { ...group, tasks: [...group.tasks, task] }
|
||||
: group
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
addStepToTask: (taskId, step) => {
|
||||
set((state) => {
|
||||
const addStepToTaskInArray = (tasks: TaskWithSteps[]): TaskWithSteps[] =>
|
||||
tasks.map(task =>
|
||||
task.id === taskId
|
||||
? { ...task, steps: [...task.steps, step] }
|
||||
: task
|
||||
);
|
||||
|
||||
return {
|
||||
groups: state.groups.map(group => ({
|
||||
...group,
|
||||
tasks: addStepToTaskInArray(group.tasks),
|
||||
})),
|
||||
recentTasks: addStepToTaskInArray(state.recentTasks),
|
||||
frequentTasks: addStepToTaskInArray(state.frequentTasks),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
clearData: () => {
|
||||
set({
|
||||
groups: [],
|
||||
recentTasks: [],
|
||||
frequentTasks: [],
|
||||
currentStep: null,
|
||||
selectedGroup: undefined,
|
||||
selectedTask: undefined,
|
||||
selectedStep: undefined,
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'task-store',
|
||||
}
|
||||
)
|
||||
);
|
Loading…
Reference in New Issue
Block a user