diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 59ba74b57..d870af84c 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -1490,6 +1490,214 @@ class ProjectController extends AbstractController return Base::retSuccess('success', $data); } + /** + * @api {get} api/project/user/projects 会员参与的项目列表 + * + * @apiDescription 需要token身份。用于会员卡片查看「该会员参与的项目」。 + * 权限:本人 / 系统管理员 / 对该会员具有部门负责人只读视角。 + * @apiVersion 1.0.0 + * @apiGroup project + * @apiName user__projects + * + * @apiParam {Number} userid 目标会员ID + * @apiParam {String} [archived] 是否归档(all/yes/no),默认no + * @apiParam {Object} [keys] 搜索条件(keys.name 项目名称) + * @apiParam {Number} [page] 当前页,默认1 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function user__projects() + { + $viewer = User::auth(); + $targetId = intval(Request::input('userid')); + $context = UserDepartment::userWorksContext($viewer, $targetId); + if (!$context['allowed']) { + return Base::retError('没有查看权限'); + } + $readonly = !$context['is_self'] && !$context['is_admin']; + // + $archived = Request::input('archived', 'no'); + $keys = Request::input('keys'); + // + $builder = Project::select(['projects.*', 'project_users.owner', 'project_users.top_at', 'project_users.sort']) + ->join('project_users', function ($join) use ($targetId) { + $join->on('projects.id', '=', 'project_users.project_id') + ->where('project_users.userid', '=', $targetId); + }); + // 部门负责人视角:限定在允许可见的项目集合内 + if ($readonly) { + $builder->whereIn('projects.id', $context['project_ids'] ?: [0]); + } + // + if ($archived == 'yes') { + $builder->whereNotNull('projects.archived_at'); + } elseif ($archived == 'no') { + $builder->whereNull('projects.archived_at'); + } + if (is_array($keys) && !empty($keys['name'])) { + $builder->where('projects.name', 'like', "%{$keys['name']}%"); + } + // + $list = $builder + ->orderByDesc('project_users.top_at') + ->orderBy('project_users.sort') + ->orderByDesc('projects.id') + ->paginate(Base::getPaginate(100, 50)); + $list->transform(function (Project $project) use ($targetId, $readonly) { + $array = $project->toArray(); + $array['department_readonly'] = $readonly; + $array = array_merge($array, $project->getTaskStatistics($targetId)); + return $array; + }); + // + return Base::retSuccess('success', $list); + } + + /** + * @api {get} api/project/user/tasks 会员参与的任务列表 + * + * @apiDescription 需要token身份。用于会员卡片查看「该会员参与的任务」(负责的 / 协作的)。 + * 权限:本人 / 系统管理员 / 对该会员具有部门负责人只读视角。 + * @apiVersion 1.0.0 + * @apiGroup project + * @apiName user__tasks + * + * @apiParam {Number} userid 目标会员ID + * @apiParam {Number} [owner] 任务身份筛选:1=负责的,0=协作的,不传=全部 + * @apiParam {Number} [project_id] 仅查询指定项目 + * @apiParam {Object} [keys] 搜索条件(keys.name 任务名称,keys.status completed/uncompleted) + * @apiParam {Number} [page] 当前页,默认1 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function user__tasks() + { + $viewer = User::auth(); + $targetId = intval(Request::input('userid')); + $context = UserDepartment::userWorksContext($viewer, $targetId); + if (!$context['allowed']) { + return Base::retError('没有查看权限'); + } + $readonly = !$context['is_self'] && !$context['is_admin']; + // + $owner = Request::input('owner'); + $owner = is_numeric($owner) ? intval($owner) : null; + $project_id = intval(Request::input('project_id')); + $keys = Request::input('keys'); + $keys = is_array($keys) ? $keys : []; + // + $builder = ProjectTask::with(['taskUser', 'taskTag', 'project:id,name']) + ->select(['project_tasks.*', 'project_task_users.owner']) + ->join('project_task_users', function ($join) use ($targetId) { + $join->on('project_tasks.id', '=', 'project_task_users.task_id') + ->where('project_task_users.userid', '=', $targetId); + }); + if ($owner !== null) { + $builder->where('project_task_users.owner', $owner); + } + // 部门负责人视角:限定可见项目集合,且仅"全员可见"(visibility=1)的任务(与 findForDepartmentView 一致,避免列出打不开的任务) + if ($readonly) { + $builder->whereIn('project_tasks.project_id', $context['project_ids'] ?: [0]); + $builder->where('project_tasks.visibility', 1); + } + if ($project_id > 0) { + $builder->where('project_tasks.project_id', $project_id); + } + if (!empty($keys['name'])) { + $builder->where(function ($query) use ($keys) { + $query->where('project_tasks.name', 'like', "%{$keys['name']}%") + ->orWhere('project_tasks.desc', 'like', "%{$keys['name']}%"); + }); + } + if (!empty($keys['status'])) { + if ($keys['status'] == 'completed') { + $builder->whereNotNull('project_tasks.complete_at'); + } elseif ($keys['status'] == 'uncompleted') { + $builder->whereNull('project_tasks.complete_at'); + } + } + $builder->whereNull('project_tasks.archived_at'); + // + $list = $builder->orderByDesc('project_tasks.id')->paginate(Base::getPaginate(100, 50)); + $list->transform(function (ProjectTask $task) use ($readonly) { + $task->setAppends(['today', 'overdue']); + $array = $task->toArray(); + $array['project_name'] = $array['project']['name'] ?? ''; + $array['department_readonly'] = $readonly; + unset($array['project']); + return $array; + }); + // + return Base::retSuccess('success', $list); + } + + /** + * @api {get} api/project/user/counts 会员参与的项目/任务数量 + * + * @apiDescription 需要token身份。用于会员卡片「项目与任务」弹窗的 Tab 角标,仅返回数量(轻量)。 + * 权限:本人 / 系统管理员 / 对该会员具有部门负责人只读视角。 + * @apiVersion 1.0.0 + * @apiGroup project + * @apiName user__counts + * + * @apiParam {Number} userid 目标会员ID + * @apiParam {Number} [owner] 任务身份筛选:1=负责的,0=协作的,不传=全部(仅影响任务数量) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data {project, todo, done} + */ + public function user__counts() + { + $viewer = User::auth(); + $targetId = intval(Request::input('userid')); + $context = UserDepartment::userWorksContext($viewer, $targetId); + if (!$context['allowed']) { + return Base::retError('没有查看权限'); + } + $readonly = !$context['is_self'] && !$context['is_admin']; + $owner = Request::input('owner'); + $owner = is_numeric($owner) ? intval($owner) : null; + // + $projectBuilder = Project::join('project_users', function ($join) use ($targetId) { + $join->on('projects.id', '=', 'project_users.project_id') + ->where('project_users.userid', '=', $targetId); + }) + ->whereNull('projects.archived_at'); + if ($readonly) { + $projectBuilder->whereIn('projects.id', $context['project_ids'] ?: [0]); + } + $projectCount = $projectBuilder->distinct()->count('projects.id'); + // + $taskBuilder = function () use ($targetId, $owner, $readonly, $context) { + $builder = ProjectTask::join('project_task_users', function ($join) use ($targetId) { + $join->on('project_tasks.id', '=', 'project_task_users.task_id') + ->where('project_task_users.userid', '=', $targetId); + }) + ->whereNull('project_tasks.archived_at'); + if ($owner !== null) { + $builder->where('project_task_users.owner', $owner); + } + if ($readonly) { + $builder->whereIn('project_tasks.project_id', $context['project_ids'] ?: [0]); + $builder->where('project_tasks.visibility', 1); + } + return $builder; + }; + $todoCount = $taskBuilder()->whereNull('project_tasks.complete_at')->count(); + $doneCount = $taskBuilder()->whereNotNull('project_tasks.complete_at')->count(); + // + return Base::retSuccess('success', [ + 'project' => $projectCount, + 'todo' => $todoCount, + 'done' => $doneCount, + ]); + } + /** * @api {get} api/project/task/easylists 任务列表-简单的 * diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 9228d27e0..85f06e81f 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -884,7 +884,8 @@ class UsersController extends AbstractController */ public function extra() { - $user = User::auth(); + $viewer = User::auth(); + $user = $viewer; // $userid = intval(Request::input('userid')); if ($userid <= 0) { @@ -919,6 +920,8 @@ class UsersController extends AbstractController $tagMeta = UserTag::listWithMeta($userid, $user); + $worksContext = UserDepartment::userWorksContext($viewer, $userid); + $data = [ 'userid' => $userid, 'birthday' => $birthday, @@ -926,6 +929,7 @@ class UsersController extends AbstractController 'introduction' => $introduction, 'personal_tags' => $tagMeta['top'], 'personal_tags_total' => $tagMeta['total'], + 'works_visible' => $worksContext['allowed'], ]; return Base::retSuccess('success', $data); diff --git a/app/Models/UserDepartment.php b/app/Models/UserDepartment.php index 6ff9d57ec..a529e757d 100644 --- a/app/Models/UserDepartment.php +++ b/app/Models/UserDepartment.php @@ -615,4 +615,69 @@ class UserDepartment extends AbstractModel return $project; } + /** + * 会员卡片「查看该会员项目/任务」的权限上下文。 + * 允许条件:本人 / 系统管理员 / 对该会员具有部门负责人只读视角。 + * @param User $viewer 当前登录用户 + * @param int $targetUserid 目标会员 + * @return array ['allowed'=>bool, 'is_self'=>bool, 'is_admin'=>bool, 'project_ids'=>int[]] + * project_ids 仅在部门负责人视角下有意义(限定可见项目集合);本人/管理员为空数组表示不限制 + */ + public static function userWorksContext(User $viewer, int $targetUserid): array + { + $result = [ + 'allowed' => false, + 'is_self' => false, + 'is_admin' => false, + 'project_ids' => [], + ]; + if ($targetUserid <= 0) { + return $result; + } + // 机器人/系统账号(或不存在)不展示项目与任务 + $target = User::select(['userid', 'bot'])->whereUserid($targetUserid)->first(); + if (empty($target) || $target->bot) { + return $result; + } + // 本人 + if ($viewer->userid === $targetUserid) { + $result['allowed'] = true; + $result['is_self'] = true; + return $result; + } + // 系统管理员 + if ($viewer->isAdmin()) { + $result['allowed'] = true; + $result['is_admin'] = true; + return $result; + } + // 部门负责人只读视角 + if (Base::settingFind('system', 'department_owner_project_view', 'close') !== 'open') { + return $result; + } + $memberUserids = self::getManagedMemberUserids($viewer->userid, 'all'); + if (!in_array($targetUserid, $memberUserids, true)) { + return $result; + } + // 目标会员参与、且未关闭「部门负责人视角可见」的项目 + $projectIds = ProjectUser::where('project_users.userid', $targetUserid) + ->join('projects', 'projects.id', '=', 'project_users.project_id') + ->whereNull('projects.deleted_at') + ->where(function ($query) { + $query->where('projects.department_owner_view', '<>', 'close') + ->orWhereNull('projects.department_owner_view'); + }) + ->distinct() + ->pluck('projects.id') + ->map(fn($v) => intval($v)) + ->values() + ->toArray(); + if (empty($projectIds)) { + return $result; + } + $result['allowed'] = true; + $result['project_ids'] = $projectIds; + return $result; + } + } diff --git a/language/original-api.txt b/language/original-api.txt index 3ca53e9b9..7a6d88e92 100644 --- a/language/original-api.txt +++ b/language/original-api.txt @@ -994,3 +994,4 @@ LDAP 用户缺少邮箱属性,请联系管理员配置 你有一条待办到提醒时间啦 发送者昵称最多不能超过20字 AI 助手 +没有查看权限 diff --git a/language/original-web.txt b/language/original-web.txt index e9629988b..cf770d82d 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -2440,3 +2440,12 @@ AI任务分析 暂无完成 取消提醒 确定取消该成员的提醒时间吗? +项目与任务 +暂无项目 +暂无任务 +负责 +协作 +成员 +(*)分钟前 +(*)小时前 +(*)天前 diff --git a/resources/assets/js/pages/manage/components/UserDetail.vue b/resources/assets/js/pages/manage/components/UserDetail.vue index 4d06ac3e6..78272d25f 100755 --- a/resources/assets/js/pages/manage/components/UserDetail.vue +++ b/resources/assets/js/pages/manage/components/UserDetail.vue @@ -26,8 +26,12 @@
@@ -115,6 +119,13 @@ :total-count="commonDialog.total || 0" @open-chat="onOpenCommonDialogChat" /> + +{{$L('暂无项目')}}
+{{$L('暂无任务')}}
+