Fix: add option to choose 24 hour working slot (#4018)
* Add option to choose 24 hour working slot * fix spec * add check to update open hour and close hour for open all day * update 24 hour working slot change in widget side * add validation to check open_all_day and closed_all_day true at the same time
This commit is contained in:
parent
7ba24b90c4
commit
e348db1e37
10 changed files with 151 additions and 19 deletions
|
@ -463,7 +463,8 @@
|
||||||
"HOURS": "hours",
|
"HOURS": "hours",
|
||||||
"VALIDATION_ERROR": "Starting time should be before closing time.",
|
"VALIDATION_ERROR": "Starting time should be before closing time.",
|
||||||
"CHOOSE": "Choose"
|
"CHOOSE": "Choose"
|
||||||
}
|
},
|
||||||
|
"ALL_DAY":"All-Day"
|
||||||
},
|
},
|
||||||
"IMAP": {
|
"IMAP": {
|
||||||
"TITLE": "IMAP",
|
"TITLE": "IMAP",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<input
|
<input
|
||||||
v-model="isDayEnabled"
|
v-model="isDayEnabled"
|
||||||
name="enable-day"
|
name="enable-day"
|
||||||
class="enable-day"
|
class="enable-checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:title="$t('INBOX_MGMT.BUSINESS_HOURS.DAY.ENABLE')"
|
:title="$t('INBOX_MGMT.BUSINESS_HOURS.DAY.ENABLE')"
|
||||||
/>
|
/>
|
||||||
|
@ -14,26 +14,38 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isDayEnabled" class="hours-select-wrap">
|
<div v-if="isDayEnabled" class="hours-select-wrap">
|
||||||
<div class="hours-range">
|
<div class="hours-range">
|
||||||
|
<div class="checkbox-wrap open-all-day">
|
||||||
|
<input
|
||||||
|
v-model="isOpenAllDay"
|
||||||
|
name="enable-open-all-day"
|
||||||
|
class="enable-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
:title="$t('INBOX_MGMT.BUSINESS_HOURS.ALL_DAY')"
|
||||||
|
/>
|
||||||
|
<span>{{ $t('INBOX_MGMT.BUSINESS_HOURS.ALL_DAY') }}</span>
|
||||||
|
</div>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-model="fromTime"
|
v-model="fromTime"
|
||||||
:options="timeSlots"
|
:options="fromTimeSlots"
|
||||||
deselect-label=""
|
deselect-label=""
|
||||||
select-label=""
|
select-label=""
|
||||||
selected-label=""
|
selected-label=""
|
||||||
:placeholder="$t('INBOX_MGMT.BUSINESS_HOURS.DAY.CHOOSE')"
|
:placeholder="$t('INBOX_MGMT.BUSINESS_HOURS.DAY.CHOOSE')"
|
||||||
:allow-empty="false"
|
:allow-empty="false"
|
||||||
|
:disabled="isOpenAllDay"
|
||||||
/>
|
/>
|
||||||
<div class="separator-icon">
|
<div class="separator-icon">
|
||||||
<fluent-icon icon="subtract" type="solid" size="16" />
|
<fluent-icon icon="subtract" type="solid" size="16" />
|
||||||
</div>
|
</div>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-model="toTime"
|
v-model="toTime"
|
||||||
:options="timeSlots"
|
:options="toTimeSlots"
|
||||||
deselect-label=""
|
deselect-label=""
|
||||||
select-label=""
|
select-label=""
|
||||||
selected-label=""
|
selected-label=""
|
||||||
:placeholder="$t('INBOX_MGMT.BUSINESS_HOURS.DAY.CHOOSE')"
|
:placeholder="$t('INBOX_MGMT.BUSINESS_HOURS.DAY.CHOOSE')"
|
||||||
:allow-empty="false"
|
:allow-empty="false"
|
||||||
|
:disabled="isOpenAllDay"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hasError" class="date-error">
|
<div v-if="hasError" class="date-error">
|
||||||
|
@ -79,9 +91,14 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
timeSlots() {
|
fromTimeSlots() {
|
||||||
return timeSlots;
|
return timeSlots;
|
||||||
},
|
},
|
||||||
|
toTimeSlots() {
|
||||||
|
return timeSlots.filter(slot => {
|
||||||
|
return slot !== '12:00 AM';
|
||||||
|
});
|
||||||
|
},
|
||||||
isDayEnabled: {
|
isDayEnabled: {
|
||||||
get() {
|
get() {
|
||||||
return this.timeSlot.from && this.timeSlot.to;
|
return this.timeSlot.from && this.timeSlot.to;
|
||||||
|
@ -93,12 +110,14 @@ export default {
|
||||||
from: timeSlots[0],
|
from: timeSlots[0],
|
||||||
to: timeSlots[16],
|
to: timeSlots[16],
|
||||||
valid: true,
|
valid: true,
|
||||||
|
openAllDay: false,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
...this.timeSlot,
|
...this.timeSlot,
|
||||||
from: '',
|
from: '',
|
||||||
to: '',
|
to: '',
|
||||||
valid: false,
|
valid: false,
|
||||||
|
openAllDay: false,
|
||||||
};
|
};
|
||||||
this.$emit('update', newSlot);
|
this.$emit('update', newSlot);
|
||||||
},
|
},
|
||||||
|
@ -146,15 +165,39 @@ export default {
|
||||||
return parse(this.toTime, 'hh:mm a', new Date());
|
return parse(this.toTime, 'hh:mm a', new Date());
|
||||||
},
|
},
|
||||||
totalHours() {
|
totalHours() {
|
||||||
const totalHours = differenceInMinutes(this.toDate, this.fromDate) / 60;
|
if (this.timeSlot.openAllDay) {
|
||||||
if (this.toTime === '12:00 AM') {
|
return 24;
|
||||||
return 24 + totalHours;
|
|
||||||
}
|
}
|
||||||
|
const totalHours = differenceInMinutes(this.toDate, this.fromDate) / 60;
|
||||||
return totalHours;
|
return totalHours;
|
||||||
},
|
},
|
||||||
hasError() {
|
hasError() {
|
||||||
return !this.timeSlot.valid;
|
return !this.timeSlot.valid;
|
||||||
},
|
},
|
||||||
|
isOpenAllDay: {
|
||||||
|
get() {
|
||||||
|
return this.timeSlot.openAllDay;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (value) {
|
||||||
|
this.$emit('update', {
|
||||||
|
...this.timeSlot,
|
||||||
|
from: '12:00 AM',
|
||||||
|
to: '11:59 PM',
|
||||||
|
valid: true,
|
||||||
|
openAllDay: value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$emit('update', {
|
||||||
|
...this.timeSlot,
|
||||||
|
from: '09:00 AM',
|
||||||
|
to: '05:00 PM',
|
||||||
|
valid: true,
|
||||||
|
openAllDay: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -182,7 +225,7 @@ export default {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
border-bottom: 1px solid var(--color-border-light);
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
}
|
}
|
||||||
.enable-day {
|
.enable-checkbox {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,4 +283,17 @@ export default {
|
||||||
font-size: var(--font-size-mini);
|
font-size: var(--font-size-mini);
|
||||||
color: var(--r-300);
|
color: var(--r-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.open-all-day {
|
||||||
|
margin-right: var(--space-medium);
|
||||||
|
span {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
margin-left: var(--space-smaller);
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -86,6 +86,7 @@ export const timeSlotParse = timeSlots => {
|
||||||
close_hour: closeHour,
|
close_hour: closeHour,
|
||||||
close_minutes: closeMinutes,
|
close_minutes: closeMinutes,
|
||||||
closed_all_day: closedAllDay,
|
closed_all_day: closedAllDay,
|
||||||
|
open_all_day: openAllDay,
|
||||||
} = slot;
|
} = slot;
|
||||||
const from = closedAllDay ? '' : getTime(openHour, openMinutes);
|
const from = closedAllDay ? '' : getTime(openHour, openMinutes);
|
||||||
const to = closedAllDay ? '' : getTime(closeHour, closeMinutes);
|
const to = closedAllDay ? '' : getTime(closeHour, closeMinutes);
|
||||||
|
@ -95,13 +96,15 @@ export const timeSlotParse = timeSlots => {
|
||||||
to,
|
to,
|
||||||
from,
|
from,
|
||||||
valid: !closedAllDay,
|
valid: !closedAllDay,
|
||||||
|
openAllDay,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const timeSlotTransform = timeSlots => {
|
export const timeSlotTransform = timeSlots => {
|
||||||
return timeSlots.map(slot => {
|
return timeSlots.map(slot => {
|
||||||
const closed = !(slot.to && slot.from);
|
const closed = slot.openAllDay ? false : !(slot.to && slot.from);
|
||||||
|
const openAllDay = slot.openAllDay;
|
||||||
let fromDate = '';
|
let fromDate = '';
|
||||||
let toDate = '';
|
let toDate = '';
|
||||||
let openHour = '';
|
let openHour = '';
|
||||||
|
@ -125,6 +128,7 @@ export const timeSlotTransform = timeSlots => {
|
||||||
open_minutes: openMinutes,
|
open_minutes: openMinutes,
|
||||||
close_hour: closeHour,
|
close_hour: closeHour,
|
||||||
close_minutes: closeMinutes,
|
close_minutes: closeMinutes,
|
||||||
|
open_all_day: openAllDay,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -40,6 +40,7 @@ describe('#timeSlotParse', () => {
|
||||||
close_hour: 4,
|
close_hour: 4,
|
||||||
close_minutes: 30,
|
close_minutes: 30,
|
||||||
closed_all_day: false,
|
closed_all_day: false,
|
||||||
|
open_all_day: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(timeSlotParse([slot])).toStrictEqual([
|
expect(timeSlotParse([slot])).toStrictEqual([
|
||||||
|
@ -48,6 +49,7 @@ describe('#timeSlotParse', () => {
|
||||||
from: '01:30 AM',
|
from: '01:30 AM',
|
||||||
to: '04:30 AM',
|
to: '04:30 AM',
|
||||||
valid: true,
|
valid: true,
|
||||||
|
openAllDay: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -60,6 +62,7 @@ describe('#timeSlotTransform', () => {
|
||||||
from: '01:30 AM',
|
from: '01:30 AM',
|
||||||
to: '04:30 AM',
|
to: '04:30 AM',
|
||||||
valid: true,
|
valid: true,
|
||||||
|
openAllDay: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(timeSlotTransform([slot])).toStrictEqual([
|
expect(timeSlotTransform([slot])).toStrictEqual([
|
||||||
|
@ -70,6 +73,7 @@ describe('#timeSlotTransform', () => {
|
||||||
close_hour: 4,
|
close_hour: 4,
|
||||||
close_minutes: 30,
|
close_minutes: 30,
|
||||||
closed_all_day: false,
|
closed_all_day: false,
|
||||||
|
open_all_day: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,9 +31,12 @@ export default {
|
||||||
closeHour,
|
closeHour,
|
||||||
closeMinute,
|
closeMinute,
|
||||||
closedAllDay,
|
closedAllDay,
|
||||||
|
openAllDay,
|
||||||
} = this.currentDayAvailability;
|
} = this.currentDayAvailability;
|
||||||
const { utcOffset } = this.channelConfig;
|
const { utcOffset } = this.channelConfig;
|
||||||
|
|
||||||
|
if (openAllDay) return true;
|
||||||
|
|
||||||
if (closedAllDay) return false;
|
if (closedAllDay) return false;
|
||||||
|
|
||||||
const startTime = buildDateFromTime(openHour, openMinute, utcOffset);
|
const startTime = buildDateFromTime(openHour, openMinute, utcOffset);
|
||||||
|
@ -56,6 +59,7 @@ export default {
|
||||||
openMinute: workingHourConfig.open_minutes,
|
openMinute: workingHourConfig.open_minutes,
|
||||||
closeHour: workingHourConfig.close_hour,
|
closeHour: workingHourConfig.close_hour,
|
||||||
closeMinute: workingHourConfig.close_minutes,
|
closeMinute: workingHourConfig.close_minutes,
|
||||||
|
openAllDay: workingHourConfig.open_all_day,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
isInBusinessHours() {
|
isInBusinessHours() {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module OutOfOffisable
|
module OutOfOffisable
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
OFFISABLE_ATTRS = %w[day_of_week closed_all_day open_hour open_minutes close_hour close_minutes].freeze
|
OFFISABLE_ATTRS = %w[day_of_week closed_all_day open_hour open_minutes close_hour close_minutes open_all_day].freeze
|
||||||
|
|
||||||
included do
|
included do
|
||||||
has_many :working_hours, dependent: :destroy_async
|
has_many :working_hours, dependent: :destroy_async
|
||||||
|
@ -29,7 +29,8 @@ module OutOfOffisable
|
||||||
# "open_hour"=>9,
|
# "open_hour"=>9,
|
||||||
# "open_minutes"=>0,
|
# "open_minutes"=>0,
|
||||||
# "close_hour"=>17,
|
# "close_hour"=>17,
|
||||||
# "close_minutes"=>0},...]
|
# "close_minutes"=>0,
|
||||||
|
# "open_all_day=>false" },...]
|
||||||
def update_working_hours(params)
|
def update_working_hours(params)
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
params.each do |working_hour|
|
params.each do |working_hour|
|
||||||
|
@ -41,12 +42,12 @@ module OutOfOffisable
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_default_working_hours
|
def create_default_working_hours
|
||||||
working_hours.create!(day_of_week: 0, closed_all_day: true)
|
working_hours.create!(day_of_week: 0, closed_all_day: true, open_all_day: false)
|
||||||
working_hours.create!(day_of_week: 1, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
working_hours.create!(day_of_week: 1, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
||||||
working_hours.create!(day_of_week: 2, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
working_hours.create!(day_of_week: 2, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
||||||
working_hours.create!(day_of_week: 3, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
working_hours.create!(day_of_week: 3, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
||||||
working_hours.create!(day_of_week: 4, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
working_hours.create!(day_of_week: 4, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
||||||
working_hours.create!(day_of_week: 5, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
working_hours.create!(day_of_week: 5, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
||||||
working_hours.create!(day_of_week: 6, closed_all_day: true)
|
working_hours.create!(day_of_week: 6, closed_all_day: true, open_all_day: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
# close_minutes :integer
|
# close_minutes :integer
|
||||||
# closed_all_day :boolean default(FALSE)
|
# closed_all_day :boolean default(FALSE)
|
||||||
# day_of_week :integer not null
|
# day_of_week :integer not null
|
||||||
|
# open_all_day :boolean default(FALSE)
|
||||||
# open_hour :integer
|
# open_hour :integer
|
||||||
# open_minutes :integer
|
# open_minutes :integer
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
class WorkingHour < ApplicationRecord
|
class WorkingHour < ApplicationRecord
|
||||||
belongs_to :inbox
|
belongs_to :inbox
|
||||||
|
|
||||||
|
before_validation :ensure_open_all_day_hours
|
||||||
before_save :assign_account
|
before_save :assign_account
|
||||||
|
|
||||||
validates :open_hour, presence: true, unless: :closed_all_day?
|
validates :open_hour, presence: true, unless: :closed_all_day?
|
||||||
|
@ -35,6 +37,7 @@ class WorkingHour < ApplicationRecord
|
||||||
validates :close_minutes, inclusion: 0..59, unless: :closed_all_day?
|
validates :close_minutes, inclusion: 0..59, unless: :closed_all_day?
|
||||||
|
|
||||||
validate :close_after_open, unless: :closed_all_day?
|
validate :close_after_open, unless: :closed_all_day?
|
||||||
|
validate :open_all_day_and_closed_all_day
|
||||||
|
|
||||||
def self.today
|
def self.today
|
||||||
find_by(day_of_week: Date.current.wday)
|
find_by(day_of_week: Date.current.wday)
|
||||||
|
@ -69,4 +72,19 @@ class WorkingHour < ApplicationRecord
|
||||||
|
|
||||||
errors.add(:close_hour, 'Closing time cannot be before opening time')
|
errors.add(:close_hour, 'Closing time cannot be before opening time')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_open_all_day_hours
|
||||||
|
return unless open_all_day?
|
||||||
|
|
||||||
|
self.open_hour = 0
|
||||||
|
self.open_minutes = 0
|
||||||
|
self.close_hour = 23
|
||||||
|
self.close_minutes = 59
|
||||||
|
end
|
||||||
|
|
||||||
|
def open_all_day_and_closed_all_day
|
||||||
|
return unless open_all_day? && closed_all_day?
|
||||||
|
|
||||||
|
errors.add(:base, 'open_all_day and closed_all_day cannot be true at the same time')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddOpenAllDayToWorkingHour < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :working_hours, :open_all_day, :boolean, default: false
|
||||||
|
end
|
||||||
|
end
|
|
@ -773,6 +773,7 @@ ActiveRecord::Schema.define(version: 2022_02_18_120357) do
|
||||||
t.integer "close_minutes"
|
t.integer "close_minutes"
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.boolean "open_all_day", default: false
|
||||||
t.index ["account_id"], name: "index_working_hours_on_account_id"
|
t.index ["account_id"], name: "index_working_hours_on_account_id"
|
||||||
t.index ["inbox_id"], name: "index_working_hours_on_inbox_id"
|
t.index ["inbox_id"], name: "index_working_hours_on_inbox_id"
|
||||||
end
|
end
|
||||||
|
|
|
@ -50,4 +50,42 @@ RSpec.describe WorkingHour do
|
||||||
expect(described_class.today.closed_now?).to be true
|
expect(described_class.today.closed_now?).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when open_all_day is true' do
|
||||||
|
let(:inbox) { create(:inbox) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Time.zone = 'UTC'
|
||||||
|
inbox.working_hours.find_by(day_of_week: 5).update(open_all_day: true)
|
||||||
|
travel_to '18.02.2022 11:00'.to_datetime
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates open hour and close hour' do
|
||||||
|
expect(described_class.today.open_all_day?).to be true
|
||||||
|
expect(described_class.today.open_hour).to be 0
|
||||||
|
expect(described_class.today.open_minutes).to be 0
|
||||||
|
expect(described_class.today.close_hour).to be 23
|
||||||
|
expect(described_class.today.close_minutes).to be 59
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when open_all_day and closed_all_day true at the same time' do
|
||||||
|
let(:inbox) { create(:inbox) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Time.zone = 'UTC'
|
||||||
|
inbox.working_hours.find_by(day_of_week: 5).update(open_all_day: true)
|
||||||
|
travel_to '18.02.2022 11:00'.to_datetime
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'throws validation error' do
|
||||||
|
working_hour = inbox.working_hours.find_by(day_of_week: 5)
|
||||||
|
working_hour.closed_all_day = true
|
||||||
|
expect(working_hour.invalid?).to be true
|
||||||
|
expect do
|
||||||
|
working_hour.save!
|
||||||
|
end.to raise_error(ActiveRecord::RecordInvalid,
|
||||||
|
'Validation failed: open_all_day and closed_all_day cannot be true at the same time')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue