Add experiments and tasks counter to project table [SCI-11944]

This commit is contained in:
Anton 2025-05-30 13:04:45 +02:00
parent 23e58a2fc2
commit 2e8152abc4
11 changed files with 203 additions and 61 deletions

View file

@ -1,43 +1,24 @@
<template>
<div class="relative leading-5 h-full flex items-center">
<div>
{{ i18n.t('experiments.card.completed_value', {
completed: params.data.completed_tasks,
all: params.data.total_tasks
}) }}
<div class="py-1">
<div class="w-24 h-1 bg-sn-light-grey">
<div class="h-full"
:class="{
'bg-sn-black': params.data.archived_on,
'bg-sn-blue': !params.data.archived_on
}"
:style="{
width: `${progress}%`
}"></div>
</div>
</div>
</div>
</div>
<CounterRenderer
:params="params"
totalField="total_tasks"
completedField="completed_tasks"
label="experiments.card.completed_value"
/>
</template>
<script>
import CounterRenderer from '../../shared/datatable/renderers/counter.vue';
export default {
name: 'CompletedTasksRenderer',
props: {
params: {
required: true,
},
},
computed: {
progress() {
const { completed_tasks: completedTasks, total_tasks: totalTasks } = this.params.data;
if (totalTasks === 0) return 3;
if (completedTasks === 0) return 3;
return (completedTasks / totalTasks) * 100;
required: true
}
},
components: {
CounterRenderer
}
};
</script>

View file

@ -81,6 +81,8 @@ import DataTable from '../shared/datatable/table.vue';
import UsersRenderer from './renderers/users.vue';
import NameRenderer from './renderers/name.vue';
import StatusRenderer from './renderers/status.vue';
import CompletedTasksRenderer from './renderers/completed_tasks.vue';
import CompletedExperimentsRenderer from './renderers/completed_experiments.vue';
import SuperviserRenderer from './renderers/superviser.vue';
import CommentsRenderer from '../shared/datatable/renderers/comments.vue';
import DueDateRenderer from '../shared/datatable/renderers/date.vue';
@ -115,7 +117,9 @@ export default {
DescriptionModal,
StatusRenderer,
SuperviserRenderer,
FavoriteRenderer
FavoriteRenderer,
CompletedTasksRenderer,
CompletedExperimentsRenderer
},
props: {
dataSource: { type: String, required: true },
@ -226,6 +230,20 @@ export default {
cellRenderer: SuperviserRenderer,
notSelectable: true
},
{
field: 'completed_experiments',
headerName: this.i18n.t('projects.index.card.completed_experiment'),
cellRenderer: CompletedExperimentsRenderer,
sortable: true,
minWidth: 110
},
{
field: 'completed_tasks',
headerName: this.i18n.t('experiments.table.column.completed_task'),
cellRenderer: CompletedTasksRenderer,
sortable: true,
minWidth: 110
},
{
field: 'created_at',
headerName: this.i18n.t('projects.index.card.start_date'),

View file

@ -0,0 +1,25 @@
<template>
<CounterRenderer
v-if="!params.data.folder"
:params="params"
totalField="total_experiments"
completedField="completed_experiments"
label="projects.index.card.completed_experiments"
/>
</template>
<script>
import CounterRenderer from '../../shared/datatable/renderers/counter.vue';
export default {
name: 'CompletedExperimentsRenderer',
props: {
params: {
required: true
}
},
components: {
CounterRenderer
}
};
</script>

View file

@ -0,0 +1,25 @@
<template>
<CounterRenderer
v-if="!params.data.folder"
:params="params"
totalField="total_tasks"
completedField="completed_tasks"
label="experiments.card.completed_value"
/>
</template>
<script>
import CounterRenderer from '../../shared/datatable/renderers/counter.vue';
export default {
name: 'CompletedTasksRenderer',
props: {
params: {
required: true
}
},
components: {
CounterRenderer
}
};
</script>

View file

@ -0,0 +1,47 @@
<template>
<div class="relative leading-5 h-full flex items-center">
<div>
{{ i18n.t(label, {
completed: this.params.data[this.completedField],
all: this.params.data[this.totalField]
}) }}
<div class="py-1">
<div class="w-24 h-1 bg-sn-light-grey">
<div class="h-full"
:class="{
'bg-sn-black': params.data.archived_on,
'bg-sn-blue': !params.data.archived_on
}"
:style="{
width: `${progress}%`
}"></div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CounterRenderer',
props: {
params: {
required: true
},
totalField: String,
completedField: String,
label: String
},
computed: {
progress() {
const completedCounter = this.params.data[this.completedField];
const totalCounter = this.params.data[this.totalField];
if (totalCounter === 0) return 3;
if (completedCounter === 0) return 3;
return (completedCounter / totalCounter) * 100;
}
}
};
</script>

