mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-11-10 08:21:37 +08:00
Add experiments and tasks counter to project table [SCI-11944]
This commit is contained in:
parent
23e58a2fc2
commit
2e8152abc4
11 changed files with 203 additions and 61 deletions
|
|
@ -1,43 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relative leading-5 h-full flex items-center">
|
<CounterRenderer
|
||||||
<div>
|
:params="params"
|
||||||
{{ i18n.t('experiments.card.completed_value', {
|
totalField="total_tasks"
|
||||||
completed: params.data.completed_tasks,
|
completedField="completed_tasks"
|
||||||
all: params.data.total_tasks
|
label="experiments.card.completed_value"
|
||||||
}) }}
|
/>
|
||||||
<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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import CounterRenderer from '../../shared/datatable/renderers/counter.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CompletedTasksRenderer',
|
name: 'CompletedTasksRenderer',
|
||||||
props: {
|
props: {
|
||||||
params: {
|
params: {
|
||||||
required: true,
|
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;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
CounterRenderer
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,8 @@ import DataTable from '../shared/datatable/table.vue';
|
||||||
import UsersRenderer from './renderers/users.vue';
|
import UsersRenderer from './renderers/users.vue';
|
||||||
import NameRenderer from './renderers/name.vue';
|
import NameRenderer from './renderers/name.vue';
|
||||||
import StatusRenderer from './renderers/status.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 SuperviserRenderer from './renderers/superviser.vue';
|
||||||
import CommentsRenderer from '../shared/datatable/renderers/comments.vue';
|
import CommentsRenderer from '../shared/datatable/renderers/comments.vue';
|
||||||
import DueDateRenderer from '../shared/datatable/renderers/date.vue';
|
import DueDateRenderer from '../shared/datatable/renderers/date.vue';
|
||||||
|
|
@ -115,7 +117,9 @@ export default {
|
||||||
DescriptionModal,
|
DescriptionModal,
|
||||||
StatusRenderer,
|
StatusRenderer,
|
||||||
SuperviserRenderer,
|
SuperviserRenderer,
|
||||||
FavoriteRenderer
|
FavoriteRenderer,
|
||||||
|
CompletedTasksRenderer,
|
||||||
|
CompletedExperimentsRenderer
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
dataSource: { type: String, required: true },
|
dataSource: { type: String, required: true },
|
||||||
|
|
@ -226,6 +230,20 @@ export default {
|
||||||
cellRenderer: SuperviserRenderer,
|
cellRenderer: SuperviserRenderer,
|
||||||
notSelectable: true
|
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',
|
field: 'created_at',
|
||||||
headerName: this.i18n.t('projects.index.card.start_date'),
|
headerName: this.i18n.t('projects.index.card.start_date'),
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
25
app/javascript/vue/projects/renderers/completed_tasks.vue
Normal file
25
app/javascript/vue/projects/renderers/completed_tasks.vue
Normal 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>
|
||||||
47
app/javascript/vue/shared/datatable/renderers/counter.vue
Normal file
47
app/javascript/vue/shared/datatable/renderers/counter.vue
Normal 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>
|
||||||
|
|
@ -6,9 +6,10 @@ module Lists
|
||||||
include Canaid::Helpers::PermissionsHelper
|
include Canaid::Helpers::PermissionsHelper
|
||||||
include CommentHelper
|
include CommentHelper
|
||||||
|
|
||||||
attributes :name, :code, :created_at, :archived_on, :users, :urls, :folder, :hidden,
|
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,
|
: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,
|
:comments, :updated_at, :permissions, :due_date_cell, :start_on_cell, :description, :status, :favorite
|
||||||
|
|
||||||
def team
|
def team
|
||||||
object.team.name
|
object.team.name
|
||||||
end
|
end
|
||||||
|
|
@ -17,6 +18,22 @@ module Lists
|
||||||
!project?
|
!project?
|
||||||
end
|
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
|
def favorite
|
||||||
object.favorite if project?
|
object.favorite if project?
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,21 @@ module Lists
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_records
|
def fetch_records
|
||||||
|
done_status_id = MyModuleStatusFlow.first.final_status.id
|
||||||
@records = @raw_data.joins(:project)
|
@records = @raw_data.joins(:project)
|
||||||
.includes(my_modules: { my_module_status: :my_module_status_implications })
|
.includes(my_modules: { my_module_status: :my_module_status_implications })
|
||||||
.includes(workflowimg_attachment: :blob, user_assignments: %i(user_role user))
|
.includes(workflowimg_attachment: :blob, user_assignments: %i(user_role user))
|
||||||
.joins('LEFT OUTER JOIN my_modules AS active_tasks ON
|
.joins('LEFT OUTER JOIN my_modules AS active_tasks ON
|
||||||
active_tasks.experiment_id = experiments.id
|
active_tasks.experiment_id = experiments.id
|
||||||
AND active_tasks.archived = FALSE')
|
AND active_tasks.archived = FALSE')
|
||||||
.joins('LEFT OUTER JOIN my_modules AS active_completed_tasks ON
|
.joins(
|
||||||
|
ActiveRecord::Base.sanitize_sql_array([
|
||||||
|
'LEFT OUTER JOIN my_modules AS active_completed_tasks ON
|
||||||
active_completed_tasks.experiment_id = experiments.id
|
active_completed_tasks.experiment_id = experiments.id
|
||||||
AND active_completed_tasks.archived = FALSE AND active_completed_tasks.state = 1')
|
AND active_completed_tasks.archived = FALSE AND active_completed_tasks.my_module_status_id = ?',
|
||||||
|
done_status_id
|
||||||
|
])
|
||||||
|
)
|
||||||
.readable_by_user(@user)
|
.readable_by_user(@user)
|
||||||
.with_favorites(@user)
|
.with_favorites(@user)
|
||||||
.select('experiments.*')
|
.select('experiments.*')
|
||||||
|
|
@ -54,9 +60,7 @@ module Lists
|
||||||
|
|
||||||
@records = @records.where('experiments.due_date <= ?', @filters[:due_date_to]) if @filters[:due_date_to].present?
|
@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]) if @filters[:updated_on_from].present?
|
||||||
@records = @records.where('experiments.updated_at > ?', @filters[:updated_on_from])
|
|
||||||
end
|
|
||||||
if @filters[:updated_on_to].present?
|
if @filters[:updated_on_to].present?
|
||||||
@records = @records.where('experiments.updated_at < ?',
|
@records = @records.where('experiments.updated_at < ?',
|
||||||
@filters[:updated_on_to])
|
@filters[:updated_on_to])
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,35 @@ module Lists
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_projects
|
def fetch_projects
|
||||||
|
done_status_id = MyModuleStatusFlow.first.final_status.id
|
||||||
@team.projects
|
@team.projects
|
||||||
.includes(:team, :project_comments, user_assignments: %i(user user_role))
|
.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)
|
.with_favorites(@user)
|
||||||
.visible_to(@user, @team)
|
.visible_to(@user, @team)
|
||||||
.left_outer_joins(:project_comments)
|
.left_outer_joins(:project_comments)
|
||||||
.select('projects.*')
|
.select('projects.*')
|
||||||
.select('COUNT(DISTINCT comments.id) AS comment_count')
|
.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')
|
.group('projects.id, favorites.id')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -64,27 +86,20 @@ module Lists
|
||||||
search_query = @params[:search].presence || @filters[:query]
|
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?
|
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 }) if @filters[:members].present?
|
||||||
records = records.joins(:user_assignments).where(user_assignments: { user_id: @filters[:members].values })
|
|
||||||
end
|
|
||||||
|
|
||||||
records = records.where(supervised_by_id: @filters[:head_of_project].values) if @filters[:head_of_project].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]) }) if @filters[:archived_on_to].present?
|
||||||
records = records.where('projects.archived_on < ?',
|
records = records.where('projects.archived_on > ?', @filters[:archived_on_from]) if @filters[:archived_on_from].present?
|
||||||
@filters[:archived_on_to])
|
|
||||||
end
|
|
||||||
if @filters[:archived_on_from].present?
|
|
||||||
records = records.where('projects.archived_on > ?', @filters[:archived_on_from])
|
|
||||||
end
|
|
||||||
|
|
||||||
if @filters[:statuses].present?
|
if @filters[:statuses].present?
|
||||||
scopes = {
|
scopes = {
|
||||||
|
|
|
||||||
|
|
@ -674,6 +674,8 @@ en:
|
||||||
description: 'Description'
|
description: 'Description'
|
||||||
supervised_by: 'Head of project'
|
supervised_by: 'Head of project'
|
||||||
status: 'Status'
|
status: 'Status'
|
||||||
|
completed_experiment: "Experiments done"
|
||||||
|
completed_experiments: "%{completed}/%{all} experiments"
|
||||||
end_of_list_placeholder: 'You’ve reached the end of the list'
|
end_of_list_placeholder: 'You’ve reached the end of the list'
|
||||||
folder:
|
folder:
|
||||||
description: "%{projects_count} projects | %{folders_count} folders"
|
description: "%{projects_count} projects | %{folders_count} folders"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ RSpec.describe Lists::ExperimentsService do
|
||||||
let(:params) {{ page: 1, per_page: 10, search: '', project: project } }
|
let(:params) {{ page: 1, per_page: 10, search: '', project: project } }
|
||||||
let(:service) { described_class.new(raw_data, params, user:) }
|
let(:service) { described_class.new(raw_data, params, user:) }
|
||||||
|
|
||||||
|
before(:all) do
|
||||||
|
MyModuleStatusFlow.ensure_default
|
||||||
|
end
|
||||||
|
|
||||||
describe '#fetch_records' do
|
describe '#fetch_records' do
|
||||||
context 'when view_mode is archived' do
|
context 'when view_mode is archived' do
|
||||||
before do
|
before do
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ RSpec.describe Lists::ProjectsService do
|
||||||
let(:params) {{ page: 1, per_page: 10, search: '', team: team } }
|
let(:params) {{ page: 1, per_page: 10, search: '', team: team } }
|
||||||
let(:service) { described_class.new(team, user, folder, params) }
|
let(:service) { described_class.new(team, user, folder, params) }
|
||||||
|
|
||||||
|
before(:all) do
|
||||||
|
MyModuleStatusFlow.ensure_default
|
||||||
|
end
|
||||||
|
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
context 'when view_mode is archived' do
|
context 'when view_mode is archived' do
|
||||||
before do
|
before do
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue