mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-17 04:15:05 +00:00
feat: Implement task approval, rejection, and cancellation with new statuses and task filtering capabilities.
This commit is contained in:
1 parent
94e7aac3ee
commit
2308b2553c
13 files changed
+344
-30
No files matched your search
+2
-1
@@ -42,4 +42,5 @@ k8s-*/*
|
||||
|
||||
# Test credentials (never commit)
|
||||
tests/**/creds.json
|
||||
ui/simple-saas
|
||||
ui/simple-saas
|
||||
*.tar
|
||||
@@ -125,7 +125,9 @@ func (m *TeamsMethods) handleDelete(ctx context.Context, client *gateway.Client,
|
||||
// --- Task List (admin view) ---
|
||||
|
||||
type teamsTaskListParams struct {
|
||||
TeamID string `json:"teamId"`
|
||||
TeamID string `json:"teamId"`
|
||||
StatusFilter string `json:"statusFilter,omitempty"`
|
||||
UserID string `json:"userId,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
@@ -152,7 +154,12 @@ func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Clien
|
||||
return
|
||||
}
|
||||
|
||||
tasks, err := m.teamStore.ListTasks(ctx, teamID, "newest", store.TeamTaskFilterAll, "")
|
||||
statusFilter := params.StatusFilter
|
||||
if statusFilter == "" {
|
||||
statusFilter = store.TeamTaskFilterAll
|
||||
}
|
||||
|
||||
tasks, err := m.teamStore.ListTasks(ctx, teamID, "newest", statusFilter, params.UserID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, err.Error()))
|
||||
return
|
||||
|
||||
@@ -132,6 +132,8 @@ export const Methods = {
|
||||
TEAMS_GET: "teams.get",
|
||||
TEAMS_DELETE: "teams.delete",
|
||||
TEAMS_TASK_LIST: "teams.tasks.list",
|
||||
TEAMS_TASK_APPROVE: "teams.tasks.approve",
|
||||
TEAMS_TASK_REJECT: "teams.tasks.reject",
|
||||
TEAMS_MEMBERS_ADD: "teams.members.add",
|
||||
TEAMS_MEMBERS_REMOVE: "teams.members.remove",
|
||||
TEAMS_UPDATE: "teams.update",
|
||||
|
||||
@@ -72,6 +72,28 @@
|
||||
"taskId": "Task ID:",
|
||||
"noDescription": "No description",
|
||||
"noResult": "No result yet"
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"cancel": "Cancel",
|
||||
"reasonPlaceholder": "Reason (optional)...",
|
||||
"confirmCancel": "Cancel this task?",
|
||||
"confirmReject": "Reject this task?"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"active": "Active",
|
||||
"completed": "Completed",
|
||||
"allUsers": "All users"
|
||||
},
|
||||
"status": {
|
||||
"pending": "pending",
|
||||
"pending_approval": "pending approval",
|
||||
"in_progress": "in progress",
|
||||
"completed": "completed",
|
||||
"blocked": "blocked",
|
||||
"failed": "failed"
|
||||
}
|
||||
},
|
||||
"delegations": {
|
||||
@@ -145,6 +167,12 @@
|
||||
"failedDelete": "Failed to delete team",
|
||||
"failedUpdate": "Failed to update team",
|
||||
"failedAddMember": "Failed to add member",
|
||||
"failedRemoveMember": "Failed to remove member"
|
||||
"failedRemoveMember": "Failed to remove member",
|
||||
"taskApproved": "Task approved",
|
||||
"taskRejected": "Task rejected",
|
||||
"taskCancelled": "Task cancelled",
|
||||
"failedApproveTask": "Failed to approve task",
|
||||
"failedRejectTask": "Failed to reject task",
|
||||
"failedCancelTask": "Failed to cancel task"
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,28 @@
|
||||
"taskId": "ID nhiệm vụ:",
|
||||
"noDescription": "Không có mô tả",
|
||||
"noResult": "Chưa có kết quả"
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Duyệt",
|
||||
"reject": "Từ chối",
|
||||
"cancel": "Huỷ",
|
||||
"reasonPlaceholder": "Lý do (tùy chọn)...",
|
||||
"confirmCancel": "Huỷ nhiệm vụ này?",
|
||||
"confirmReject": "Từ chối nhiệm vụ này?"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Tất cả",
|
||||
"active": "Đang hoạt động",
|
||||
"completed": "Đã hoàn thành",
|
||||
"allUsers": "Tất cả người dùng"
|
||||
},
|
||||
"status": {
|
||||
"pending": "chờ xử lý",
|
||||
"pending_approval": "chờ duyệt",
|
||||
"in_progress": "đang thực hiện",
|
||||
"completed": "hoàn thành",
|
||||
"blocked": "bị chặn",
|
||||
"failed": "thất bại"
|
||||
}
|
||||
},
|
||||
"delegations": {
|
||||
@@ -145,6 +167,12 @@
|
||||
"failedDelete": "Không thể xóa nhóm",
|
||||
"failedUpdate": "Không thể cập nhật nhóm",
|
||||
"failedAddMember": "Không thể thêm thành viên",
|
||||
"failedRemoveMember": "Không thể xóa thành viên"
|
||||
"failedRemoveMember": "Không thể xóa thành viên",
|
||||
"taskApproved": "Đã duyệt nhiệm vụ",
|
||||
"taskRejected": "Đã từ chối nhiệm vụ",
|
||||
"taskCancelled": "Đã huỷ nhiệm vụ",
|
||||
"failedApproveTask": "Không thể duyệt nhiệm vụ",
|
||||
"failedRejectTask": "Không thể từ chối nhiệm vụ",
|
||||
"failedCancelTask": "Không thể huỷ nhiệm vụ"
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,28 @@
|
||||
"taskId": "任务 ID:",
|
||||
"noDescription": "无描述",
|
||||
"noResult": "暂无结果"
|
||||
},
|
||||
"actions": {
|
||||
"approve": "批准",
|
||||
"reject": "拒绝",
|
||||
"cancel": "取消",
|
||||
"reasonPlaceholder": "原因(可选)...",
|
||||
"confirmCancel": "取消此任务?",
|
||||
"confirmReject": "拒绝此任务?"
|
||||
},
|
||||
"filter": {
|
||||
"all": "全部",
|
||||
"active": "进行中",
|
||||
"completed": "已完成",
|
||||
"allUsers": "所有用户"
|
||||
},
|
||||
"status": {
|
||||
"pending": "待处理",
|
||||
"pending_approval": "待审批",
|
||||
"in_progress": "进行中",
|
||||
"completed": "已完成",
|
||||
"blocked": "被阻塞",
|
||||
"failed": "失败"
|
||||
}
|
||||
},
|
||||
"delegations": {
|
||||
@@ -145,6 +167,12 @@
|
||||
"failedDelete": "删除Team失败",
|
||||
"failedUpdate": "更新Team失败",
|
||||
"failedAddMember": "添加成员失败",
|
||||
"failedRemoveMember": "移除成员失败"
|
||||
"failedRemoveMember": "移除成员失败",
|
||||
"taskApproved": "任务已批准",
|
||||
"taskRejected": "任务已拒绝",
|
||||
"taskCancelled": "任务已取消",
|
||||
"failedApproveTask": "批准任务失败",
|
||||
"failedRejectTask": "拒绝任务失败",
|
||||
"failedCancelTask": "取消任务失败"
|
||||
}
|
||||
}
|
||||
@@ -58,16 +58,30 @@ export function useTeams() {
|
||||
);
|
||||
|
||||
const getTeamTasks = useCallback(
|
||||
async (teamId: string) => {
|
||||
async (teamId: string, statusFilter?: string, userId?: string) => {
|
||||
const res = await ws.call<{ tasks: TeamTaskData[]; count: number }>(
|
||||
Methods.TEAMS_TASK_LIST,
|
||||
{ teamId },
|
||||
{ teamId, statusFilter, userId },
|
||||
);
|
||||
return res;
|
||||
},
|
||||
[ws],
|
||||
);
|
||||
|
||||
const approveTask = useCallback(
|
||||
async (taskId: string) => {
|
||||
await ws.call(Methods.TEAMS_TASK_APPROVE, { task_id: taskId });
|
||||
},
|
||||
[ws],
|
||||
);
|
||||
|
||||
const rejectTask = useCallback(
|
||||
async (taskId: string, reason?: string) => {
|
||||
await ws.call(Methods.TEAMS_TASK_REJECT, { task_id: taskId, reason });
|
||||
},
|
||||
[ws],
|
||||
);
|
||||
|
||||
const addMember = useCallback(
|
||||
async (teamId: string, agent: string, role?: string) => {
|
||||
await ws.call(Methods.TEAMS_MEMBERS_ADD, { teamId, agent, role });
|
||||
@@ -100,5 +114,5 @@ export function useTeams() {
|
||||
[ws],
|
||||
);
|
||||
|
||||
return { teams, loading, load, createTeam, deleteTeam, getTeam, getTeamTasks, addMember, removeMember, updateTeamSettings, getKnownUsers };
|
||||
return { teams, loading, load, createTeam, deleteTeam, getTeam, getTeamTasks, approveTask, rejectTask, addMember, removeMember, updateTeamSettings, getKnownUsers };
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -5,18 +6,56 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import type { TeamTaskData } from "@/types/team";
|
||||
import { taskStatusBadgeVariant } from "./task-utils";
|
||||
import { taskStatusBadgeVariant, isTaskActionable } from "./task-utils";
|
||||
|
||||
interface TaskDetailDialogProps {
|
||||
task: TeamTaskData;
|
||||
onClose: () => void;
|
||||
onApprove: (taskId: string) => Promise<void>;
|
||||
onReject: (taskId: string, reason?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function TaskDetailDialog({ task, onClose }: TaskDetailDialogProps) {
|
||||
export function TaskDetailDialog({ task, onClose, onApprove, onReject }: TaskDetailDialogProps) {
|
||||
const { t } = useTranslation("teams");
|
||||
const [reason, setReason] = useState("");
|
||||
const [acting, setActing] = useState(false);
|
||||
|
||||
const handleApprove = async () => {
|
||||
setActing(true);
|
||||
try {
|
||||
await onApprove(task.id);
|
||||
onClose();
|
||||
} finally {
|
||||
setActing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
setActing(true);
|
||||
try {
|
||||
await onReject(task.id, reason || undefined);
|
||||
onClose();
|
||||
} finally {
|
||||
setActing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
setActing(true);
|
||||
try {
|
||||
await onReject(task.id, reason || "Cancelled by user");
|
||||
onClose();
|
||||
} finally {
|
||||
setActing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const showActions = isTaskActionable(task.status);
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={() => onClose()}>
|
||||
@@ -37,7 +76,7 @@ export function TaskDetailDialog({ task, onClose }: TaskDetailDialogProps) {
|
||||
<div>
|
||||
<span className="text-muted-foreground">{t("tasks.detail.status")}</span>{" "}
|
||||
<Badge variant={taskStatusBadgeVariant(task.status)} className="text-xs">
|
||||
{task.status.replace("_", " ")}
|
||||
{t(`tasks.status.${task.status}`, task.status.replace("_", " "))}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
@@ -87,6 +126,34 @@ export function TaskDetailDialog({ task, onClose }: TaskDetailDialogProps) {
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
{showActions && (
|
||||
<div className="space-y-3 border-t pt-4">
|
||||
<Input
|
||||
placeholder={t("tasks.actions.reasonPlaceholder")}
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
{task.status === "pending_approval" ? (
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={handleReject} disabled={acting}>
|
||||
{t("tasks.actions.reject")}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleApprove} disabled={acting}>
|
||||
{t("tasks.actions.approve")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="destructive" size="sm" onClick={handleCancel} disabled={acting}>
|
||||
{t("tasks.actions.cancel")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { useState } from "react";
|
||||
import { ClipboardList } from "lucide-react";
|
||||
import { ClipboardList, Check, X, Trash2 } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TeamTaskData } from "@/types/team";
|
||||
import { taskStatusBadgeVariant } from "./task-utils";
|
||||
import { taskStatusBadgeVariant, isTaskActionable } from "./task-utils";
|
||||
import { TaskDetailDialog } from "./task-detail-dialog";
|
||||
|
||||
interface TaskListProps {
|
||||
tasks: TeamTaskData[];
|
||||
loading: boolean;
|
||||
onApprove: (taskId: string) => Promise<void>;
|
||||
onReject: (taskId: string, reason?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function TaskList({ tasks, loading }: TaskListProps) {
|
||||
export function TaskList({ tasks, loading, onApprove, onReject }: TaskListProps) {
|
||||
const { t } = useTranslation("teams");
|
||||
const [selectedTask, setSelectedTask] = useState<TeamTaskData | null>(null);
|
||||
|
||||
@@ -38,16 +41,17 @@ export function TaskList({ tasks, loading }: TaskListProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-auto rounded-lg border">
|
||||
<div className="grid min-w-[400px] grid-cols-[1fr_90px_100px_60px] items-center gap-2 border-b bg-muted/50 px-4 py-2.5 text-xs font-medium text-muted-foreground">
|
||||
<div className="grid min-w-[500px] grid-cols-[1fr_110px_100px_60px_80px] items-center gap-2 border-b bg-muted/50 px-4 py-2.5 text-xs font-medium text-muted-foreground">
|
||||
<span>{t("tasks.columns.subject")}</span>
|
||||
<span>{t("tasks.columns.status")}</span>
|
||||
<span>{t("tasks.columns.owner")}</span>
|
||||
<span>{t("tasks.columns.priority")}</span>
|
||||
<span className="text-right">{t("tasks.columns.actions")}</span>
|
||||
</div>
|
||||
{tasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="grid min-w-[400px] cursor-pointer grid-cols-[1fr_90px_100px_60px] items-center gap-2 border-b px-4 py-3 last:border-0 hover:bg-muted/30"
|
||||
className="grid min-w-[500px] cursor-pointer grid-cols-[1fr_110px_100px_60px_80px] items-center gap-2 border-b px-4 py-3 last:border-0 hover:bg-muted/30"
|
||||
onClick={() => setSelectedTask(task)}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
@@ -64,7 +68,7 @@ export function TaskList({ tasks, loading }: TaskListProps) {
|
||||
)}
|
||||
</div>
|
||||
<Badge variant={taskStatusBadgeVariant(task.status)}>
|
||||
{task.status.replace("_", " ")}
|
||||
{t(`tasks.status.${task.status}`, task.status.replace("_", " "))}
|
||||
</Badge>
|
||||
<span className="truncate text-sm text-muted-foreground">
|
||||
{task.owner_agent_key || "—"}
|
||||
@@ -72,6 +76,41 @@ export function TaskList({ tasks, loading }: TaskListProps) {
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{task.priority}
|
||||
</span>
|
||||
<div className="flex justify-end gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
{task.status === "pending_approval" && (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-emerald-600 hover:text-emerald-700"
|
||||
title={t("tasks.actions.approve")}
|
||||
onClick={() => onApprove(task.id)}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive hover:text-destructive/80"
|
||||
title={t("tasks.actions.reject")}
|
||||
onClick={() => onReject(task.id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{isTaskActionable(task.status) && task.status !== "pending_approval" && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive hover:text-destructive/80"
|
||||
title={t("tasks.actions.cancel")}
|
||||
onClick={() => onReject(task.id, "Cancelled by user")}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -80,6 +119,8 @@ export function TaskList({ tasks, loading }: TaskListProps) {
|
||||
<TaskDetailDialog
|
||||
task={selectedTask}
|
||||
onClose={() => setSelectedTask(null)}
|
||||
onApprove={onApprove}
|
||||
onReject={onReject}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
export function taskStatusBadgeVariant(status: string) {
|
||||
switch (status) {
|
||||
case "pending": return "outline" as const;
|
||||
case "pending_approval": return "default" as const;
|
||||
case "in_progress": return "info" as const;
|
||||
case "completed": return "success" as const;
|
||||
case "blocked": return "warning" as const;
|
||||
case "failed": return "destructive" as const;
|
||||
default: return "outline" as const;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the task can be acted on (approve/reject/cancel) */
|
||||
export function isTaskActionable(status: string) {
|
||||
return status !== "completed" && status !== "failed";
|
||||
}
|
||||
@@ -20,7 +20,7 @@ interface TeamDetailPageProps {
|
||||
|
||||
export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) {
|
||||
const { t } = useTranslation("teams");
|
||||
const { getTeam, getTeamTasks, addMember, removeMember, deleteTeam } = useTeams();
|
||||
const { getTeam, getTeamTasks, approveTask, rejectTask, getKnownUsers, addMember, removeMember, deleteTeam } = useTeams();
|
||||
const [team, setTeam] = useState<TeamData | null>(null);
|
||||
const [members, setMembers] = useState<TeamMemberData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -134,7 +134,13 @@ export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tasks" className="mt-4">
|
||||
<TeamTasksTab teamId={teamId} getTeamTasks={getTeamTasks} />
|
||||
<TeamTasksTab
|
||||
teamId={teamId}
|
||||
getTeamTasks={getTeamTasks}
|
||||
getKnownUsers={getKnownUsers}
|
||||
onApprove={approveTask}
|
||||
onReject={rejectTask}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="delegations" className="mt-4">
|
||||
|
||||
@@ -3,44 +3,128 @@ import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMinLoading } from "@/hooks/use-min-loading";
|
||||
import { toast } from "@/stores/use-toast-store";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { TeamTaskData } from "@/types/team";
|
||||
import { TaskList } from "./task-sections";
|
||||
|
||||
interface TeamTasksTabProps {
|
||||
teamId: string;
|
||||
getTeamTasks: (teamId: string) => Promise<{ tasks: TeamTaskData[]; count: number }>;
|
||||
getTeamTasks: (teamId: string, statusFilter?: string, userId?: string) => Promise<{ tasks: TeamTaskData[]; count: number }>;
|
||||
getKnownUsers: (teamId: string) => Promise<string[]>;
|
||||
onApprove: (taskId: string) => Promise<void>;
|
||||
onReject: (taskId: string, reason?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function TeamTasksTab({ teamId, getTeamTasks }: TeamTasksTabProps) {
|
||||
const STATUS_FILTERS = ["all", "active", "completed"] as const;
|
||||
|
||||
export function TeamTasksTab({ teamId, getTeamTasks, getKnownUsers, onApprove, onReject }: TeamTasksTabProps) {
|
||||
const { t } = useTranslation("teams");
|
||||
const [tasks, setTasks] = useState<TeamTaskData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const spinning = useMinLoading(loading);
|
||||
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||
const [userFilter, setUserFilter] = useState<string>("");
|
||||
const [knownUsers, setKnownUsers] = useState<string[]>([]);
|
||||
|
||||
// Load known users once
|
||||
useEffect(() => {
|
||||
getKnownUsers(teamId).then(setKnownUsers).catch(() => {});
|
||||
}, [teamId, getKnownUsers]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getTeamTasks(teamId);
|
||||
const res = await getTeamTasks(teamId, statusFilter, userFilter || undefined);
|
||||
setTasks(res.tasks ?? []);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [teamId, getTeamTasks]);
|
||||
}, [teamId, getTeamTasks, statusFilter, userFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const handleApprove = useCallback(async (taskId: string) => {
|
||||
try {
|
||||
await onApprove(taskId);
|
||||
toast.success(t("toast.taskApproved"));
|
||||
load();
|
||||
} catch {
|
||||
toast.error(t("toast.failedApproveTask"));
|
||||
}
|
||||
}, [onApprove, load, t]);
|
||||
|
||||
const handleReject = useCallback(async (taskId: string, reason?: string) => {
|
||||
try {
|
||||
await onReject(taskId, reason);
|
||||
toast.success(t("toast.taskCancelled"));
|
||||
load();
|
||||
} catch {
|
||||
toast.error(t("toast.failedCancelTask"));
|
||||
}
|
||||
}, [onReject, load, t]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" size="sm" onClick={load} disabled={spinning} className="gap-1">
|
||||
<RefreshCw className={"h-3.5 w-3.5" + (spinning ? " animate-spin" : "")} /> {t("tasks.refresh")}
|
||||
</Button>
|
||||
{/* Filter bar */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Status filter toggle buttons */}
|
||||
<div className="flex rounded-md border">
|
||||
{STATUS_FILTERS.map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setStatusFilter(f)}
|
||||
className={
|
||||
"px-3 py-1.5 text-xs font-medium transition-colors first:rounded-l-md last:rounded-r-md" +
|
||||
(statusFilter === f
|
||||
? " bg-primary text-primary-foreground"
|
||||
: " hover:bg-muted")
|
||||
}
|
||||
>
|
||||
{t(`tasks.filter.${f}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* User filter dropdown */}
|
||||
{knownUsers.length > 0 && (
|
||||
<Select value={userFilter} onValueChange={(v) => setUserFilter(v === "_all" ? "" : v)}>
|
||||
<SelectTrigger className="h-8 w-auto min-w-[140px] text-xs">
|
||||
<SelectValue placeholder={t("tasks.filter.allUsers")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" className="max-h-[50vh] sm:max-h-96">
|
||||
<SelectItem value="_all">{t("tasks.filter.allUsers")}</SelectItem>
|
||||
{knownUsers.map((u) => (
|
||||
<SelectItem key={u} value={u}>{u}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<div className="ml-auto">
|
||||
<Button variant="outline" size="sm" onClick={load} disabled={spinning} className="gap-1">
|
||||
<RefreshCw className={"h-3.5 w-3.5" + (spinning ? " animate-spin" : "")} /> {t("tasks.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TaskList tasks={tasks} loading={loading} />
|
||||
|
||||
<TaskList
|
||||
tasks={tasks}
|
||||
loading={loading}
|
||||
onApprove={handleApprove}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -36,12 +36,13 @@ export interface TeamTaskData {
|
||||
team_id: string;
|
||||
subject: string;
|
||||
description?: string;
|
||||
status: "pending" | "in_progress" | "completed" | "blocked";
|
||||
status: "pending" | "pending_approval" | "in_progress" | "completed" | "blocked" | "failed";
|
||||
owner_agent_id?: string;
|
||||
owner_agent_key?: string;
|
||||
blocked_by?: string[];
|
||||
priority: number;
|
||||
result?: string;
|
||||
user_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
Reference in new issue
Block a user