View file

@ -6,17 +6,34 @@ module Lists
include Canaid::Helpers::PermissionsHelper
include CommentHelper
attributes :name, :code, :created_at, :archived_on, :users, :urls, :folder, :hidden,
:folder_info, :default_public_user_role_id, :team, :top_level_assignable, :supervised_by,
:comments, :updated_at, :permissions, :due_date_cell, :start_on_cell, :description, :status, :favorite,
def team
object.team.name
end
attributes :name, :code, :created_at, :archived_on, :users, :urls, :folder, :hidden, :completed_experiments, :completed_tasks, :total_tasks,
:folder_info, :default_public_user_role_id, :team, :top_level_assignable, :supervised_by, :total_experiments,
:comments, :updated_at, :permissions, :due_date_cell, :start_on_cell, :description, :status, :favorite
def team
object.team.name
end
def folder
!project?
end
def completed_experiments
object[:completed_experiments_count]
end
def total_experiments
object[:experiments_count]
end
def completed_tasks
object[:completed_tasks_count]
end
def total_tasks
object[:tasks_count]
end
def favorite
object.favorite if project?
end

View file

@ -5,15 +5,21 @@ module Lists
private
def fetch_records
done_status_id = MyModuleStatusFlow.first.final_status.id
@records = @raw_data.joins(:project)
.includes(my_modules: { my_module_status: :my_module_status_implications })
.includes(workflowimg_attachment: :blob, user_assignments: %i(user_role user))
.joins('LEFT OUTER JOIN my_modules AS active_tasks ON
active_tasks.experiment_id = experiments.id
AND active_tasks.archived = FALSE')
.joins('LEFT OUTER JOIN my_modules AS active_completed_tasks ON
active_completed_tasks.experiment_id = experiments.id
AND active_completed_tasks.archived = FALSE AND active_completed_tasks.state = 1')
.joins(
ActiveRecord::Base.sanitize_sql_array([
'LEFT OUTER JOIN my_modules AS active_completed_tasks ON
active_completed_tasks.experiment_id = experiments.id
AND active_completed_tasks.archived = FALSE AND active_completed_tasks.my_module_status_id = ?',
done_status_id
])
)
.readable_by_user(@user)
.with_favorites(@user)
.select('experiments.*')
@ -54,21 +60,19 @@ module Lists
@records = @records.where('experiments.due_date <= ?', @filters[:due_date_to]) if @filters[:due_date_to].present?
if @filters[:updated_on_from].present?
@records = @records.where('experiments.updated_at > ?', @filters[:updated_on_from])
end
@records = @records.where('experiments.updated_at > ?', @filters[:updated_on_from]) if @filters[:updated_on_from].present?
if @filters[:updated_on_to].present?
@records = @records.where('experiments.updated_at < ?',
@filters[:updated_on_to])
@filters[:updated_on_to])
end
if @filters[:archived_on_from].present?
@records = @records.where('COALESCE(experiments.archived_on, projects.archived_on) > ?',
@filters[:archived_on_from])
@filters[:archived_on_from])
end
if @filters[:archived_on_to].present?
@records = @records.where('COALESCE(experiments.archived_on, projects.archived_on) < ?',
@filters[:archived_on_to])
@filters[:archived_on_to])
end
if @filters[:statuses].present?

View file

@ -35,13 +35,35 @@ module Lists
private
def fetch_projects
done_status_id = MyModuleStatusFlow.first.final_status.id
@team.projects
.includes(:team, :project_comments, user_assignments: %i(user user_role))
.joins('LEFT OUTER JOIN experiments AS active_experiments ON
active_experiments.project_id = projects.id
AND active_experiments.archived = FALSE')
.joins('LEFT OUTER JOIN experiments AS active_completed_experiments ON
active_completed_experiments.project_id = projects.id
AND active_completed_experiments.archived = FALSE AND active_completed_experiments.completed_at IS NOT NULL')
.joins('LEFT OUTER JOIN my_modules AS active_tasks ON
active_tasks.experiment_id = active_experiments.id
AND active_tasks.archived = FALSE')
.joins(
ActiveRecord::Base.sanitize_sql_array([
'LEFT OUTER JOIN my_modules AS active_completed_tasks ON
active_completed_tasks.experiment_id = active_experiments.id
AND active_completed_tasks.archived = FALSE AND active_completed_tasks.my_module_status_id = ?',
done_status_id
])
)
.with_favorites(@user)
.visible_to(@user, @team)
.left_outer_joins(:project_comments)
.select('projects.*')
.select('COUNT(DISTINCT comments.id) AS comment_count')
.select('COUNT(DISTINCT active_experiments.id) AS experiments_count')
.select('COUNT(DISTINCT active_completed_experiments.id) AS completed_experiments_count')
.select('COUNT(DISTINCT active_tasks.id) AS tasks_count')
.select('COUNT(DISTINCT active_completed_tasks.id) AS completed_tasks_count')
.group('projects.id, favorites.id')
end
@ -64,27 +86,20 @@ module Lists
search_query = @params[:search].presence || @filters[:query]
records = records.where_attributes_like(['projects.name', Project::PREFIXED_ID_SQL, 'projects.description'], search_query) if search_query.present?
if @filters[:members].present?
records = records.joins(:user_assignments).where(user_assignments: { user_id: @filters[:members].values })
end
records = records.joins(:user_assignments).where(user_assignments: { user_id: @filters[:members].values }) if @filters[:members].present?
records = records.where(supervised_by_id: @filters[:head_of_project].values) if @filters[:head_of_project].present?
records = records.where('projects.start_on >= ?', @filters[:start_on_from]) if @filters[:start_on_from].present?
records = records.where(projects: { start_on: (@filters[:start_on_from]).. }) if @filters[:start_on_from].present?
records = records.where('projects.start_on <= ?', @filters[:start_on_to]) if @filters[:start_on_to].present?
records = records.where(projects: { start_on: ..(@filters[:start_on_to]) }) if @filters[:start_on_to].present?
records = records.where('projects.due_date >= ?', @filters[:due_date_from]) if @filters[:due_date_from].present?
records = records.where(projects: { due_date: (@filters[:due_date_from]).. }) if @filters[:due_date_from].present?
records = records.where('projects.due_date <= ?', @filters[:due_date_to]) if @filters[:due_date_to].present?
records = records.where(projects: { due_date: ..(@filters[:due_date_to]) }) if @filters[:due_date_to].present?
if @filters[:archived_on_to].present?
records = records.where('projects.archived_on < ?',
@filters[:archived_on_to])
end
if @filters[:archived_on_from].present?
records = records.where('projects.archived_on > ?', @filters[:archived_on_from])
end
records = records.where(projects: { archived_on: ...(@filters[:archived_on_to]) }) if @filters[:archived_on_to].present?
records = records.where('projects.archived_on > ?', @filters[:archived_on_from]) if @filters[:archived_on_from].present?
if @filters[:statuses].present?
scopes = {

View file

@ -674,6 +674,8 @@ en:
description: 'Description'
supervised_by: 'Head of project'
status: 'Status'
completed_experiment: "Experiments done"
completed_experiments: "%{completed}/%{all} experiments"
end_of_list_placeholder: 'Youve reached the end of the list'
folder:
description: "%{projects_count} projects | %{folders_count} folders"

View file

@ -11,6 +11,10 @@ RSpec.describe Lists::ExperimentsService do
let(:params) {{ page: 1, per_page: 10, search: '', project: project } }
let(:service) { described_class.new(raw_data, params, user:) }
before(:all) do
MyModuleStatusFlow.ensure_default
end
describe '#fetch_records' do
context 'when view_mode is archived' do
before do

View file

@ -11,6 +11,10 @@ RSpec.describe Lists::ProjectsService do
let(:params) {{ page: 1, per_page: 10, search: '', team: team } }
let(:service) { described_class.new(team, user, folder, params) }
before(:all) do
MyModuleStatusFlow.ensure_default
end
describe '#call' do
context 'when view_mode is archived' do
before do