feat: add wiki, metadata, and task management
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled

This commit is contained in:
gitmuhammedalbayrak
2025-11-30 16:44:38 +03:00
parent b9148cfa4b
commit 9918a7556a
12 changed files with 2573 additions and 249 deletions

View File

@@ -5,6 +5,8 @@ import { TenantsModule } from './tenants/tenants.module';
import { ProjectsModule } from './projects/projects.module'; import { ProjectsModule } from './projects/projects.module';
import { Tenant } from './tenants/tenant.entity'; import { Tenant } from './tenants/tenant.entity';
import { Project } from './projects/project.entity'; import { Project } from './projects/project.entity';
import { Task } from './tasks/task.entity';
import { TasksModule } from './tasks/tasks.module';
@Module({ @Module({
imports: [ imports: [
@@ -16,11 +18,12 @@ import { Project } from './projects/project.entity';
username: process.env.DB_USERNAME || 'postgres', username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'postgres', password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_DATABASE || 'evrak', database: process.env.DB_DATABASE || 'evrak',
entities: [Tenant, Project], entities: [Tenant, Project, Task],
synchronize: false, // Schema is managed by SQL migration files synchronize: true, // Enabled for development
}), }),
TenantsModule, TenantsModule,
ProjectsModule, ProjectsModule,
TasksModule,
], ],
}) })
export class AppModule { } export class AppModule { }

View File

@@ -19,6 +19,9 @@ export class Project {
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
description: string; description: string;
@Column({ type: 'text', nullable: true })
wiki_content: string;
@Column({ type: 'ltree' }) @Column({ type: 'ltree' })
path: string; path: string;

View File

@@ -0,0 +1,44 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { Project } from '../projects/project.entity';
import { Tenant } from '../tenants/tenant.entity';
@Entity('tasks')
export class Task {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
tenant_id: string;
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@Column({ type: 'uuid' })
project_id: string;
@ManyToOne(() => Project)
@JoinColumn({ name: 'project_id' })
project: Project;
@Column()
title: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ default: 'todo' })
status: string; // todo, in_progress, done
@Column({ default: 'medium' })
priority: string; // low, medium, high
@Column({ type: 'timestamp', nullable: true })
due_date: Date;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}

View File

