rails_attend 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (214) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +22 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/attend/admin/absence_stats_controller.rb +60 -0
  6. data/app/controllers/attend/admin/absences_controller.rb +77 -0
  7. data/app/controllers/attend/admin/attendance_logs_controller.rb +103 -0
  8. data/app/controllers/attend/admin/attendance_settings_controller.rb +86 -0
  9. data/app/controllers/attend/admin/attendance_stats_controller.rb +58 -0
  10. data/app/controllers/attend/admin/attendances_controller.rb +57 -0
  11. data/app/controllers/attend/admin/base_controller.rb +4 -0
  12. data/app/controllers/attend/admin/extra_days_controller.rb +63 -0
  13. data/app/controllers/attend/admin/financial_months_controller.rb +69 -0
  14. data/app/controllers/attend/admin/overtimes_controller.rb +59 -0
  15. data/app/controllers/attend/me/absences_controller.rb +71 -0
  16. data/app/controllers/attend/me/attendance_logs_controller.rb +74 -0
  17. data/app/controllers/attend/me/attendance_settings_controller.rb +52 -0
  18. data/app/controllers/attend/me/attendance_stats_controller.rb +27 -0
  19. data/app/controllers/attend/me/attendances_controller.rb +43 -0
  20. data/app/controllers/attend/me/base_controller.rb +4 -0
  21. data/app/controllers/attend/me/calendars_controller.rb +33 -0
  22. data/app/controllers/attend/me/overtimes_controller.rb +65 -0
  23. data/app/javascript/packs/controllers/attend/admin/attendance_logs/index.js +1 -0
  24. data/app/javascript/packs/controllers/attend/admin/attendance_logs/my.js +14 -0
  25. data/app/javascript/packs/controllers/attend/admin/attendance_settings/create.js +1 -0
  26. data/app/javascript/packs/controllers/attend/admin/attendance_settings/index.js +1 -0
  27. data/app/javascript/packs/controllers/attend/admin/attendance_settings/my.js +13 -0
  28. data/app/javascript/packs/controllers/attend/admin/attendance_settings/new.js +11 -0
  29. data/app/javascript/packs/controllers/attend/me/absences/new.js +16 -0
  30. data/app/javascript/packs/controllers/attend/me/absences/update.js +3 -0
  31. data/app/javascript/packs/controllers/attend/me/attendance_settings/create.js +1 -0
  32. data/app/javascript/packs/controllers/attend/me/attendance_settings/edit.js +1 -0
  33. data/app/javascript/packs/controllers/attend/me/attendance_settings/new.js +2 -0
  34. data/app/javascript/packs/controllers/attend/me/calendars/show.js +48 -0
  35. data/app/jobs/attendance_job.rb +14 -0
  36. data/app/models/absence/attend/annual_leave.rb +5 -0
  37. data/app/models/absence/attend/bereavement_leave.rb +5 -0
  38. data/app/models/absence/attend/business_trip.rb +5 -0
  39. data/app/models/absence/attend/children_day_leave.rb +5 -0
  40. data/app/models/absence/attend/compensatory_leave.rb +5 -0
  41. data/app/models/absence/attend/marriage_leave.rb +4 -0
  42. data/app/models/absence/attend/maternity_leave.rb +5 -0
  43. data/app/models/absence/attend/parental_leave.rb +5 -0
  44. data/app/models/absence/attend/personal_leave.rb +5 -0
  45. data/app/models/absence/attend/prenatal_leave.rb +4 -0
  46. data/app/models/absence/attend/sick_leave.rb +5 -0
  47. data/app/models/absence/attend/vacation_leave.rb +5 -0
  48. data/app/models/absence/attend/welfare_leave.rb +5 -0
  49. data/app/models/attend/absence.rb +5 -0
  50. data/app/models/attend/absence_stat.rb +5 -0
  51. data/app/models/attend/application_record.rb +5 -0
  52. data/app/models/attend/attendance.rb +5 -0
  53. data/app/models/attend/attendance_log.rb +5 -0
  54. data/app/models/attend/attendance_setting.rb +5 -0
  55. data/app/models/attend/attendance_stat.rb +5 -0
  56. data/app/models/attend/ext/member.rb +33 -0
  57. data/app/models/attend/extra_day.rb +5 -0
  58. data/app/models/attend/financial_month.rb +5 -0
  59. data/app/models/attend/model/absence/business_trip.rb +10 -0
  60. data/app/models/attend/model/absence/compensatory_leave.rb +5 -0
  61. data/app/models/attend/model/absence/personal_leave.rb +5 -0
  62. data/app/models/attend/model/absence/welfare_leave/annual_leave.rb +16 -0
  63. data/app/models/attend/model/absence/welfare_leave/vacation_leave.rb +16 -0
  64. data/app/models/attend/model/absence/welfare_leave.rb +5 -0
  65. data/app/models/attend/model/absence.rb +208 -0
  66. data/app/models/attend/model/absence_stat.rb +17 -0
  67. data/app/models/attend/model/attendance.rb +314 -0
  68. data/app/models/attend/model/attendance_log.rb +165 -0
  69. data/app/models/attend/model/attendance_setting.rb +61 -0
  70. data/app/models/attend/model/attendance_stat.rb +104 -0
  71. data/app/models/attend/model/extra_day.rb +21 -0
  72. data/app/models/attend/model/financial_month.rb +114 -0
  73. data/app/models/attend/model/overtime.rb +93 -0
  74. data/app/models/attend/overtime.rb +5 -0
  75. data/app/models/attend.rb +11 -0
  76. data/app/models/concerns/absent.rb +103 -0
  77. data/app/views/admin/_attend_nav.html.erb +15 -0
  78. data/app/views/attend/admin/absence_stats/_filter_form.html.erb +9 -0
  79. data/app/views/attend/admin/absence_stats/_form.html.erb +12 -0
  80. data/app/views/attend/admin/absence_stats/_index_tbody.html.erb +3 -0
  81. data/app/views/attend/admin/absence_stats/_index_thead.html.erb +4 -0
  82. data/app/views/attend/admin/absence_stats/index.html.erb +5 -0
  83. data/app/views/attend/admin/absences/_absence.html.erb +38 -0
  84. data/app/views/attend/admin/absences/_edit_form.html.erb +3 -0
  85. data/app/views/attend/admin/absences/_filter_form.html.erb +8 -0
  86. data/app/views/attend/admin/absences/_form.html.erb +5 -0
  87. data/app/views/attend/admin/absences/_index_tbody.html.erb +18 -0
  88. data/app/views/attend/admin/absences/_index_thead.html.erb +10 -0
  89. data/app/views/attend/admin/absences/_new_form.html.erb +3 -0
  90. data/app/views/attend/admin/absences/_show.html.erb +11 -0
  91. data/app/views/attend/admin/absences/index.html.erb +5 -0
  92. data/app/views/attend/admin/absences/my.html.erb +5 -0
  93. data/app/views/attend/admin/attendance_logs/_edit_form.html.erb +5 -0
  94. data/app/views/attend/admin/attendance_logs/_filter_form.html.erb +11 -0
  95. data/app/views/attend/admin/attendance_logs/_form.html.erb +6 -0
  96. data/app/views/attend/admin/attendance_logs/_index_thead.html.erb +19 -0
  97. data/app/views/attend/admin/attendance_logs/_list.html.erb +61 -0
  98. data/app/views/attend/admin/attendance_logs/_new_form.html.erb +3 -0
  99. data/app/views/attend/admin/attendance_logs/index.html.erb +5 -0
  100. data/app/views/attend/admin/attendance_logs/my.html.erb +5 -0
  101. data/app/views/attend/admin/attendance_settings/_button.html.erb +4 -0
  102. data/app/views/attend/admin/attendance_settings/_filter_form.html.erb +11 -0
  103. data/app/views/attend/admin/attendance_settings/_form.html.erb +7 -0
  104. data/app/views/attend/admin/attendance_settings/_index_tbody.html.erb +15 -0
  105. data/app/views/attend/admin/attendance_settings/_index_thead.html.erb +10 -0
  106. data/app/views/attend/admin/attendance_settings/_new_form.html.erb +8 -0
  107. data/app/views/attend/admin/attendance_settings/_show_table.html.erb +12 -0
  108. data/app/views/attend/admin/attendance_settings/index.html.erb +5 -0
  109. data/app/views/attend/admin/attendance_settings/my.html.erb +5 -0
  110. data/app/views/attend/admin/attendance_stats/_filter_form.html.erb +10 -0
  111. data/app/views/attend/admin/attendance_stats/_form.html.erb +6 -0
  112. data/app/views/attend/admin/attendance_stats/_index_thead.html.erb +10 -0
  113. data/app/views/attend/admin/attendance_stats/index.html.erb +44 -0
  114. data/app/views/attend/admin/attendances/_button.html.erb +2 -0
  115. data/app/views/attend/admin/attendances/_filter_form.html.erb +10 -0
  116. data/app/views/attend/admin/attendances/_form.html.erb +23 -0
  117. data/app/views/attend/admin/attendances/_index_tbody.html.erb +61 -0
  118. data/app/views/attend/admin/attendances/_index_thead.html.erb +22 -0
  119. data/app/views/attend/admin/attendances/_new_form.html.erb +4 -0
  120. data/app/views/attend/admin/attendances/index.html.erb +7 -0
  121. data/app/views/attend/admin/base/_financial_month_menu.html.erb +10 -0
  122. data/app/views/attend/admin/base/_nav.html.erb +3 -0
  123. data/app/views/attend/admin/base/_nav_attendances.html.erb +7 -0
  124. data/app/views/attend/admin/extra_days/_breadcrumb.html.erb +0 -0
  125. data/app/views/attend/admin/extra_days/_edit_form.html.erb +4 -0
  126. data/app/views/attend/admin/extra_days/_filter_form.html.erb +5 -0
  127. data/app/views/attend/admin/extra_days/_form.html.erb +4 -0
  128. data/app/views/attend/admin/extra_days/_index_tbody.html.erb +3 -0
  129. data/app/views/attend/admin/extra_days/_index_thead.html.erb +4 -0
  130. data/app/views/attend/admin/extra_days/_new_form.html.erb +4 -0
  131. data/app/views/attend/admin/extra_days/index.html.erb +7 -0
  132. data/app/views/attend/admin/financial_months/_breadcrumb.html.erb +0 -0
  133. data/app/views/attend/admin/financial_months/_edit_form.html.erb +4 -0
  134. data/app/views/attend/admin/financial_months/_filter_form.html.erb +9 -0
  135. data/app/views/attend/admin/financial_months/_form.html.erb +4 -0
  136. data/app/views/attend/admin/financial_months/_index_tbody.html.erb +9 -0
  137. data/app/views/attend/admin/financial_months/_index_thead.html.erb +6 -0
  138. data/app/views/attend/admin/financial_months/_new_form.html.erb +4 -0
  139. data/app/views/attend/admin/financial_months/events.json.jbuilder +9 -0
  140. data/app/views/attend/admin/financial_months/index.html.erb +7 -0
  141. data/app/views/attend/admin/financial_months/index.js +60 -0
  142. data/app/views/attend/admin/financial_months/show.html.erb +10 -0
  143. data/app/views/attend/admin/overtimes/_button.html.erb +0 -0
  144. data/app/views/attend/admin/overtimes/_edit_form.html.erb +4 -0
  145. data/app/views/attend/admin/overtimes/_filter_form.html.erb +8 -0
  146. data/app/views/attend/admin/overtimes/_form.html.erb +3 -0
  147. data/app/views/attend/admin/overtimes/_index_tbody.html.erb +22 -0
  148. data/app/views/attend/admin/overtimes/_index_thead.html.erb +9 -0
  149. data/app/views/attend/admin/overtimes/_new_form.html.erb +4 -0
  150. data/app/views/attend/admin/overtimes/index.html.erb +5 -0
  151. data/app/views/attend/admin/overtimes/my.html.erb +8 -0
  152. data/app/views/attend/me/absences/_absence.html.erb +34 -0
  153. data/app/views/attend/me/absences/_edit_form.html.erb +3 -0
  154. data/app/views/attend/me/absences/_filter_form.html.erb +11 -0
  155. data/app/views/attend/me/absences/_form.html.erb +33 -0
  156. data/app/views/attend/me/absences/_index_tbody.html.erb +24 -0
  157. data/app/views/attend/me/absences/_index_thead.html.erb +8 -0
  158. data/app/views/attend/me/absences/_new_form.html.erb +3 -0
  159. data/app/views/attend/me/absences/_redeeming_form.html.erb +5 -0
  160. data/app/views/attend/me/absences/_show.html.erb +11 -0
  161. data/app/views/attend/me/absences/dashboard.html.erb +44 -0
  162. data/app/views/attend/me/absences/index.html.erb +7 -0
  163. data/app/views/attend/me/absences/redeeming.js.erb +6 -0
  164. data/app/views/attend/me/absences/show.html.erb +1 -0
  165. data/app/views/attend/me/attendance_logs/_edit_form.html.erb +4 -0
  166. data/app/views/attend/me/attendance_logs/_filter_form.html.erb +11 -0
  167. data/app/views/attend/me/attendance_logs/_form.html.erb +11 -0
  168. data/app/views/attend/me/attendance_logs/_index_thead.html.erb +9 -0
  169. data/app/views/attend/me/attendance_logs/_new_form.html.erb +4 -0
  170. data/app/views/attend/me/attendance_logs/index.html.erb +42 -0
  171. data/app/views/attend/me/attendance_settings/_edit_form.html.erb +6 -0
  172. data/app/views/attend/me/attendance_settings/_index_tbody.html.erb +15 -0
  173. data/app/views/attend/me/attendance_settings/_index_thead.html.erb +12 -0
  174. data/app/views/attend/me/attendance_settings/_new_form.html.erb +8 -0
  175. data/app/views/attend/me/attendance_settings/index.html.erb +5 -0
  176. data/app/views/attend/me/attendance_stats/_form.html.erb +6 -0
  177. data/app/views/attend/me/attendance_stats/_search_form.html.erb +5 -0
  178. data/app/views/attend/me/attendance_stats/edit.html.erb +9 -0
  179. data/app/views/attend/me/attendance_stats/index.html.erb +49 -0
  180. data/app/views/attend/me/attendance_stats/new.html.erb +9 -0
  181. data/app/views/attend/me/attendance_stats/show.html.erb +28 -0
  182. data/app/views/attend/me/attendances/_button.html.erb +3 -0
  183. data/app/views/attend/me/attendances/_filter_form.html.erb +9 -0
  184. data/app/views/attend/me/attendances/_form.html.erb +22 -0
  185. data/app/views/attend/me/attendances/_index_tbody.html.erb +49 -0
  186. data/app/views/attend/me/attendances/_index_thead.html.erb +19 -0
  187. data/app/views/attend/me/attendances/index.html.erb +7 -0
  188. data/app/views/attend/me/base/_menu.html.erb +8 -0
  189. data/app/views/attend/me/base/_nav_absences.html.erb +9 -0
  190. data/app/views/attend/me/base/_nav_attendances.html.erb +10 -0
  191. data/app/views/attend/me/calendars/events.json.jbuilder +19 -0
  192. data/app/views/attend/me/calendars/show.html.erb +12 -0
  193. data/app/views/attend/me/overtimes/_edit_form.html.erb +4 -0
  194. data/app/views/attend/me/overtimes/_filter_form.html.erb +9 -0
  195. data/app/views/attend/me/overtimes/_form.html.erb +16 -0
  196. data/app/views/attend/me/overtimes/_index_tbody.html.erb +23 -0
  197. data/app/views/attend/me/overtimes/_index_thead.html.erb +6 -0
  198. data/app/views/attend/me/overtimes/_new_form.html.erb +4 -0
  199. data/app/views/attend/me/overtimes/index.html.erb +7 -0
  200. data/app/views/me/_attend_nav.html.erb +12 -0
  201. data/app/views/me/_nav_attendance_settings.html.erb +8 -0
  202. data/config/locales/en.attributes.yml +34 -0
  203. data/config/locales/en.controller.yml +18 -0
  204. data/config/locales/en.enum.yml +43 -0
  205. data/config/locales/en.notify.yml +38 -0
  206. data/config/locales/zh.attributes.yml +34 -0
  207. data/config/locales/zh.controller.yml +40 -0
  208. data/config/locales/zh.enum.yml +43 -0
  209. data/config/locales/zh.notify.yml +38 -0
  210. data/config/routes.rb +70 -0
  211. data/lib/rails_attend/config.rb +11 -0
  212. data/lib/rails_attend/engine.rb +25 -0
  213. data/lib/rails_attend.rb +2 -0
  214. metadata +269 -0
