diff options
| -rw-r--r-- | app/assets/stylesheets/components/_referential_overview.sass | 171 | ||||
| -rw-r--r-- | app/helpers/referentials_helper.rb | 5 | ||||
| -rw-r--r-- | app/services/referential_overview.rb | 218 | ||||
| -rw-r--r-- | app/views/referentials/_overview.html.slim | 31 | ||||
| -rw-r--r-- | app/views/referentials/show.html.slim | 2 | ||||
| -rw-r--r-- | config/locales/referentials.en.yml | 5 | ||||
| -rw-r--r-- | config/locales/referentials.fr.yml | 5 | ||||
| -rw-r--r-- | spec/services/referential_overview_spec.rb | 154 | 
8 files changed, 591 insertions, 0 deletions
| diff --git a/app/assets/stylesheets/components/_referential_overview.sass b/app/assets/stylesheets/components/_referential_overview.sass new file mode 100644 index 000000000..0af5a99f7 --- /dev/null +++ b/app/assets/stylesheets/components/_referential_overview.sass @@ -0,0 +1,171 @@ +.referential-overview +  display: flex +  margin-top: 50px +  $left-size: 100px +  .head +    height: $left-size +    border-top: 1px solid $lightgrey +  .line +    height: 80px +  .left +    flex: 0 0 +    background: $lightgrey +    min-width: $left-size +    overflow: hidden +    border-right: 1px solid white +    .head +      position: relative +      border-bottom: 1px solid white +      border-right: 1px solid $lightgrey +      .dates, .lines +        position: absolute +        font-size: 0.8em +        z-index: 2 +      .dates +        right: 20px +        top: 20px +      .lines +        left: 20px +        bottom: 20px +      &:after +        position: absolute +        border-left: ($left-size - 2px)/2 solid transparent +        border-bottom: ($left-size - 2px)/2 solid transparent +        border-right: ($left-size - 2px)/2 solid white +        border-top: ($left-size - 2px)/2 solid white +        z-index: 1 +        top: 0 +        right: 0 +        width: 0 +        content: "" +    .line +      padding: 15px 10px 10px +      border-bottom: 1px solid white +      font-size: 0.8em +      &:last-child +        border-bottom: 1px solid $grey +      .number +        border-radius: 100px +        display: inline-block +        width: 20px +        height: 20px +        text-align: center +        padding-top: 1px +        text-decoration: none +        color: black +        border: 1px solid $grey +      .name +        display: inline-block +        width: $left-size - 50px() +        white-space: nowrap +        line-height: 20px +        margin-left: 5px +        text-overflow: ellipsis +        overflow: hidden +        vertical-align: bottom +        color: black +        text-decoration: none + +      .company, .mode +        font-size: 0.9em +        margin-top: 5px +        white-space: nowrap +        text-overflow: ellipsis +        overflow: hidden + +      .mode +        margin-top: 0 +        text-transform: uppercase +        color: $grey +        font-weight: bold +  .right +    flex: 1 1 +    overflow-x: scroll +    overflow-y: hidden +    .head +      white-space: nowrap +      .week +        display: inline-block +        position: relative +        height: 100% +        .week-span +          left: 5px +          top: 10px +          right: 30px +          white-space: nowrap +          overflow: hidden +          text-overflow: ellipsis +          position: absolute + +        .week-number +          background-color: lightgrey +          color: white +          position: absolute +          top: 0 +          right: 0 +          padding: 2px 4px + +        &:after +          position: absolute +          right: 0 +          top: 0 +          bottom: 0 +          background: $grey +          width: 1px +          content: "" + +        .days +          position: relative +          top: 50% +          height: 50% +          border-top: 1px solid $grey +          border-bottom: 1px solid $grey + +        .day +          float: left +          border-left: 1px solid $grey +          box-sizing: border-box +          padding-left: 5px +          padding-top: 3px +          position: relative +          height: 100% +          .name, .number +            position: absolute +            left: 10px +            right: 10px +            top: 50% +            transform: translateY(-50%) +            margin-top: 10px +          .name +            font-weight: bold +            font-size: 0.8em +            margin-top: -10px +          &:first-child +            border: none +          &.weekend +            background: $lightgrey + +    .line +      border-bottom: 1px solid $grey +      position: relative +      overflow: hidden + +    .period +      height: 100% +      top: 0 +      background: rgb(219, 249, 196) +      position: absolute +      &:before +        content: "" +        border-right: 1px dashed $grey +        top: -100% +        bottom: -100% +        position: absolute +        right: -1px +        z-index: 10 +      &.empty +        background: rgb(244, 67, 67) +        background: repeating-linear-gradient(-45deg, #f5e1cf,#f5e1cf 10px,#e49393 10px,#e49393 20px) +        &.accepted +          background: #f19039 +          background: repeating-linear-gradient(-45deg, #f5e1cf,#f5e1cf 10px,#f19039 10px,#f19039 20px) diff --git a/app/helpers/referentials_helper.rb b/app/helpers/referentials_helper.rb index 01e5a5879..8251377aa 100644 --- a/app/helpers/referentials_helper.rb +++ b/app/helpers/referentials_helper.rb @@ -10,4 +10,9 @@ module ReferentialsHelper          t('true')      end    end + +  def referential_overview referential +    service = ReferentialOverview.new referential, self +    render partial: "referentials/overview", locals: {referential: referential, overview: service} +  end  end diff --git a/app/services/referential_overview.rb b/app/services/referential_overview.rb new file mode 100644 index 000000000..5b0e144db --- /dev/null +++ b/app/services/referential_overview.rb @@ -0,0 +1,218 @@ +class ReferentialOverview +  attr_reader :h + +  def initialize referential, h +    @referential = referential +    @page = 1 +    @h = h +  end + +  def lines +    @referential.metadatas_lines.includes(:company).page(@page).map{|l| Line.new(l, @referential, period.first, h)} +  end + +  def period +    @period ||= @referential.metadatas_period || [] +  end + +  def weeks +    @weeks = {} +    period.map do |d| +      @weeks[Week.key(d)] ||= Week.new(d, period.last, h) +    end +    @weeks.values +  end + +  class Line +    attr_reader :h +    attr_reader :referential_line + +    delegate :name, :number, :company, :color, :transport_mode, to: :referential_line + +    def initialize line, referential, start, h +      @referential_line = line +      @referential = referential +      @start = start +      @h = h +    end + +    def period +      @period ||= @referential.metadatas_period || [] +    end + +    def referential_periods +      @referential_periods ||= @referential.metadatas.include_lines([@referential_line.id]).map(&:periodes).flatten.sort{|p1, p2| p1.first <=> p2.first} +    end + +    def periods +      @periods ||= begin +        periods = referential_periods.flatten.map{|p| Period.new p, @start, h} +        periods = fill_periods periods +        periods = merge_periods periods +        periods +      end +    end + +    def fill_periods periods +      [].tap do |out| +        previous = OpenStruct.new(end: period.first - 1.day) +        (periods + [OpenStruct.new(start: period.last + 1.day)]).each do |p| +          if p.start > previous.end + 1.day +            out << Period.new((previous.end+1.day..p.start-1.day), @start, h).tap{|p| p.empty = true} +          end +          out << p if p.respond_to?(:end) +          previous = p +        end +      end +    end + +    def merge_periods periods +      [].tap do |out| +        current = periods.first +        periods[1..-1].each do |p| +          if p.start <= current.end +            current.end = p.end +          else +            out << current +            current = p +          end +        end +        out << current +      end +    end + +    def width +      period.count * Day::WIDTH +    end + +    def html_style +      { +        width: "#{width}px" +      }.map{|k, v| "#{k}: #{v}"}.join("; ") +    end + +    def html_class +      out = [] +      out +    end + +    class Period +      attr_accessor :empty +      attr_accessor :h + +      def initialize period, start, h +        @period = period +        @start = start +        @empty = false +        @h = h +      end + +      def start +        @period.first +      end + +      def end +        @period.last +      end + +      def end= val +        @period = (start..val) +      end + +      def width +        @period.count * Day::WIDTH +      end + +      def left +        (@period.first - @start).to_i * Day::WIDTH +      end + +      def html_style +        { +          width: "#{width}px", +          left: "#{left}px", +        }.map{|k, v| "#{k}: #{v}"}.join("; ") +      end + +      def empty? +        @empty +      end + +      def accepted? +        @period.count < 7 +      end + +      def title +        h.l(self.start, format: :short) + " - " + h.l(self.end, format: :short) +      end + +      def html_class +        out = [] +        out << "empty" if empty? +        out << "accepted" if accepted? +        out +      end +    end +  end + +  class Week +    attr_reader :h +    attr_reader :start_date +    attr_reader :end_date + +    def initialize start_date, boundary, h +      @start_date = start_date.to_date +      @end_date = [start_date.end_of_week, boundary].min.to_date +      @h = h +    end + +    def self.key date +      date.beginning_of_week.to_s +    end + +    def span +      h.l(@start_date, format: "#{@start_date.day}-#{@end_date.day} %b") +    end + +    def number +      h.l(@start_date, format: "%W") +    end + +    def period +      (@start_date..@end_date) +    end + +    def days +      period.map {|d| Day.new d, h } +    end +  end + +  class Day +    attr_reader :h + +    WIDTH=50 + +    def initialize date, h +      @date = date +      @h = h +    end + +    def html_style +      {width: "#{WIDTH}px"}.map{|k, v| "#{k}: #{v}"}.join("; ") +    end + +    def html_class +      out = [] +      out << "weekend" if [0, 6].include?(@date.wday) +      out +    end + +    def short_name +      h.l(@date, format: "%a") +    end + +    def number +      @date.day +    end +  end +end diff --git a/app/views/referentials/_overview.html.slim b/app/views/referentials/_overview.html.slim new file mode 100644 index 000000000..03c72752e --- /dev/null +++ b/app/views/referentials/_overview.html.slim @@ -0,0 +1,31 @@ +.referential-overview +  .left +    .head +      .dates= I18n.t("referentials.overview.head.dates") +      .lines= I18n.t("referentials.overview.head.lines") +    .lines +      - overview.lines.each do |line| +        .line +          a.number style="background-color: #{line.color.present? ? "##{line.color}" : 'whitesmoke'}" title=line.name +            = line.number +          - unless line.number == line.name +            a.name title=line.name +              = line.name +          .company= line.company&.name +          .mode= line.transport_mode +  .right +    .head +      - overview.weeks.each do |week| +        .week +          .week-span= week.span +          .week-number= week.number +          .days +            - week.days.each do |day| +              .day style=day.html_style class=day.html_class +                .name= day.short_name +                .number= day.number +    .lines +      - overview.lines.each do |line| +        .line style=line.html_style class=line.html_class +          - line.periods.each do |period| +            .period style=period.html_style class=period.html_class title=period.title diff --git a/app/views/referentials/show.html.slim b/app/views/referentials/show.html.slim index 6c88f5b81..289e802d7 100644 --- a/app/views/referentials/show.html.slim +++ b/app/views/referentials/show.html.slim @@ -67,6 +67,8 @@            = replacement_msg t('referential_lines.search_no_results') +    = referential_overview resource +  / Modal(s)  = modalbox 'purgeModal' do    = simple_form_for [@referential, CleanUp.new] do |f| diff --git a/config/locales/referentials.en.yml b/config/locales/referentials.en.yml index eb8eae98d..dd7f776fd 100644 --- a/config/locales/referentials.en.yml +++ b/config/locales/referentials.en.yml @@ -48,6 +48,11 @@ en:        overlapped_referential: "%{referential} cover the same perimeter"        overlapped_period: "Another period is on the same period"        short_period: Min period length is two days +    overview: +      head: +        dates: Dates +        lines: Lignes +            activerecord:      models:        referential: diff --git a/config/locales/referentials.fr.yml b/config/locales/referentials.fr.yml index 37af8a4eb..48b98214e 100644 --- a/config/locales/referentials.fr.yml +++ b/config/locales/referentials.fr.yml @@ -48,6 +48,11 @@ fr:        overlapped_referential: "%{referential} couvre le même périmètre d'offre"        overlapped_period: "Une autre période chevauche cette période"        short_period: "La durée minimum d'une période est de deux jours" +    overview: +      head: +        dates: Dates +        lines: Lignes +    activerecord:      models:        referential: diff --git a/spec/services/referential_overview_spec.rb b/spec/services/referential_overview_spec.rb new file mode 100644 index 000000000..39a90360a --- /dev/null +++ b/spec/services/referential_overview_spec.rb @@ -0,0 +1,154 @@ +RSpec.describe ReferentialOverview do + +  subject{ described_class } + +end + +RSpec.describe ReferentialOverview::Week do + +  describe "#initialize" do +    it "should respect the boundary" do +      week = ReferentialOverview::Week.new(Time.now.beginning_of_week, 1.week.from_now, {}) +      expect(week.start_date).to eq Time.now.beginning_of_week.to_date +      expect(week.end_date).to eq Time.now.end_of_week.to_date + +      week = ReferentialOverview::Week.new(Time.now.beginning_of_week, Time.now, {}) +      expect(week.start_date).to eq Time.now.beginning_of_week.to_date +      expect(week.end_date).to eq Time.now.to_date +    end +  end +end + +RSpec.describe ReferentialOverview::Line do + +  let(:ref_line){create(:line)} +  let(:referential){create(:referential)} +  let(:start){2.days.ago} +  let(:period_1){(10.days.ago..8.days.ago)} +  let(:period_2){(5.days.ago..1.days.ago)} + +  subject(:line){ReferentialOverview::Line.new ref_line, referential, start, {}} + +  before(:each) do +    create(:referential_metadata, referential: referential, line_ids: [ref_line.id], periodes: [period_1, period_2].compact) +  end + +  describe "#width" do +    it "should have the right value" do +      expect(line.width).to eq ReferentialOverview::Day::WIDTH * referential.metadatas_period.count +    end +  end + +  describe "#periods" do +    context "when the periodes are split into several metadatas" do +      let(:period_2){nil} +      before do +        create(:referential_metadata, referential: referential, line_ids: [ref_line.id], periodes: [(17.days.ago..11.days.ago)]) +      end +      it "should find them all" do +        expect(line.periods.count).to eq 2 +        expect(line.periods[0].empty?).to be_falsy +        expect(line.periods[1].empty?).to be_falsy +      end + +      context "when the periodes overlap" do +        let(:period_2){nil} +        before do +          create(:referential_metadata, referential: referential, line_ids: [ref_line.id], periodes: [(17.days.ago..9.days.ago)]) +        end +        it "should merge them" do +          expect(line.periods.count).to eq 1 +          expect(line.periods[0].empty?).to be_falsy +          expect(line.periods[0].start).to eq 17.days.ago.to_date +          expect(line.periods[0].end).to eq 8.days.ago.to_date +        end +      end +    end +  end + +  describe "#fill_periods" do +    it "should fill the voids" do +      expect(line.periods.count).to eq 3 +      expect(line.periods[0].empty?).to be_falsy +      expect(line.periods[1].empty?).to be_truthy +      expect(line.periods[1].accepted?).to be_truthy +      expect(line.periods[2].empty?).to be_falsy +    end + +    context "with no void" do +      let(:period_1){(10.days.ago..8.days.ago)} +      let(:period_2){(7.days.ago..1.days.ago)} + +      it "should find no void" do +        expect(line.periods.count).to eq 2 +        expect(line.periods[0].empty?).to be_falsy +        expect(line.periods[1].empty?).to be_falsy +      end +    end + +    context "with a large void" do +      let(:period_1){(20.days.ago..19.days.ago)} +      let(:period_2){(2.days.ago..1.days.ago)} + +      it "should fill the void" do +        expect(line.periods.count).to eq 3 +        expect(line.periods[0].empty?).to be_falsy +        expect(line.periods[1].empty?).to be_truthy +        expect(line.periods[1].accepted?).to be_falsy +        expect(line.periods[2].empty?).to be_falsy +      end +    end + +    context "with a void at the end" do +      before do +        create(:referential_metadata, referential: referential,  periodes: [(2.days.ago..1.days.ago)]) +      end +      let(:period_1){(20.days.ago..19.days.ago)} +      let(:period_2){nil} + +      it "should fill the void" do +        expect(line.periods.count).to eq 2 +        expect(line.periods[0].empty?).to be_falsy +        expect(line.periods[1].empty?).to be_truthy +        expect(line.periods[1].accepted?).to be_falsy +        expect(line.periods[1].end).to eq 1.days.ago.to_date +      end +    end + +    context "with a void at the beginning" do +      before do +        create(:referential_metadata, referential: referential,  periodes: [(200.days.ago..199.days.ago)]) +      end +      let(:period_1){(20.days.ago..19.days.ago)} +      let(:period_2){nil} + +      it "should fill the void" do +        expect(line.periods.count).to eq 2 +        expect(line.periods[0].start).to eq 200.days.ago.to_date +        expect(line.periods[0].empty?).to be_truthy +        expect(line.periods[0].accepted?).to be_falsy +        expect(line.periods[1].empty?).to be_falsy +      end +    end +  end +end + +RSpec.describe ReferentialOverview::Line::Period do + +  let(:period){(1.day.ago.to_date..Time.now.to_date)} +  let(:start){2.days.ago} + +  subject(:line_period){ReferentialOverview::Line::Period.new period, start, nil} + +  describe "#width" do +    it "should have the right value" do +      expect(line_period.width).to eq ReferentialOverview::Day::WIDTH * 2 +    end +  end + +  describe "#left" do +    it "should have the right value" do +      expect(line_period.width).to eq ReferentialOverview::Day::WIDTH * 1 +    end +  end +end | 