@@ -0,0 +1,29 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Query } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { AuthGuard } from '@nestjs/passport';
@Controller('tasks')
@UseGuards(AuthGuard('jwt'))
export class TasksController {
constructor(private readonly tasksService: TasksService) { }
@Get()
findAll(@Query('projectId') projectId: string) {
return this.tasksService.findAll(projectId);
}
@Post()
create(@Body() createTaskDto: any) {
return this.tasksService.create(createTaskDto);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateTaskDto: any) {
return this.tasksService.update(id, updateTaskDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.tasksService.remove(id);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksService } from './tasks.service';
import { TasksController } from './tasks.controller';
import { Task } from './task.entity';
@Module({
imports: [TypeOrmModule.forFeature([Task])],
controllers: [TasksController],
providers: [TasksService],
})
export class TasksModule { }

View File

@@ -0,0 +1,49 @@
import { Injectable, Inject, Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Task } from './task.entity';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class TasksService {
constructor(
@InjectRepository(Task)
private tasksRepository: Repository<Task>,
@Inject(REQUEST) private request: Request,
) { }
private getTenantId(): string {
const user = (this.request as any).user;
return user.tenantId;
}
async findAll(projectId: string): Promise<Task[]> {
return this.tasksRepository.find({
where: {
project_id: projectId,
tenant_id: this.getTenantId()
},
order: { createdAt: 'DESC' }
});
}
async create(createTaskDto: any): Promise<Task> {
return this.tasksRepository.save({
...createTaskDto,
tenant_id: this.getTenantId()
});
}
async update(id: string, updateTaskDto: any): Promise<Task | null> {
await this.tasksRepository.update(
{ id, tenant_id: this.getTenantId() },
updateTaskDto
);
return this.tasksRepository.findOne({ where: { id, tenant_id: this.getTenantId() } });
}
async remove(id: string): Promise<void> {
await this.tasksRepository.delete({ id, tenant_id: this.getTenantId() });
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,9 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.1.0", "@ant-design/icons": "^6.1.0",
"@uiw/react-md-editor": "^4.0.8",
"antd": "^6.0.0", "antd": "^6.0.0",
"dayjs": "^1.11.19",
"i18next": "^25.6.3", "i18next": "^25.6.3",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"react": "^19.2.0", "react": "^19.2.0",

View File

@@ -1,9 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, Form, Input, Button, message, Typography, Divider } from 'antd'; import { Card, Form, Input, Button, message, Typography, Divider, Tabs, Select, DatePicker, List, Checkbox, Tag, Space } from 'antd';
import { SaveOutlined, DeleteOutlined, ArrowLeftOutlined } from '@ant-design/icons'; import { SaveOutlined, DeleteOutlined, ArrowLeftOutlined, PlusOutlined } from '@ant-design/icons';
import { useProjectStore } from '../store/useProjectStore'; import { useProjectStore } from '../store/useProjectStore';
import { useTaskStore } from '../store/useTaskStore';
import MDEditor from '@uiw/react-md-editor';
import dayjs from 'dayjs';
const { Text } = Typography; const { Text } = Typography;
const { Option } = Select;
interface ProjectDetailPanelProps { interface ProjectDetailPanelProps {
projectId: string | null; projectId: string | null;
@@ -13,8 +17,11 @@ interface ProjectDetailPanelProps {
const ProjectDetailPanel: React.FC<ProjectDetailPanelProps> = ({ projectId, onClose }) => { const ProjectDetailPanel: React.FC<ProjectDetailPanelProps> = ({ projectId, onClose }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const { projects, updateProject, deleteProject, fetchProjects } = useProjectStore(); const { projects, updateProject, deleteProject, fetchProjects } = useProjectStore();
const { tasks, fetchTasks, createTask, updateTask, deleteTask } = useTaskStore();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false);
const [wikiContent, setWikiContent] = useState<string | undefined>('');
const [newTaskTitle, setNewTaskTitle] = useState('');
const project = projectId ? projects.find(p => p.id === projectId) : null; const project = projectId ? projects.find(p => p.id === projectId) : null;
@@ -23,12 +30,19 @@ const ProjectDetailPanel: React.FC<ProjectDetailPanelProps> = ({ projectId, onCl
form.setFieldsValue({ form.setFieldsValue({
name: project.name, name: project.name,
description: project.description || '', description: project.description || '',
attributes: project.attributes ? JSON.stringify(project.attributes, null, 2) : '{}', status: project.attributes?.status || 'active',
priority: project.attributes?.priority || 'medium',
startDate: project.attributes?.startDate ? dayjs(project.attributes.startDate) : null,
dueDate: project.attributes?.dueDate ? dayjs(project.attributes.dueDate) : null,
tags: project.attributes?.tags || [],
}); });
setWikiContent(project.wiki_content || '');
fetchTasks(project.id);
} else { } else {
form.resetFields(); form.resetFields();
setWikiContent('');
} }
}, [project, form]); }, [project, form, fetchTasks]);
const handleSave = async () => { const handleSave = async () => {
if (!project) return; if (!project) return;
@@ -37,22 +51,22 @@ const ProjectDetailPanel: React.FC<ProjectDetailPanelProps> = ({ projectId, onCl
setLoading(true); setLoading(true);
const values = await form.validateFields(); const values = await form.validateFields();
// Validate JSON if provided const attributes = {
if (values.attributes) { ...project.attributes,
try { status: values.status,
JSON.parse(values.attributes); priority: values.priority,
} catch (e) { startDate: values.startDate ? values.startDate.toISOString() : null,
message.error('Invalid JSON in attributes field'); dueDate: values.dueDate ? values.dueDate.toISOString() : null,
return; tags: values.tags,
} };
}
await updateProject(project.id, { await updateProject(project.id, {
name: values.name, name: values.name,
description: values.description || null, description: values.description || null,
wiki_content: wikiContent,
attributes: attributes,
}); });
// Note: attributes update would need backend support
message.success('Project updated successfully'); message.success('Project updated successfully');
await fetchProjects(); await fetchProjects();
} catch (error: any) { } catch (error: any) {
@@ -77,6 +91,18 @@ const ProjectDetailPanel: React.FC<ProjectDetailPanelProps> = ({ projectId, onCl
} }
}; };
const handleAddTask = async () => {
if (!newTaskTitle.trim() || !project) return;
await createTask({
title: newTaskTitle,
project_id: project.id,
status: 'todo',
priority: 'medium'
});
setNewTaskTitle('');
message.success('Task added');
};
if (!project) { if (!project) {
return ( return (
<div style={{ <div style={{
@@ -96,231 +122,128 @@ const ProjectDetailPanel: React.FC<ProjectDetailPanelProps> = ({ projectId, onCl
); );
} }
const children = projects.filter(p => p.parentId === project.id); const items = [
const parent = project.parentId ? projects.find(p => p.id === project.parentId) : null; {
key: '1',
label: 'Details',
children: (
<Form form={form} layout="vertical">
<Form.Item name="name" label="Project Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="description" label="Description">
<Input.TextArea rows={4} />
</Form.Item>
<Divider>Metadata</Divider>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<Form.Item name="status" label="Status">
<Select>
<Option value="active">Active</Option>
<Option value="on_hold">On Hold</Option>
<Option value="completed">Completed</Option>
</Select>
</Form.Item>
<Form.Item name="priority" label="Priority">
<Select>
<Option value="high">High</Option>
<Option value="medium">Medium</Option>
<Option value="low">Low</Option>
</Select>
</Form.Item>
<Form.Item name="startDate" label="Start Date">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="dueDate" label="Due Date">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</div>
<Form.Item name="tags" label="Tags">
<Select mode="tags" placeholder="Add tags" />
</Form.Item>
</Form>
),
},
{
key: '2',
label: 'Wiki',
children: (
<div data-color-mode="light">
<MDEditor
value={wikiContent}
onChange={setWikiContent}
height={500}
/>
</div>
),
},
{
key: '3',
label: `Tasks (${tasks.length})`,
children: (
<div>
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<Input
placeholder="Add a new task..."
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
onPressEnter={handleAddTask}
/>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddTask}>Add</Button>
</div>
<List
dataSource={tasks}
renderItem={(task) => (
<List.Item
actions={[
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => deleteTask(task.id)} />
]}
>
<List.Item.Meta
avatar={
<Checkbox
checked={task.status === 'done'}
onChange={(e) => updateTask(task.id, { status: e.target.checked ? 'done' : 'todo' })}
/>
}
title={
<Text delete={task.status === 'done'}>
{task.title}
</Text>
}
description={
<Space>
<Tag color={task.priority === 'high' ? 'red' : task.priority === 'medium' ? 'orange' : 'green'}>
{task.priority}
</Tag>
{task.due_date && <Text type="secondary" style={{ fontSize: '12px' }}>Due: {dayjs(task.due_date).format('YYYY-MM-DD')}</Text>}
</Space>
}
/>
</List.Item>
)}
/>
</div>
),
},
];
return ( return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#fff' }}> <div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#fff' }}>
{/* Header */} <div style={{ padding: '12px 16px', borderBottom: '1px solid #f0f0f0', background: '#fafafa', display: 'flex', justifyContent: 'space-between' }}>
<div style={{ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
padding: '12px 16px', {onClose && <Button type="text" icon={<ArrowLeftOutlined />} onClick={onClose} />}
borderBottom: '1px solid #f0f0f0', <Text strong style={{ fontSize: '15px' }}>{project.name}</Text>
background: '#fafafa'
}}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '12px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flex: 1 }}>
{onClose && (
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={onClose}
size="small"
style={{
padding: '0 4px',
minWidth: 'auto'
}}
/>
)}
<Text strong style={{ fontSize: '15px', fontWeight: 600 }}>Project Details</Text>
</div>
<Button
type="primary"
icon={<SaveOutlined />}
loading={loading}
onClick={handleSave}
size="small"
style={{
minWidth: '70px',
height: '28px',
fontSize: '13px',
padding: '0 12px'
}}
>
Save
</Button>
</div>
{project && (
<div style={{
marginTop: '8px',
padding: '8px 12px',
background: '#fff',
borderRadius: '6px',
border: '1px solid #e8e8e8'
}}>
<Text type="secondary" style={{ fontSize: '12px', display: 'block', marginBottom: '4px' }}>Current Project</Text>
<Text strong style={{ fontSize: '14px' }}>{project.name}</Text>
</div>
)}
<div style={{ marginTop: '8px' }}>
<Button
danger
icon={<DeleteOutlined />}
loading={deleteLoading}
onClick={handleDelete}
size="small"
block
style={{
height: '28px',
fontSize: '13px'
}}
>
Delete Project
</Button>
</div> </div>
<Space>
<Button danger icon={<DeleteOutlined />} loading={deleteLoading} onClick={handleDelete}>Delete</Button>
<Button type="primary" icon={<SaveOutlined />} loading={loading} onClick={handleSave}>Save</Button>
</Space>
</div> </div>
{/* Content */}
<div style={{ flex: 1, overflow: 'auto', padding: '20px' }}> <div style={{ flex: 1, overflow: 'auto', padding: '20px' }}>
<Card bordered={false} style={{ boxShadow: 'none', padding: 0 }}> <Tabs defaultActiveKey="1" items={items} />
<Form form={form} layout="vertical" style={{ maxWidth: '100%' }}>
<Form.Item
name="name"
label={<Text strong style={{ fontSize: '13px', marginBottom: '6px', display: 'block' }}>Project Name</Text>}
rules={[{ required: true, message: 'Please enter project name' }]}
style={{ marginBottom: '18px' }}
>
<Input
placeholder="Enter project name"
style={{
borderRadius: '6px',
fontSize: '14px'
}}
/>
</Form.Item>
<Form.Item
name="description"
label={<Text strong style={{ fontSize: '13px', marginBottom: '6px', display: 'block' }}>Description</Text>}
style={{ marginBottom: '20px' }}
>
<Input.TextArea
rows={4}
placeholder="Enter project description (optional)"
showCount
maxLength={500}
style={{
borderRadius: '6px',
fontSize: '14px'
}}
/>
</Form.Item>
<Divider style={{ marginTop: '8px', marginBottom: '24px' }}>
<Text strong style={{ fontSize: '15px' }}>Hierarchy</Text>
</Divider>
<div style={{ marginBottom: '32px' }}>
<div style={{ marginBottom: '20px' }}>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '10px', fontWeight: 500 }}>Parent</Text>
{parent ? (
<div style={{
padding: '12px 16px',
background: '#e6f7ff',
border: '1px solid #91d5ff',
borderRadius: '8px',
cursor: 'pointer',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#bae7ff';
e.currentTarget.style.borderColor = '#69c0ff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#e6f7ff';
e.currentTarget.style.borderColor = '#91d5ff';
}}
>
<Text strong style={{ color: '#1890ff', fontSize: '14px' }}>{parent.name}</Text>
</div>
) : (
<Text type="secondary" style={{ fontStyle: 'italic', fontSize: '13px' }}>Root project (no parent)</Text>
)}
</div>
<div>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '10px', fontWeight: 500 }}>Children ({children.length})</Text>
{children.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{children.map(child => (
<div
key={child.id}
style={{
padding: '12px 16px',
background: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '8px',
cursor: 'pointer',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#e6f7ff';
e.currentTarget.style.borderColor = '#91d5ff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#f5f5f5';
e.currentTarget.style.borderColor = '#d9d9d9';
}}
>
<Text style={{ fontSize: '14px' }}>{child.name}</Text>
</div>
))}
</div>
) : (
<Text type="secondary" style={{ fontStyle: 'italic', fontSize: '13px' }}>No child projects</Text>
)}
</div>
</div>
<Divider style={{ marginTop: '8px', marginBottom: '24px' }}>
<Text strong style={{ fontSize: '15px' }}>Metadata</Text>
</Divider>
<Form.Item
name="attributes"
label={<Text strong style={{ fontSize: '14px', marginBottom: '8px', display: 'block' }}>Attributes (JSON)</Text>}
tooltip='Custom attributes in JSON format. Example: {"key": "value"}'
style={{ marginBottom: '32px' }}
>
<Input.TextArea
rows={6}
placeholder='{"key": "value", "anotherKey": "anotherValue"}'
style={{
fontFamily: 'monospace',
fontSize: '13px',
borderRadius: '8px',
padding: '10px 16px'
}}
/>
</Form.Item>
<Divider style={{ marginTop: '8px', marginBottom: '24px' }} />
<div style={{
padding: '16px 20px',
background: '#fafafa',
borderRadius: '8px',
border: '1px solid #f0f0f0'
}}>
<div style={{ marginBottom: '16px' }}>
<Text type="secondary" style={{ fontSize: '12px', display: 'block', marginBottom: '8px', fontWeight: 500 }}>Project ID</Text>
<Text code style={{ fontSize: '12px', wordBreak: 'break-all', padding: '4px 8px', background: '#fff', borderRadius: '4px' }}>{project.id}</Text>
</div>
<div>
<Text type="secondary" style={{ fontSize: '12px', display: 'block', marginBottom: '8px', fontWeight: 500 }}>Path</Text>
<Text code style={{ fontSize: '12px', wordBreak: 'break-all', padding: '4px 8px', background: '#fff', borderRadius: '4px' }}>{project.path}</Text>
</div>
</div>
</Form>
</Card>
</div> </div>
</div> </div>
); );
}; };
export default ProjectDetailPanel; export default ProjectDetailPanel;