@@ -0,0 +1,5 @@
1
+ module Attend
2
+ class Attendance < ApplicationRecord
3
+ include Model::Attendance
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Attend
2
+ class AttendanceLog < ApplicationRecord
3
+ include Model::AttendanceLog
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Attend
2
+ class AttendanceSetting < ApplicationRecord
3
+ include Model::AttendanceSetting
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Attend
2
+ class AttendanceStat < ApplicationRecord
3
+ include Model::AttendanceStat
4
+ end
5
+ end
@@ -0,0 +1,33 @@
1
+ module Attend
2
+ module Ext::Member
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_one :absence_stat, -> { where(year: Date.today.year) }
7
+ has_many :absences, class_name: 'Attend::Absence', dependent: :destroy
8
+ has_many :overtimes, class_name: 'Attend::Overtime', dependent: :destroy
9
+ has_many :attendances, class_name: 'Attend::Attendance', dependent: :destroy
10
+ has_many :attendance_logs, class_name: 'Attend::AttendanceLog', dependent: :nullify
11
+ has_many :attendance_stats, class_name: 'Attend::AttendanceStat', dependent: :destroy
12
+ has_many :attendance_settings, class_name: 'Attend::AttendanceSetting', dependent: :destroy
13
+
14
+ composed_of :absent, mapping: ['id', 'member_id']
15
+ end
16
+
17
+ def attendance_setting(date = Date.today)
18
+ _attendance_setting = self.attendance_settings.default_where('financial_month.begin_date-lte': date).first
19
+ _attendance_setting || self.attendance_settings.build(financial_month: FinancialMonth.current_month)
20
+ end
21
+
22
+ def on_time(date = Date.today)
23
+ hour, min = (self.attendance_setting(date).on_time).split(':')
24
+ date.in_time_zone(timezone).change(hour: hour, min: min)
25
+ end
26
+
27
+ def off_time(date = Date.today)
28
+ hour, min = (self.attendance_setting(date).off_time).split(':')
29
+ date.in_time_zone(timezone).change(hour: hour, min: min)
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module Attend
2
+ class ExtraDay < ApplicationRecord
3
+ include Model::ExtraDay
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Attend
2
+ class FinancialMonth < ApplicationRecord
3
+ include Model::FinancialMonth
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ module Attend
2
+ module Model::Absence::BusinessTrip
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ validates :note, presence: true
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ module Attend
2
+ module Model::Absence::CompensatoryLeave
3
+
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Attend
2
+ module Model::Absence::PersonalLeave
3
+
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ module Attend
2
+ module Model::Absence::WelfareLeave::AnnualLeave #年假
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ validate :validate_left_hours, if: -> { start_at_changed? || finish_at_changed? }
7
+ end
8
+
9
+ def validate_left_hours
10
+ if self.hours > self.absent.left_hours.to_f
11
+ self.errors.add :base, 'You do not have enough annual hours!'
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Attend
2
+ module Model::Absence::WelfareLeave::VacationLeave
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ validate :validate_left_hours, if: -> { start_at_changed? || finish_at_changed? }
7
+ end
8
+
9
+ def validate_left_hours
10
+ if self.hours > self.absent.left_hours.to_f
11
+ self.errors.add :base, 'You do not have enough annual hours!'
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ module Attend
2
+ module Model::Absence::WelfareLeave
3
+
4
+ end
5
+ end
@@ -0,0 +1,208 @@
1
+ module Attend
2
+ module Model::Absence
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attribute :type, :string
7
+ attribute :state, :string, default: 'init'
8
+ attribute :hours, :float, default: 0
9
+ attribute :kind, :string
10
+ attribute :start_at, :datetime
11
+ attribute :finish_at, :datetime
12
+ attribute :note, :string, limit: 2048
13
+ attribute :comment, :string, limit: 2048
14
+ attribute :redeeming, :boolean
15
+ attribute :redeeming_days, :string, array: true
16
+ attribute :processed, :boolean, default: false
17
+ attribute :divided, :boolean, default: false
18
+
19
+ belongs_to :member
20
+ belongs_to :merged, class_name: self.name, optional: true
21
+ has_many :divideds, class_name: self.name, foreign_key: 'merged_id'
22
+
23
+ composed_of :absent, mapping: [
24
+ ['member_id', 'member_id'],
25
+ ['type', 'type']
26
+ ]
27
+
28
+ enum state: {
29
+ init: 'init',
30
+ approved: 'approved',
31
+ denied: 'denied'
32
+ }
33
+
34
+ scope :current_range, -> (begin_date, finish_date) {
35
+ default_where('start_at-gte': begin_date, 'finish_at-lte': finish_date, state: ['init', 'approved'])
36
+ }
37
+ scope :current_range_stat, -> (begin_date, finish_date) {
38
+ current_range(begin_date, finish_date).group(:type).sum(:hours)
39
+ }
40
+ scope :current_range_events, -> (begin_date, finish_date, department_ids) {
41
+ current_range(begin_date, finish_date).default_where('member.department_id': department_ids).map do |i|
42
+ {
43
+ title: i.member.name,
44
+ start: i.start_at,
45
+ end: i.finish_at,
46
+ url: i.url_helpers.oa_absence_path(i.id)
47
+ }
48
+ end.as_json
49
+ }
50
+
51
+ validate :validate_same_month, if: -> { start_at_changed? || finish_at_changed? }
52
+ validates :start_at, presence: true
53
+ validates :finish_at, presence: true
54
+
55
+ before_save :compute_hours
56
+ after_create_commit :send_notification
57
+
58
+ delegate :name, to: :member, prefix: true
59
+ acts_as_notify(
60
+ :default,
61
+ only: [:hours, :start_at, :finish_at, :note],
62
+ methods: [:member_name, :type_i18n, :state_i18n]
63
+ )
64
+ acts_as_notify(
65
+ :request,
66
+ only: [:hours, :start_at, :finish_at, :note],
67
+ methods: [:member_name, :type_i18n, :state_i18n],
68
+ cc_emails: [
69
+ -> (o){ o.member.office&.absence_email },
70
+ -> (o){ o.member.email },
71
+ -> (o){ o.cc_emails }
72
+ ]
73
+ )
74
+ end
75
+
76
+ def do_trigger(params = {})
77
+ self.trigger_to(params.slice(:state))
78
+
79
+ if self.approved?
80
+ sync_attendance
81
+ end
82
+
83
+ self.class.transaction do
84
+ self.save!
85
+ to_notification(
86
+ member: member,
87
+ link: url_helpers.oa_absences_url(id: self.id),
88
+ verbose: true
89
+ )
90
+ end
91
+ end
92
+
93
+ def send_notification
94
+ to_notification(
95
+ member: member.parent,
96
+ link: url_helpers.my_admin_absences_url(id: self.id),
97
+ code: :request,
98
+ verbose: true
99
+ )
100
+ end
101
+
102
+ def compute_hours
103
+ _hours = 0
104
+
105
+ if workdays.size > 2
106
+ _hours += (workdays.size - 2) * 8
107
+ end
108
+
109
+ if workdays.size > 1
110
+ _seconds_start = TimeHelper.interval(start_time, off_time)
111
+ _hours += _seconds_start.div(1800) / 2.0
112
+
113
+ _seconds_finish = TimeHelper.interval(on_time, finish_time)
114
+ _hours += (_seconds_finish).div(1800) / 2.0
115
+ else
116
+ _start_at = start_time > on_time ? start_time : on_time
117
+ _finish_at = finish_time < off_time ? finish_time : off_time
118
+ _seconds = TimeHelper.interval(_start_at, _finish_at)
119
+ _hours = _seconds.div(1800) / 2.0
120
+ end
121
+
122
+ self.hours = _hours
123
+ self.processed = true
124
+ end
125
+
126
+ def compute_hours!
127
+ self.compute_hours
128
+ self.save
129
+ end
130
+
131
+ def workdays
132
+ @workdays ||= (start_time.to_date..finish_time.to_date).reject { |date| financial_month.rest_day?(date) }
133
+ end
134
+
135
+ def on_time
136
+ return @on_time if @on_time
137
+
138
+ if workdays.size > 1
139
+ @on_time = self.member.on_time.change year: finish_at.year, month: finish_at.month, day: finish_at.day
140
+ else
141
+ @on_time = self.member.on_time.change year: start_at.year, month: start_at.month, day: start_at.day
142
+ end
143
+ end
144
+
145
+ # todo repeat with attendance
146
+ def start_time
147
+ start_at.in_time_zone(member.timezone)
148
+ end
149
+
150
+ # todo repeat with attendance
151
+ def finish_time
152
+ finish_at.in_time_zone(member.timezone)
153
+ end
154
+
155
+ def off_time
156
+ return @off_time if @off_time
157
+
158
+ if workdays.size > 1
159
+ @off_time = self.member.off_time.change year: start_at.year, month: start_at.month, day: start_at.day
160
+ else
161
+ @off_time = self.member.off_time.change year: finish_at.year, month: finish_at.month, day: finish_at.day
162
+ end
163
+ end
164
+
165
+ def sync_attendance
166
+ self.workdays.each_with_index do |workday, index|
167
+ att = self.member.attendances.find_or_initialize_by('attend_on': workday)
168
+
169
+ if index == 0 && self.start_at <= self.on_time
170
+ att.late_absence_id = self.id
171
+ elsif index == 0 && self.finish_at >= self.off_time
172
+ att.leave_absence_id = self.id
173
+ else
174
+ att.interval_absence_id = self.id
175
+ end
176
+
177
+ att.save
178
+ end
179
+ end
180
+
181
+ def financial_months
182
+ FinancialMonth.default_where('begin_date-lte': self.start_at.to_date, 'end_date-gte': self.finish_at.to_date)
183
+ end
184
+
185
+ def financial_month
186
+ @financial_month ||= financial_months.take
187
+ end
188
+
189
+ def validate_same_month
190
+ count = financial_months.count
191
+ if count > 1
192
+ self.errors.add :base, 'You can not request crossing financial month, please make more than one request'
193
+ end
194
+ if self.finish_at < self.start_at
195
+ self.errors.add :base, 'Start time can not be later than end'
196
+ end
197
+ end
198
+
199
+ class_methods do
200
+ def available_rest_days
201
+ # todo improve performance, use bsearch
202
+ _rest_days = FinancialMonth.available_rest_days.select { |d| d >= Date.today }
203
+ Array(_rest_days).map { |d| [d.to_s, d.to_s] }
204
+ end
205
+ end
206
+
207
+ end
208
+ end
@@ -0,0 +1,17 @@
1
+ module Attend
2
+ module Model::AbsenceStat
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attribute :year, :string
7
+ attribute :annual_days, :float
8
+ attribute :annual_add, :float
9
+ attribute :left_annual_days, :float
10
+ attribute :vacation_days, :float
11
+ attribute :details, :string, limit: 1024
12
+
13
+ belongs_to :member
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,314 @@
1
+ module Attend
2
+ module Model::Attendance
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attribute :late_minutes, :integer
7
+ attribute :leave_minutes, :integer
8
+ attribute :overtime_hours, :float
9
+ attribute :attend_hours, :float
10
+ attribute :interval_hours, :float
11
+ attribute :total_hours, :float
12
+ attribute :attend_on, :date
13
+ attribute :start_at, :datetime
14
+ attribute :finish_at, :datetime
15
+ attribute :interval_start_at, :datetime
16
+ attribute :interval_finish_at, :datetime
17
+ attribute :kind, :string
18
+ attribute :absence_minutes, :integer
19
+ attribute :absence_redeeming, :boolean
20
+ attribute :lost_logs, :string, array: true
21
+ attribute :workday, :boolean, default: true
22
+ attribute :processed, :boolean, default: false
23
+
24
+ belongs_to :member
25
+ belongs_to :interval_absence, class_name: 'Absence', optional: true
26
+ belongs_to :late_absence, class_name: 'Absence', optional: true
27
+ belongs_to :leave_absence, class_name: 'Absence', optional: true
28
+ has_many :attendance_logs, dependent: :nullify
29
+
30
+ enum kind: {
31
+ belated: 'belated',
32
+ asked_leave: 'asked_leave',
33
+ no_meal: 'no_meal',
34
+ allowance: 'allowance'
35
+ }
36
+
37
+ validates :attend_on, uniqueness: { scope: :member_id }
38
+ before_save :sync_workday, if: -> { attend_on_changed? }
39
+ end
40
+
41
+ def compute_time_type(record_at)
42
+ arr = [start_at, interval_start_at, interval_finish_at, finish_at, record_at].compact.uniq
43
+ arr.sort!
44
+
45
+ if arr.length < 4
46
+ arr.mjust! 4, nil
47
+ end
48
+
49
+ if arr.length == 5
50
+ if arr[1] < self.on_time
51
+ arr.delete_at(1)
52
+ elsif arr[-2] > self.off_time
53
+ arr.delete_at(-2)
54
+ else
55
+ arr.delete_at(2)
56
+ end
57
+ end
58
+
59
+ logger.debug "-----> results: #{arr.map(&->(o){ o.to_s }).inspect}"
60
+
61
+ result = arr.find_index { |x| x == record_at }
62
+
63
+ self.start_at = arr[0]
64
+ self.interval_start_at = arr[1] if arr[1] && arr[1] > self.on_time
65
+ self.interval_finish_at = arr[-2] if arr[-2] && arr[-2] < self.off_time
66
+ self.finish_at = arr[-1]
67
+
68
+ if result
69
+ ['start_at', 'interval_start_at', 'interval_finish_at', 'finish_at'].fetch(result)
70
+ else
71
+ nil
72
+ end
73
+ end
74
+
75
+ def compute_summary
76
+ if self.finish_at && self.start_at
77
+ self.total_hours = (self.finish_at - self.start_at).div(1800) / 2.0
78
+ else
79
+ return
80
+ end
81
+
82
+ late_summary
83
+ leave_summary
84
+ self.absence_minutes = self.late_minutes.to_i + self.leave_minutes.to_i
85
+
86
+ interval_summary
87
+ total_leave = self.interval_hours.to_f + (self.absence_minutes / 30.0).ceil / 2.0
88
+ self.attend_hours = 8 - total_leave
89
+
90
+ if self.attend_hours < 8 && self.attend_hours > 4
91
+ self.kind = 'belated'
92
+ elsif self.attend_hours < 4
93
+ self.kind = 'no_meal'
94
+ else
95
+ self.kind = 'allowance'
96
+ end
97
+
98
+ self.overtime_hours = morning_over + evening_over
99
+ self.processed = true
100
+ self.changes
101
+ end
102
+
103
+ def compute_summary!
104
+ self.compute_summary
105
+ self.save
106
+ end
107
+
108
+ def start_time
109
+ @start_time ||= start_at.in_time_zone(member.timezone)
110
+ end
111
+
112
+ def finish_time
113
+ @finish_time ||= finish_at.in_time_zone(member.timezone)
114
+ end
115
+
116
+ def interval_start_time
117
+ @interval_start_time ||= interval_start_at.in_time_zone(member.timezone)
118
+ end
119
+
120
+ def interval_finish_time
121
+ @interval_finish_time ||= interval_finish_at.in_time_zone(member.timezone)
122
+ end
123
+
124
+ def off_time
125
+ return @off_time if @off_time
126
+ if night_shift
127
+ @off_time = self.attend_on.to_datetime.in_time_zone(member.timezone).change hour: 21
128
+ else
129
+ @off_time = self.member.off_time(self.attend_on)
130
+ end
131
+ end
132
+
133
+ def on_time
134
+ return @on_time if @on_time
135
+ if night_shift
136
+ @on_time = self.attend_on.to_datetime.in_time_zone(member.timezone).change hour: 13
137
+ else
138
+ @on_time = self.member.on_time(self.attend_on)
139
+ end
140
+ end
141
+
142
+ def night_shift
143
+ return false unless member.department&.night_shift
144
+
145
+ self.start_time.hour <= 13 && self.start_time.hour >= 11 && self.finish_time.hour >= 21
146
+ end
147
+
148
+ def lunch_time
149
+ return @lunch_time if @lunch_time
150
+ start = self.member.lunch_time[0].change year: attend_on.year, month: attend_on.month, day: attend_on.day
151
+ finish = self.member.lunch_time[1].change year: attend_on.year, month: attend_on.month, day: attend_on.day
152
+ @lunch_time = [start, finish]
153
+ end
154
+
155
+ def interval_time
156
+ return @interval_time if @interval_time
157
+ return [] unless interval_start_time && interval_finish_time
158
+
159
+ if interval_start_time > lunch_time[0] && interval_start_time < lunch_time[1]
160
+ start = lunch_time[1]
161
+ else
162
+ start = interval_start_time
163
+ end
164
+
165
+ if interval_finish_time > lunch_time[0] && interval_finish_time < lunch_time[1] && interval_finish_time > start
166
+ finish = lunch_time[0]
167
+ elsif interval_finish_time > lunch_time[0] && interval_finish_time < lunch_time[1] && interval_finish_time <= start
168
+ finish = lunch_time[1]
169
+ else
170
+ finish = interval_finish_time
171
+ end
172
+
173
+ @interval_time = [start, finish]
174
+ end
175
+
176
+ def morning_time
177
+ @morning_time ||= on_time - 30.minutes
178
+ end
179
+
180
+ def evening_time
181
+ @evening_time ||= off_time + 90.minutes
182
+ end
183
+
184
+ def evening_over
185
+ return 0 if self.finish_at.blank? || leave_early?
186
+ if self.finish_time < evening_time
187
+ result = self.finish_time.to_i - off_time.to_i
188
+ else
189
+ result = evening_time.to_i - off_time.to_i
190
+ end
191
+
192
+ result.div(1800) / 2.0
193
+ end
194
+
195
+ def morning_over
196
+ return 0 if self.start_at.blank? || late?
197
+ if self.start_time < morning_time
198
+ result = on_time.to_i - morning_time.to_i
199
+ else
200
+ result = on_time.to_i - self.start_time.to_i
201
+ end
202
+
203
+ result.div(1800) / 2.0
204
+ end
205
+
206
+ def late?
207
+ self.start_at && self.start_time > self.on_time
208
+ end
209
+
210
+ def late_summary
211
+ return unless self.start_time
212
+ unless late?
213
+ self.late_absence_id = nil
214
+ return if self.late_minutes.blank?
215
+ end
216
+
217
+ if self.start_time > self.on_time
218
+ self.late_minutes = ((self.start_time.to_i - on_time.to_i) / 60.0).floor
219
+ _late = Absence.default_where(member_id: self.member_id, 'start_at-lte': start_time, 'finish_at-gte': start_time, 'finish_at-lte': off_time).take
220
+ if _late
221
+ self.late_absence_id = _late.id
222
+ else
223
+ self.late_absence_id = nil
224
+ end
225
+ else
226
+ self.late_minutes = nil
227
+ end
228
+ end
229
+
230
+ def leave_early?
231
+ self.finish_at && self.finish_time < self.off_time
232
+ end
233
+
234
+ def leave_summary
235
+ return unless self.finish_at
236
+ unless leave_early?
237
+ self.leave_absence_id = nil
238
+ return if self.leave_minutes.blank?
239
+ end
240
+
241
+ if self.finish_time < self.off_time
242
+ self.leave_minutes = ((off_time.to_i - self.finish_time.to_i) / 60.0).floor
243
+ _leave = Absence.default_where(member_id: self.member_id, 'start_at-gte': finish_time, 'finish_at-lte': off_time).take
244
+ if _leave
245
+ self.leave_absence_id = _leave.id
246
+ else
247
+ self.leave_absence_id = nil
248
+ end
249
+ else
250
+ self.leave_minutes = nil
251
+ end
252
+ end
253
+
254
+ def interval_leave?
255
+ return false unless interval_start_at && interval_finish_at
256
+ !(self.interval_start_time > self.lunch_time[0] && self.interval_finish_time < self.lunch_time[1])
257
+ end
258
+
259
+ def interval_summary
260
+ return unless interval_leave?
261
+
262
+ _interval = Absence.default_where(member_id: self.member_id, 'start_at-lte': interval_start_time, 'finish_at-gte': interval_finish_time).take
263
+ if _interval
264
+ self.interval_absence_id = _interval.id
265
+ else
266
+ self.interval_absence_id = nil
267
+ end
268
+
269
+ _interval_seconds = TimeHelper.interval(self.interval_time[0], self.interval_time[1], interval_start: '12:30')
270
+ interval = (_interval_seconds / 1800.0).ceil / 2.0
271
+ self.interval_hours = interval
272
+ end
273
+
274
+ def financial_month
275
+ @financial_month ||= FinancialMonth.current_month(self.attend_on)
276
+ end
277
+
278
+ def sync_workday
279
+ if self.rest_day?
280
+ self.workday = false
281
+ end
282
+ end
283
+
284
+ def work_day?
285
+ financial_month.work_day?(self.attend_on)
286
+ end
287
+
288
+ def rest_day?
289
+ financial_month.rest_day?(self.attend_on)
290
+ end
291
+
292
+ class_methods do
293
+ def compute_summary!
294
+ self.where(processed: false).each do |at|
295
+ at.compute_summary!
296
+ end
297
+ end
298
+
299
+ def notify_completion
300
+ normal_ids = self.distinct(:member_id).where(finish_time: nil).where.not(start_at: nil).pluck(:member_id)
301
+ normal_ids.each do |member_id|
302
+ AttendanceMailer.normal_completion(member_id).deliver_later
303
+ end
304
+
305
+ interval_ids = self.distinct(:member_id).where(interval_finish_at: nil).where.not(interval_start_at: nil).pluck(:member_id)
306
+ interval_ids.each do |member_id|
307
+ AttendanceMailer.interval_completion(member_id).deliver_later
308
+ end
309
+ [normal_ids, interval_ids]
310
+ end
311
+ end
312
+
313
+ end
314
+ end