import React, { useState, useEffect } from 'react';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import axios from 'axios';
const statuses = [
{ id: 'todo', title: 'To Do', color: '#FF6347' },
{ id: 'progress', title: 'On Progress', color: '#FFA500' },
{ id: 'done', title: 'Done', color: '#32CD32' },
{ id: 'archived', title: 'Archived', color: '#A9A9A9' },
];
const API_BASE_URL = 'http://localhost:3000'; // Ganti sesuai backend Anda
function App() {
// Data state
const [tasks, setTasks] = useState([]);
const [pics, setPics] = useState([]);
const [taskOptions, setTaskOptions] = useState([]);
// Modal control state
const [modalOpen, setModalOpen] = useState(null); // 'task' | 'pic' | 'activity'
| null
// Form states
const [newTaskContent, setNewTaskContent] = useState('');
const [newTaskPic, setNewTaskPic] = useState('');
const [newTaskStatus, setNewTaskStatus] = useState('todo');
const [newTaskDetail, setNewTaskDetail] = useState('');
const [newPicName, setNewPicName] = useState('');
const [newActivityName, setNewActivityName] = useState('');
// Load initial data
useEffect(() => {
fetchData();
}, []);
const fetchData = () => {
axios.get(`${API_BASE_URL}/api/tasks`).then((res) => setTasks(res.data));
axios.get(`${API_BASE_URL}/api/pics`).then((res) => setPics(res.data));
axios.get(`${API_BASE_URL}/api/task-options`).then((res) =>
setTaskOptions(res.data));
};
// Update task status and timestamps on drag end
const onDragEnd = (result) => {
const { destination, source, draggableId } = result;
if (!destination) return;
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
)
return;
const task = tasks.find((t) => t.id === draggableId);
if (!task) return;
const newStatus = destination.droppableId;
const now = new Date().toISOString();
const newTimestamps = { ...task.timestamps };
newTimestamps[newStatus] = now;
const updatedTask = {
...task,
status: newStatus,
timestamps: newTimestamps,
};
const newTasks = tasks.map((t) => (t.id === task.id ? updatedTask : t));
setTasks(newTasks);
axios
.post(`${API_BASE_URL}/api/tasks/update`, updatedTask)
.catch((err) => console.error('Error updating task:', err));
};
// Handlers for adding new data
const handleAddTask = async () => {
if (!newTaskContent || !newTaskPic) {
alert('Please select Task and PIC');
return;
}
const now = new Date().toISOString();
const newTask = {
content: newTaskContent,
pic: newTaskPic,
detail: newTaskDetail,
status: newTaskStatus,
timestamps: {
todo: null,
progress: null,
done: null,
archived: null,
},
};
newTask.timestamps[newTaskStatus] = now;
try {
await axios.post(`${API_BASE_URL}/api/tasks`, newTask);
fetchData();
closeModal();
resetTaskForm();
} catch (error) {
alert('Failed to add task');
console.error(error);
}
};
const handleAddPic = async () => {
if (!newPicName.trim()) {
alert('Please enter PIC name');
return;
}
try {
await axios.post(`${API_BASE_URL}/api/pics`, { name: newPicName.trim() });
fetchData();
closeModal();
setNewPicName('');
} catch (error) {
alert('Failed to add PIC');
console.error(error);
}
};
const handleAddActivity = async () => {
if (!newActivityName.trim()) {
alert('Please enter Activity name');
return;
}
try {
await axios.post(`${API_BASE_URL}/api/task-options`, { name:
newActivityName.trim() });
fetchData();
closeModal();
setNewActivityName('');
} catch (error) {
alert('Failed to add Activity');
console.error(error);
}
};
const closeModal = () => setModalOpen(null);
const resetTaskForm = () => {
setNewTaskContent('');
setNewTaskPic('');
setNewTaskStatus('todo');
setNewTaskDetail('');
};
// Modal component
const Modal = ({ children }) => (
<div
style={{
position: 'fixed',
top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
}}
onClick={closeModal}
>
<div
style={{
backgroundColor: 'white',
padding: 20,
borderRadius: 8,
minWidth: 320,
maxWidth: '90%',
boxShadow: '0 2px 10px rgba(0,0,0,0.3)',
}}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
);
return (
<div>
{/* Navbar */}
<nav
style={{
backgroundColor: '#333',
color: 'white',
padding: '10px 20px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div style={{ fontWeight: 'bold', fontSize: 20 }}>Kanban App</div>
<div>
<button
style={navButtonStyle}
onClick={() => setModalOpen('task')}
>
Add Task
</button>
<button
style={navButtonStyle}
onClick={() => setModalOpen('pic')}
>
Add PIC
</button>
<button
style={navButtonStyle}
onClick={() => setModalOpen('activity')}
>
Add Activity
</button>
</div>
</nav>
{/* Kanban Board */}
<DragDropContext onDragEnd={onDragEnd}>
<div style={{ display: 'flex', padding: 20, gap: 20 }}>
{statuses.map(({ id, title, color }) => (
<Droppable droppableId={id} key={id}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
style={{
backgroundColor: '#f0f0f0',
padding: 10,
borderRadius: 5,
width: 280,
minHeight: 500,
}}
>
<h3 style={{ color }}>{title}</h3>
{tasks
.filter((task) => task.status === id)
.map((task, index) => (
<Draggable
draggableId={task.id}
index={index}
key={task.id}
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
userSelect: 'none',
padding: 16,
margin: '0 0 8px 0',
minHeight: '50px',
backgroundColor: color,
color: 'white',
borderRadius: 4,
boxShadow: snapshot.isDragging
? '0 2px 8px rgba(0,0,0,0.2)'
: 'none',
...provided.draggableProps.style,
}}
>
<div><strong>{task.content}</strong></div>
<div>PIC: {task.pic}</div>
<div>Detail: {task.detail}</div>
<small>
Timestamps:
<ul style={{ paddingLeft: 15, margin: 0 }}>
{Object.entries(task.timestamps).map(
([statusKey, time]) => (
<li key={statusKey}>
{statuses.find((s) => s.id ===
statusKey)?.title} :{' '}
{time ? new Date(time).toLocaleString() :
'-'}
</li>
)
)}
</ul>
</small>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
))}
</div>
</DragDropContext>
{/* Modal Forms */}
{modalOpen === 'task' && (
<Modal>
<h3>Add New Task</h3>
<div style={{ marginBottom: 10 }}>
<label>
Task:{' '}
<select
value={newTaskContent}
onChange={(e) => setNewTaskContent(e.target.value)}
>
<option value="">-- Select Task --</option>
{taskOptions.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
</label>
</div>
<div style={{ marginBottom: 10 }}>
<label>
PIC:{' '}
<select
value={newTaskPic}
onChange={(e) => setNewTaskPic(e.target.value)}
>
<option value="">-- Select PIC --</option>
{pics.map((pic) => (
<option key={pic} value={pic}>
{pic}
</option>
))}
</select>
</label>
</div>
<div style={{ marginBottom: 10 }}>
<label>
Status:{' '}
<select
value={newTaskStatus}
onChange={(e) => setNewTaskStatus(e.target.value)}
>
{statuses.map(({ id, title }) => (
<option key={id} value={id}>
{title}
</option>
))}
</select>
</label>
</div>
<div style={{ marginBottom: 10 }}>
<label>
Detail:{' '}
<input
type="text"
value={newTaskDetail}
onChange={(e) => setNewTaskDetail(e.target.value)}
placeholder="Task detail"
style={{ width: '100%' }}
/>
</label>
</div>
<div style={{ textAlign: 'right' }}>
<button onClick={closeModal} style={{ marginRight: 10 }}>
Cancel
</button>
<button onClick={handleAddTask}>Add Task</button>
</div>
</Modal>
)}
{modalOpen === 'pic' && (
<Modal>
<h3>Add New PIC</h3>
<div style={{ marginBottom: 10 }}>
<label>
PIC Name:{' '}
<input
type="text"
value={newPicName}
onChange={(e) => setNewPicName(e.target.value)}
placeholder="Enter PIC name"
style={{ width: '100%' }}
/>
</label>
</div>
<div style={{ textAlign: 'right' }}>
<button onClick={closeModal} style={{ marginRight: 10 }}>
Cancel
</button>
<button onClick={handleAddPic}>Add PIC</button>
</div>
</Modal>
)}
{modalOpen === 'activity' && (
<Modal>
<h3>Add New Activity</h3>
<div style={{ marginBottom: 10 }}>
<label>
Activity Name:{' '}
<input
type="text"
value={newActivityName}
onChange={(e) => setNewActivityName(e.target.value)}
placeholder="Enter activity name"
style={{ width: '100%' }}
/>
</label>
</div>
<div style={{ textAlign: 'right' }}>
<button onClick={closeModal} style={{ marginRight: 10 }}>
Cancel
</button>
<button onClick={handleAddActivity}>Add Activity</button>
</div>
</Modal>
)}
</div>
);
}
const navButtonStyle = {
backgroundColor: '#555',
color: 'white',
border: 'none',
padding: '8px 12px',
marginLeft: 10,
borderRadius: 4,
cursor: 'pointer',
};
export default App;