View File

@@ -6,6 +6,7 @@ interface Project {
path: string; path: string;
parentId: string | null; parentId: string | null;
description?: string | null; description?: string | null;
wiki_content?: string;
attributes?: Record<string, any>; attributes?: Record<string, any>;
children?: Project[]; children?: Project[];
} }
@@ -20,7 +21,7 @@ interface ProjectState {
expandNode: (key: string) => void; expandNode: (key: string) => void;
fetchProjects: () => Promise<void>; fetchProjects: () => Promise<void>;
createProject: (data: { name: string; parentId?: string | null; description?: string }) => Promise<void>; createProject: (data: { name: string; parentId?: string | null; description?: string }) => Promise<void>;
updateProject: (id: string, data: { name?: string; description?: string; parentId?: string | null }) => Promise<void>; updateProject: (id: string, data: { name?: string; description?: string; parentId?: string | null; wiki_content?: string; attributes?: any }) => Promise<void>;
deleteProject: (id: string) => Promise<void>; deleteProject: (id: string) => Promise<void>;
} }
@@ -101,6 +102,8 @@ export const useProjectStore = create<ProjectState>((set) => ({
...(data.name && { name: data.name }), ...(data.name && { name: data.name }),
...(data.description !== undefined && { description: data.description }), ...(data.description !== undefined && { description: data.description }),
...(data.parentId !== undefined && { parent_id: data.parentId }), ...(data.parentId !== undefined && { parent_id: data.parentId }),
...(data.wiki_content !== undefined && { wiki_content: data.wiki_content }),
...(data.attributes !== undefined && { attributes: data.attributes }),
}), }),
}); });
if (response.ok) { if (response.ok) {

View File

@@ -0,0 +1,102 @@
import { create } from 'zustand';
interface Task {
id: string;
title: string;
description?: string;
status: string;
priority: string;
due_date?: string;
project_id: string;
}
interface TaskState {
tasks: Task[];
loading: boolean;
fetchTasks: (projectId: string) => Promise<void>;
createTask: (task: Partial<Task>) => Promise<void>;
updateTask: (id: string, updates: Partial<Task>) => Promise<void>;
deleteTask: (id: string) => Promise<void>;
}
export const useTaskStore = create<TaskState>((set) => ({
tasks: [],
loading: false,
fetchTasks: async (projectId: string) => {
set({ loading: true });
try {
const response = await fetch(`http://localhost:3000/tasks?projectId=${projectId}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
const tasks = await response.json();
set({ tasks });
}
} catch (error) {
console.error('Failed to fetch tasks:', error);
} finally {
set({ loading: false });
}
},
createTask: async (task: Partial<Task>) => {
try {
const response = await fetch('http://localhost:3000/tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(task)
});
if (response.ok) {
const newTask = await response.json();
set(state => ({ tasks: [newTask, ...state.tasks] }));
}
} catch (error) {
console.error('Failed to create task:', error);
}
},
updateTask: async (id: string, updates: Partial<Task>) => {
try {
const response = await fetch(`http://localhost:3000/tasks/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(updates)
});
if (response.ok) {
const updatedTask = await response.json();
set(state => ({
tasks: state.tasks.map(t => t.id === id ? updatedTask : t)
}));
}
} catch (error) {
console.error('Failed to update task:', error);
}
},
deleteTask: async (id: string) => {
try {
const response = await fetch(`http://localhost:3000/tasks/${id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
set(state => ({
tasks: state.tasks.filter(t => t.id !== id)
}));
}
} catch (error) {
console.error('Failed to delete task:', error);
}
}
}));

View File

@@ -3,11 +3,16 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022", "target": "ES2022",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"], "lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"module": "ESNext", "module": "ESNext",
"types": ["vite/client"], "types": [
"vite/client"
],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
@@ -15,14 +20,15 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": false,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["src"] "include": [
"src"
]
} }