diff --git a/app/assets/stylesheets/_billables.scss b/app/assets/stylesheets/_billables.scss index e1face21..b9f98262 100644 --- a/app/assets/stylesheets/_billables.scss +++ b/app/assets/stylesheets/_billables.scss @@ -26,10 +26,8 @@ max-width: 90em; - .submit-billable-entries { - @include media($laptop-screen) { - opacity: 0; - } + #submit-billable-entries-test { + opacity: .0; } } } diff --git a/app/controllers/billables_controller.rb b/app/controllers/billables_controller.rb index 784521c3..8e20e66b 100644 --- a/app/controllers/billables_controller.rb +++ b/app/controllers/billables_controller.rb @@ -1,12 +1,6 @@ class BillablesController < ApplicationController def index - @hours_entries = Hour.query(entry_filter_or_default, - [:user, :project, :category]) - @mileages_entries = Mileage.query(entry_filter_or_default, - [:user, :project]) - - @billable_list = BillableList.new(@hours_entries, @mileages_entries) - @filters = EntryFilter.new(entry_filter_or_default) + @projects = projects_with_billable_entries end def bill_entries @@ -22,7 +16,11 @@ def bill_entries private - def entry_filter_or_default - params[:entry_filter] || { billed: false } + def projects_with_billable_entries + billable_projects = Project.where(billable: true, archived: false) + + billable_projects.select do |project| + project if project.has_billable_entries? + end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6364ef52..9875204b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -42,7 +42,7 @@ def current_locale def billable_entry_checkbox(entry, entry_type) if entry.billed - "√" + "✓" else tag(:input, type: "checkbox", @@ -77,4 +77,9 @@ def easter? holiday = Holidays.on(Date.today, :nl) holiday[0][:name] == "Pasen" if holiday.any? end + + def billable_hours_of(project) + Hour.includes(:category, :user, :project). + where(project: project, billed: false) + end end diff --git a/app/models/billable_list.rb b/app/models/billable_list.rb deleted file mode 100644 index 00d00ec6..00000000 --- a/app/models/billable_list.rb +++ /dev/null @@ -1,16 +0,0 @@ -class BillableList - attr_reader :clients, :hours_entries, :mileages_entries - - def initialize(hours_entries, mileages_entries) - @hours_entries = hours_entries - @mileages_entries = mileages_entries - - @clients = Client.eager_load( - projects: [hours: [:user, :category], - mileages: [:user]]).where( - "hours.id in (?) OR mileages.id in (?)", - hours_entries.map(&:id), - mileages_entries.map(&:id) - ).by_last_updated - end -end diff --git a/app/models/project.rb b/app/models/project.rb index ee1b6d9d..f217ed3c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -50,6 +50,11 @@ def budget_status budget - hours.sum(:value) if budget end + def has_billable_entries? + hours.exists?(billed: false) || + mileages.exists?(billed: false) + end + private def slug_source diff --git a/app/views/billables/_entries.html.haml b/app/views/billables/_entries.html.haml deleted file mode 100644 index fee77d7d..00000000 --- a/app/views/billables/_entries.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -%table.entries - %tr - %th - %input{ type: "checkbox", "data-project-id" => project.id, class: "bill-project" } - = t("billables.table.billed") - %th= t("billables.table.date") - %th= t("billables.table.category") - %th= t("billables.table.hours") - %th= t("billables.table.user") - %th= t("billables.table.unbill") - - entries.each do |entry| - %tr.info-row - %td=billable_entry_checkbox(entry) - %td=l(entry.date) - %td=entry.category.name - %td.center= entry.hours - %td - = link_to entry.user do - = entry.user.full_name - %td - - if entry.billed? - %button{name: "entries_to_unbill[]", class: "unbill_button", value: entry.id} - = t("billables.table.unbill") diff --git a/app/views/billables/_filters.html.haml b/app/views/billables/_filters.html.haml deleted file mode 100644 index c67ba9ca..00000000 --- a/app/views/billables/_filters.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -%h3= t("entry_filters.title") -= simple_form_for(@filters, url: billables_path, method: "get") do |f| - = f.input :client_id, collection: @filters.clients, label: false, include_blank: t("entry_filters.clients") - = f.input :project_id, collection: @filters.projects, label: false, include_blank: t("entry_filters.projects") - = f.input :from_date, input_html: { class: "datepicker", value: @filters.from_date ? @filters.from_date.strftime(I18n.t("date.formats.default")) : "" } - = f.input :to_date, input_html: { class: "datepicker", value: @filters.to_date ? @filters.to_date.strftime(I18n.t("date.formats.default")) : "" } - = f.input :billed, collection: @filters.billed_options, label: false, include_blank: t("entry_filters.billed_empty") - = f.button :submit, t("billables.buttons.filter"), class: "button-grey" diff --git a/app/views/billables/_hours_entries.html.haml b/app/views/billables/_hours_entries.html.haml index ab6d2e87..d9f6eac1 100644 --- a/app/views/billables/_hours_entries.html.haml +++ b/app/views/billables/_hours_entries.html.haml @@ -8,14 +8,12 @@ %th= t("billables.table.category") %th= t("billables.table.hours") %th= t("billables.table.user") - - entries.each do |entry| + - billable_hours_of(project).each do |hour| %tr.info-row - %td=billable_entry_checkbox(entry, "hours") - %td=I18n.l (entry.date) - %td=entry.category.name - %td.center= entry.value + %td= billable_entry_checkbox(hour, "hours") + %td= I18n.l (hour.date) + %td= hour.category.name + %td.center= hour.value %td - = link_to entry.user do - = entry.user.full_name - - + = link_to hour.user do + = hour.user.full_name diff --git a/app/views/billables/_mileages_entries.html.haml b/app/views/billables/_mileages_entries.html.haml index 61a3005a..a1e9a49c 100644 --- a/app/views/billables/_mileages_entries.html.haml +++ b/app/views/billables/_mileages_entries.html.haml @@ -8,12 +8,12 @@ %th %th= t("entries.index.mileages") %th= t("billables.table.user") - - entries.each do |entry| + - project.mileages.where(billed: false).each do |mileage| %tr.info-row - %td=billable_entry_checkbox(entry, "mileages") - %td=I18n.l (entry.date) + %td= billable_entry_checkbox(mileage, "mileages") + %td= I18n.l (mileage.date) %td - %td.center= entry.value + %td.center= mileage.value %td - = link_to entry.user do - = entry.user.full_name + = link_to mileage.user do + = mileage.user.full_name diff --git a/app/views/billables/_projects.html.haml b/app/views/billables/_projects.html.haml deleted file mode 100644 index ce0aed21..00000000 --- a/app/views/billables/_projects.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- projects.each do |project| - - hours = @billable_list.hours_entries.where(project: project) - - mileages = @billable_list.mileages_entries.where(project: project) - .container - %h1= project.name - - unless hours.empty? - = render "hours_entries", entries: hours, project: project - - unless mileages.empty? - = render "mileages_entries", entries: mileages, project: project - %button.submit-billable-entries= t("billables.buttons.bill_selected") diff --git a/app/views/billables/index.html.haml b/app/views/billables/index.html.haml index 243b2173..0d2362aa 100644 --- a/app/views/billables/index.html.haml +++ b/app/views/billables/index.html.haml @@ -4,17 +4,14 @@ %button{id: "submit-billable-entries", disabled: true} =t("billables.buttons.bill_selected") .billables - .sidebar - .container - = render partial: "filters", locals: { filter_url: billables_path } .outer %h1= t("billables.billable_entries") - - unless @hours_entries.any? || @mileages_entries.any? - .info - %p= t("info.no_billable_entries_html") = form_tag("/billables", method: "post", remote: true, id: "billable-entries-form", autocomplete: "off") do - - @billable_list.clients.each do |client| - %h1= client.name - = render "projects", projects: client.projects - - + - @projects.each do |project| + .container + %h1= project.name + - if project.hours.exists?(billed: false) + = render "hours_entries", project: project + - if project.mileages.exists?(billed: false) + = render "mileages_entries", project: project + %button#submit-billable-entries-test= t("billables.buttons.bill_selected") diff --git a/spec/features/user_manages_billables_spec.rb b/spec/features/user_manages_billables_spec.rb deleted file mode 100644 index 30b82dad..00000000 --- a/spec/features/user_manages_billables_spec.rb +++ /dev/null @@ -1,185 +0,0 @@ -feature "User manages billables" do - let(:subdomain) { generate(:subdomain) } - - before(:each) do - user = build(:user) - create(:account_with_schema, subdomain: subdomain, owner: user) - sign_in_user(user, subdomain: subdomain) - end - - scenario "bill an hours entry" do - client = create(:client) - project = create(:project, client: client, billable: true) - entry = create(:hour, project: project, billed: false) - - visit billables_url(subdomain: subdomain) - - find(:css, ".bill_checkbox").set(true) - - find(:css, ".submit-billable-entries").click - - visit billables_url(subdomain: subdomain) - - expect(entry.reload.billed).to eq(true) - expect(page.body).to_not have_selector(".bill_checkbox") - end - - scenario "bill a mileages entry" do - client = create(:client) - project = create(:project, client: client, billable: true) - user = create(:user) - entry = create( - :mileage, project: project, user: user, value: 2, billed: false) - - visit billables_url(subdomain: subdomain) - find(:css, ".bill_checkbox").set(true) - - find(:css, ".submit-billable-entries").click - - visit billables_url(subdomain: subdomain) - - expect(entry.reload.billed).to eq(true) - expect(page.body).to_not have_selector(".bill_checkbox") - end - - context "filters" do - scenario "hours by client" do - client1 = create(:client) - client2 = create(:client) - project1 = create(:project, client: client1, billable: true) - project2 = create(:project, client: client2, billable: true) - create(:hour, project: project1, billed: false) - create(:hour, project: project2, billed: false) - - visit billables_url(subdomain: subdomain) - - select(client1.name, from: "entry_filter_client_id") - find(:css, "input[value='#{I18n.t('billables.buttons.filter')}']").click - - entries_table = find(".outer").text - expect(entries_table).to have_content(client1.name) - expect(entries_table).to_not have_content(client2.name) - end - - scenario "mileages by client" do - client1 = create(:client) - client2 = create(:client) - project1 = create(:project, client: client1, billable: true) - project2 = create(:project, client: client2, billable: true) - create(:mileage, project: project1, billed: false) - create(:mileage, project: project2, billed: false) - - visit billables_url(subdomain: subdomain) - - select(client1.name, from: "entry_filter_client_id") - find(:css, "input[value='#{I18n.t('billables.buttons.filter')}']").click - - entries_table = find(".outer").text - expect(entries_table).to have_content(client1.name) - expect(entries_table).to_not have_content(client2.name) - end - - scenario "mileages by date" do - client = create(:client) - project = create(:project, client: client, billable: true) - date1 = Date.yesterday - date2 = Date.current - - create(:mileage, project: project, billed: false, date: date1) - create(:mileage, project: project, billed: false, date: date2) - - visit billables_url(subdomain: subdomain) - - fill_in "entry_filter_from_date", with: date2 - fill_in "entry_filter_to_date", with: date2 - find(:css, "input[value='#{I18n.t('billables.buttons.filter')}']").click - - entries_table = find(".outer").text - expect(entries_table).to have_content(I18n.l date2) - expect(entries_table).to_not have_content(I18n.l date1) - end - - scenario "hours by date" do - client = create(:client) - project = create(:project, client: client, billable: true) - date1 = Date.yesterday - date2 = Date.current - - create(:hour, project: project, billed: false, date: date1) - create(:hour, project: project, billed: false, date: date2) - - visit billables_url(subdomain: subdomain) - - fill_in "entry_filter_from_date", with: date2 - fill_in "entry_filter_to_date", with: date2 - find(:css, "input[value='#{I18n.t('billables.buttons.filter')}']").click - - entries_table = find(".outer").text - expect(entries_table).to have_content(I18n.l date2) - expect(entries_table).to_not have_content(I18n.l date1) - end - - scenario "mileages by billed" do - client = create(:client) - project = create(:project, client: client, billable: true) - - not_billed_mileage = create( - :mileage, project: project, billed: false, value: 100) - billed_mileage = create( - :mileage, project: project, billed: true, value: 200) - - visit billables_url(subdomain: subdomain) - - select(I18n.t("entry_filters.not_billed"), from: "entry_filter_billed") - find(:css, "input[value='#{I18n.t('billables.buttons.filter')}']").click - - entries_table = find(".outer").text - expect(entries_table).to have_content(not_billed_mileage.value) - expect(entries_table).to_not have_content(billed_mileage.value) - end - - scenario "hours by billed" do - client = create(:client) - project = create(:project, client: client, billable: true) - - not_billed_hour = create( - :hour, project: project, billed: false, value: 100) - billed_hour = create( - :hour, project: project, billed: true, value: 200) - - visit billables_url(subdomain: subdomain) - - select(I18n.t("entry_filters.not_billed"), from: "entry_filter_billed") - find(:css, "input[value='#{I18n.t('billables.buttons.filter')}']").click - - entries_table = find(".outer").text - expect(entries_table).to have_content(not_billed_hour.value) - expect(entries_table).to_not have_content(billed_hour.value) - end - - scenario "hours and mileages by billed" do - client = create(:client) - project = create(:project, client: client, billable: true) - - not_billed_hour = create( - :hour, project: project, billed: false, value: 1111) - not_billed_mileage = create( - :mileage, project: project, billed: false, value: 2222) - billed_hour = create( - :hour, project: project, billed: true, value: 3333) - billed_mileage = create( - :mileage, project: project, billed: true, value: 4444) - - visit billables_url(subdomain: subdomain) - - select(I18n.t("entry_filters.billed"), from: "entry_filter_billed") - find(:css, "input[value='#{I18n.t('billables.buttons.filter')}']").click - - entries_table = find(".outer").text - expect(entries_table).to have_content(billed_hour.value) - expect(entries_table).to have_content(billed_mileage.value) - expect(entries_table).to_not have_content(not_billed_hour.value) - expect(entries_table).to_not have_content(not_billed_mileage.value) - end - end -end diff --git a/spec/features/user_visits_billable_entries_overview_spec.rb b/spec/features/user_visits_billable_entries_overview_spec.rb new file mode 100644 index 00000000..d7b8839b --- /dev/null +++ b/spec/features/user_visits_billable_entries_overview_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +feature "User manages billables" do + let(:subdomain) { generate(:subdomain) } + + before(:each) do + user = build(:user) + create(:account_with_schema, subdomain: subdomain, owner: user) + sign_in_user(user, subdomain: subdomain) + end + + scenario "an overview of all projects with billable hours is shown" do + client = create(:client) + project = create(:project, client: client, billable: true) + project2 = create(:project, client: client, billable: false) + create(:hour, project: project, billed: false) + + visit billables_url(subdomain: subdomain) + + expect(page).to have_content "Billable Entries" + expect(page).to have_content project.name + expect(page).to_not have_content project2.name + end + + scenario "billable projects that have no billable hours are not shown" do + client = create(:client) + project = create(:project, client: client, billable: true) + project2 = create(:project, client: client, billable: true) + create(:hour, project: project, billed: false) + + visit billables_url(subdomain: subdomain) + + expect(page).to_not have_content project2.name + end + + scenario "bill an hours entry" do + client = create(:client) + project = create(:project, client: client, billable: true) + entry = create(:hour, project: project, billed: false) + + visit billables_url(subdomain: subdomain) + + expect(page).to have_content entry.category.name + expect(page.body).to have_selector(".bill_checkbox") + + find(:css, ".bill_checkbox").set(true) + find(:css, "#submit-billable-entries-test").click + + expect(entry.reload.billed).to eq(true) + visit billables_url(subdomain: subdomain) + expect(page.body).to_not have_selector(".bill_checkbox") + end + + scenario "bill a mileages entry" do + client = create(:client) + project = create(:project, client: client, billable: true) + user = create(:user) + entry = create( + :mileage, project: project, user: user, value: 2, billed: false + ) + + visit billables_url(subdomain: subdomain) + + find(:css, ".bill_checkbox").set(true) + find(:css, "#submit-billable-entries-test").click + + visit billables_url(subdomain: subdomain) + + expect(entry.reload.billed).to eq(true) + expect(page.body).to_not have_selector(".bill_checkbox") + end +end diff --git a/spec/models/billable_list_spec.rb b/spec/models/billable_list_spec.rb deleted file mode 100644 index 933ee36d..00000000 --- a/spec/models/billable_list_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -describe BillableList do - let(:client1) { create(:client) } - let(:client2) { create(:client) } - let(:client3) { create(:client) } - - let(:project1) { create(:project, client: client1) } - let(:project2) { create(:project, client: client2, billable: true) } - let(:project3) { create(:project, client: client3, billable: true) } - - let!(:entry1) { create(:hour, project: project1) } - let!(:entry2) { create(:hour, project: project2) } - let!(:entry3) { create(:mileage, project: project3) } - - let(:billable_list) { BillableList.new(Hour.billable, Mileage.billable) } - - it "has a list of clients" do - expect(billable_list.clients.count).to eq(2) - end -end