+
-
-
-
-
-
-
+
-
+
+
+
-
@@ -283,13 +397,22 @@
active="0"
sequence="10"/>
+
+
+
+
+
diff --git a/addons_extensions/hr_recruitment_extended/views/survey_survey.xml b/addons_extensions/hr_recruitment_extended/views/survey_survey.xml
new file mode 100644
index 000000000..32561ed4e
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/views/survey_survey.xml
@@ -0,0 +1,14 @@
+
+
+
+ hr.survey.survey.form.inherit
+ survey.survey
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/hrms_emp_dashboard/__init__.py b/addons_extensions/hrms_emp_dashboard/__init__.py
index e046e49fb..91c5580fe 100644
--- a/addons_extensions/hrms_emp_dashboard/__init__.py
+++ b/addons_extensions/hrms_emp_dashboard/__init__.py
@@ -1 +1,2 @@
from . import controllers
+from . import models
diff --git a/addons_extensions/hrms_emp_dashboard/__manifest__.py b/addons_extensions/hrms_emp_dashboard/__manifest__.py
index 0eee6443a..099561d7f 100644
--- a/addons_extensions/hrms_emp_dashboard/__manifest__.py
+++ b/addons_extensions/hrms_emp_dashboard/__manifest__.py
@@ -1,8 +1,8 @@
{
- "name": "HRMS Employee Dashboard",
+ "name": "Dashboard",
"version": "18.0.1.0.0",
"category": "Human Resources",
- "summary": "Employee self-service dashboard with attendance, leaves, expenses, equipment, and payslips",
+ "summary": "Employee, manager, and HR self-service dashboard",
"author": "Pranay",
"license": "LGPL-3",
"depends": [
@@ -11,6 +11,12 @@
"hr",
"hr_attendance",
"hr_holidays",
+ "hr_expense",
+ "calendar",
+ "project_todo",
+ "hr_resignation",
+ "knowledge",
+ "website",
"maintenance",
"employee_it_declaration",
"business_travel_expense_management",
@@ -18,6 +24,7 @@
],
"data": [
"views/hrms_emp_dashboard_views.xml",
+ "views/res_config_settings_views.xml",
],
"assets": {
"web.assets_backend": [
diff --git a/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py b/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py
index 9ae0b4e09..79a108c84 100644
--- a/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py
+++ b/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py
@@ -34,11 +34,17 @@ class HrmsEmployeeDashboard(http.Controller):
attendances = self._get_attendances(employee, range_start, range_end)
leaves = self._get_leaves(employee, range_start, range_end)
public_holidays = self._get_public_holidays(employee, range_start, range_end)
+ all_public_holidays = self._get_all_public_holidays(employee)
calendar_attendances = self._get_attendances(employee, calendar_start, calendar_end)
calendar_leaves = self._get_leaves(employee, calendar_start, calendar_end)
calendar_public_holidays = self._get_public_holidays(employee, calendar_start, calendar_end)
+ calendar_events = self._get_calendar_events(employee, calendar_start, calendar_end)
return {
"success": True,
+ "access": {
+ "manager": request.env.user.has_group("hr.group_hr_user"),
+ "hr": request.env.user.has_group("hr.group_hr_manager"),
+ },
"employee": self._employee_card(employee),
"attendance_state": employee.attendance_state,
"date_from": range_start.strftime("%Y-%m-%d"),
@@ -48,20 +54,117 @@ class HrmsEmployeeDashboard(http.Controller):
"calendar_view": calendar_view,
"leave_balances": self._leave_balances(employee),
"public_holidays": self._holiday_list(public_holidays),
+ "all_public_holidays": self._holiday_list(all_public_holidays),
"attendance_calendar": self._attendance_calendar(
calendar_start,
calendar_end,
calendar_attendances,
calendar_leaves,
calendar_public_holidays,
+ calendar_events,
calendar_view,
),
"attendance_summary": self._attendance_summary(range_start, range_end, attendances, leaves, public_holidays),
+ "dashboard_menus": self._dashboard_menus(employee, range_start, range_end),
"expenses": self._expense_data(employee, range_start, range_end),
"equipment": self._equipment_data(employee),
"latest_payslip": self._latest_payslip(employee),
+ "manager_dashboard": self._manager_dashboard(employee, range_start, range_end),
+ "hr_dashboard": self._hr_dashboard(range_start, range_end),
}
+ @http.route('/hrms_emp_dashboard/day_details', type='json', auth='user')
+ def day_details(self, date, **kwargs):
+ try:
+ employee = request.env.user.employee_id
+ if not employee:
+ return {'success': False}
+
+ signals = []
+ day_start = date + ' 00:00:00'
+ day_end = date + ' 23:59:59'
+
+ # 1. Calendar events — use same search as _get_calendar_events
+ try:
+ partner = employee.user_id.partner_id if employee.user_id else False
+ domain = [
+ ('start', '<=', day_end),
+ ('stop', '>=', day_start),
+ ]
+ if partner:
+ domain = ['|', ('partner_ids', 'in', [partner.id]), ('user_id', '=', employee.user_id.id)] + domain
+ else:
+ domain.append(('user_id', '=', employee.user_id.id))
+
+ events = request.env['calendar.event'].sudo().search(domain, order='start asc')
+
+ for event in events:
+ start_dt = fields.Datetime.context_timestamp(request.env.user, event.start)
+ start_time = start_dt.strftime('%H:%M')
+ signals.append({
+ 'type': 'event',
+ 'label': f"{start_time} {event.name}",
+ 'icon': 'fa-calendar',
+ })
+ except Exception:
+ pass
+
+ # 2. Public holidays
+ try:
+ day_date = fields.Date.from_string(date)
+ holidays = request.env['hr.holidays.public.line'].sudo().search([
+ ('date', '=', day_date),
+ ])
+ for holiday in holidays:
+ signals.append({
+ 'type': 'holiday',
+ 'label': holiday.name,
+ 'icon': 'fa-umbrella-beach',
+ })
+ except Exception:
+ pass
+
+ # 3. Leaves
+ try:
+ leaves = request.env['hr.leave'].sudo().search([
+ ('employee_id', '=', employee.id),
+ ('request_date_from', '<=', day_end),
+ ('request_date_to', '>=', day_start),
+ ('state', 'in', ['confirm', 'validate1', 'validate']),
+ ])
+ for leave in leaves:
+ signals.append({
+ 'type': 'leave',
+ 'label': leave.holiday_status_id.name,
+ 'icon': 'fa-calendar-times-o',
+ })
+ except Exception:
+ pass
+
+ # 4. To-dos / tasks
+ try:
+ tasks = request.env['project.task'].sudo().search([
+ ('user_ids', 'in', [request.env.uid]),
+ ('date_deadline', '=', date),
+ ], limit=10)
+ for task in tasks:
+ signals.append({
+ 'type': 'event',
+ 'label': f"To-do: {task.name}",
+ 'icon': 'fa-check-square',
+ })
+ except Exception:
+ pass
+
+ return {
+ 'success': True,
+ 'day': {
+ 'signals': signals,
+ },
+ }
+ except Exception as e:
+ return {'success': False, 'error': str(e)}
+
def _date_range(self, month=False, date_from=False, date_to=False):
today = fields.Date.context_today(request.env.user)
if date_from and date_to:
@@ -84,6 +187,59 @@ class HrmsEmployeeDashboard(http.Controller):
"message": _("Checked in") if employee.sudo().attendance_state == "checked_in" else _("Checked out"),
}
+ @http.route("/hrms_emp_dashboard/approval_action", type="json", auth="user")
+ def approval_action(self, model=False, record_id=False, operation=False, **kwargs):
+ allowed = {
+ "hr.leave": {
+ "approve": ["action_approve", "action_validate"],
+ "reject": ["action_refuse"],
+ },
+ "on.duty.form": {"approve": ["action_approve"], "reject": ["action_reject"]},
+ "late.coming.request": {"approve": ["action_approve"], "reject": ["action_reject"]},
+ "overtime.request": {"approve": ["action_approve"], "reject": ["action_reject"]},
+ "shift.swap.request": {"approve": ["action_approve"], "reject": ["action_reject"]},
+ "travel.trip": {"approve": ["action_approve"]},
+ "travel.expense": {"approve": ["action_approve"]},
+ }
+ if model not in allowed or operation not in allowed[model] or model not in request.env:
+ return {"success": False, "error": _("This approval action is not available.")}
+
+ record = request.env[model].browse(int(record_id or 0)).exists()
+ if not record:
+ return {"success": False, "error": _("Record not found.")}
+ if not self._can_use_record_action(record):
+ return {"success": False, "error": _("You are not allowed to update this record.")}
+
+ last_error = False
+ for method_name in allowed[model][operation]:
+ if not hasattr(record, method_name):
+ continue
+ try:
+ getattr(record, method_name)()
+ return {"success": True, "message": _("Request updated.")}
+ except Exception as error:
+ last_error = error
+ return {"success": False, "error": str(last_error) if last_error else _("No matching action was found.")}
+
+ def _can_use_record_action(self, record):
+ user = request.env.user
+ if user.has_group("hr.group_hr_manager"):
+ return True
+ if not user.has_group("hr.group_hr_user"):
+ return False
+ employee = user.employee_id
+ if not employee:
+ return False
+ if "manager_id" in record._fields and record.manager_id:
+ manager = record.manager_id.sudo()
+ return manager == employee or manager.user_id == user
+ if "employee_id" in record._fields and record.employee_id:
+ return record.employee_id.sudo().parent_id == employee
+ if record._name == "travel.expense" and "activity_id" in record._fields:
+ trip = record.activity_id.trip_id
+ return trip and trip.manager_id.sudo().user_id == user
+ return False
+
def _employee_card(self, employee):
return {
"id": employee.id,
@@ -200,15 +356,54 @@ class HrmsEmployeeDashboard(http.Controller):
)
return holidays
- def _attendance_calendar(self, month_start, month_end, attendances, leaves, public_holidays, calendar_view="monthly"):
+ def _get_all_public_holidays(self, employee):
+ today = date.today()
+
+ year_start = date(today.year, 1, 1)
+ year_end = date(today.year, 12, 31)
+
+ start_dt = datetime.combine(year_start, time.min)
+ end_dt = datetime.combine(year_end, time.max)
+
+ calendar = employee.resource_calendar_id or employee.company_id.resource_calendar_id
+ holidays = request.env["resource.calendar.leaves"].sudo().search([
+ ("date_from", "<=", end_dt),
+ ("date_to", ">=", start_dt),
+ ("company_id", "in", [False, employee.company_id.id]),
+ ("resource_id", "=", False), # Only company/public holidays
+ ], order="date_from asc")
+ if calendar:
+ holidays = holidays.filtered(
+ lambda h: not h.calendar_id or h.calendar_id == calendar
+ )
+ return holidays
+
+ def _get_calendar_events(self, employee, range_start, range_end):
+ if "calendar.event" not in request.env:
+ return request.env["ir.model"].browse()
+ start_dt = datetime.combine(range_start, time.min)
+ end_dt = datetime.combine(range_end, time.max)
+ domain = [
+ ("start", "<=", end_dt),
+ ("stop", ">=", start_dt),
+ ]
+ partner = employee.user_id.partner_id
+ if partner:
+ domain = ["|", ("partner_ids", "in", [partner.id]), ("user_id", "=", employee.user_id.id)] + domain
+ else:
+ domain.append(("user_id", "=", employee.user_id.id))
+ return request.env["calendar.event"].sudo().search(domain, order="start asc", limit=500)
+
+ def _attendance_calendar(self, month_start, month_end, attendances, leaves, public_holidays, calendar_events=False, calendar_view="monthly"):
if calendar_view == "yearly":
- return self._yearly_attendance_calendar(month_start, month_end, attendances, leaves, public_holidays)
+ return self._yearly_attendance_calendar(month_start, month_end, attendances, leaves, public_holidays, calendar_events)
today = fields.Date.context_today(request.env.user)
attendance_by_day = self._attendance_hours_by_day(attendances)
leave_by_day = self._leave_days(leaves)
holiday_by_day = self._days_from_datetime_range_records(public_holidays, "date_from", "date_to")
+ events_by_day = self._calendar_events_by_day(calendar_events)
days = []
cursor = month_start
@@ -227,6 +422,7 @@ class HrmsEmployeeDashboard(http.Controller):
break_hours = round(attendance_hours.get("break_hours", 0.0), 2)
has_worked_hours = hours > 0.004
is_holiday = cursor in holiday_by_day
+ day_events = events_by_day.get(cursor, [])
is_leave = cursor in leave_by_day
is_half_day_leave = bool(leave_by_day.get(cursor, {}).get("is_half_day"))
is_weekend = cursor.weekday() in (5, 6)
@@ -267,6 +463,10 @@ class HrmsEmployeeDashboard(http.Controller):
"outside_period": outside_period,
"status": status,
"label": label,
+ "events": day_events,
+ "event_count": len(day_events),
+ "has_events": bool(day_events),
+ "signals": self._calendar_day_signals(holiday_by_day.get(cursor), leave_by_day.get(cursor), day_events),
"show_metrics": show_metrics,
"is_half_day_leave": is_half_day_leave,
"hours": hours,
@@ -280,6 +480,18 @@ class HrmsEmployeeDashboard(http.Controller):
"expected_display": self._format_hours(expected_hours),
"leave_display": self._format_hours(leave_hours),
"balance_display": self._format_signed_hours(hours - expected_hours),
+ "details": self._calendar_day_details(
+ cursor,
+ attendance_hours,
+ leave_by_day.get(cursor),
+ holiday_by_day.get(cursor),
+ day_events,
+ expected_hours,
+ hours,
+ break_hours,
+ status,
+ ),
+ "actions": self._calendar_day_actions(cursor, status),
})
cursor += timedelta(days=1)
return days
@@ -298,6 +510,39 @@ class HrmsEmployeeDashboard(http.Controller):
start += timedelta(days=1)
return result
+ def _calendar_events_by_day(self, events):
+ result = defaultdict(list)
+ for event in events or []:
+ start_dt = fields.Datetime.context_timestamp(request.env.user, event.start)
+ stop_dt = fields.Datetime.context_timestamp(request.env.user, event.stop)
+ cursor = start_dt.date()
+ end = stop_dt.date()
+ event_info = {
+ "id": event.id,
+ "name": event.name or _("Meeting"),
+ "start": fields.Datetime.to_string(start_dt),
+ "stop": fields.Datetime.to_string(stop_dt),
+ "display_time": event.display_time or "",
+ "allday": bool(event.allday),
+ "attendee_count": len(event.partner_ids),
+ }
+ while cursor and end and cursor <= end:
+ result[cursor].append(event_info)
+ cursor += timedelta(days=1)
+ return result
+
+ def _calendar_day_signals(self, holiday_label, leave_data, events):
+ signals = []
+ if holiday_label:
+ signals.append({"type": "holiday", "label": holiday_label, "icon": "fa fa-star"})
+ if leave_data:
+ signals.append({"type": "leave", "label": _("Time Off"), "icon": "fa fa-plane"})
+ for event in (events or [])[:2]:
+ signals.append({"type": "event", "label": event["name"], "icon": "fa fa-users"})
+ if len(events or []) > 2:
+ signals.append({"type": "more", "label": _("+%s more") % (len(events) - 2), "icon": "fa fa-ellipsis-h"})
+ return signals
+
def _is_half_day_leave(self, leave):
if "request_unit_half" in leave._fields and leave.request_unit_half:
return True
@@ -307,8 +552,8 @@ class HrmsEmployeeDashboard(http.Controller):
return True
return bool(leave.number_of_days and leave.number_of_days < 1)
- def _yearly_attendance_calendar(self, range_start, range_end, attendances, leaves, public_holidays):
- days = self._attendance_calendar(range_start, range_end, attendances, leaves, public_holidays, "monthly")
+ def _yearly_attendance_calendar(self, range_start, range_end, attendances, leaves, public_holidays, calendar_events=False):
+ days = self._attendance_calendar(range_start, range_end, attendances, leaves, public_holidays, calendar_events, "monthly")
months = []
cursor = range_start.replace(day=1)
while cursor <= range_end:
@@ -317,6 +562,7 @@ class HrmsEmployeeDashboard(http.Controller):
counts = defaultdict(int)
for day in month_days:
counts[day["status"]] += 1
+ event_count = sum(day.get("event_count", 0) for day in month_days)
months.append({
"type": "month",
"date": month_key,
@@ -328,6 +574,11 @@ class HrmsEmployeeDashboard(http.Controller):
"absent": counts["absent"],
"leave": counts["leave"],
"holiday": counts["holiday"],
+ "event_count": event_count,
+ "has_events": bool(event_count),
+ "signals": [
+ {"type": "event", "label": _("%s meetings/events") % event_count, "icon": "fa fa-users"}
+ ] if event_count else [],
"hours": round(sum(day["hours"] for day in month_days), 2),
"worked_hours": round(sum(day["worked_hours"] for day in month_days), 2),
"break_hours": round(sum(day["break_hours"] for day in month_days), 2),
@@ -335,6 +586,17 @@ class HrmsEmployeeDashboard(http.Controller):
"worked_display": self._format_hours(sum(day["worked_hours"] for day in month_days)),
"break_display": self._format_hours(sum(day["break_hours"] for day in month_days)),
"expected_display": self._format_hours(sum(day["expected_hours"] for day in month_days)),
+ "details": [
+ {"label": _("Present Days"), "value": counts["present"]},
+ {"label": _("Leave Days"), "value": counts["leave"]},
+ {"label": _("Public Holidays"), "value": counts["holiday"]},
+ {"label": _("Meetings/Events"), "value": event_count},
+ {"label": _("Worked Hours"), "value": self._format_hours(sum(day["worked_hours"] for day in month_days))},
+ ],
+ "actions": [
+ {"key": "create_meeting", "label": _("Create Meeting"), "icon": "fa fa-users", "primary": True},
+ {"key": "open_calendar", "label": _("Open Calendar"), "icon": "fa fa-calendar"},
+ ],
})
cursor += relativedelta(months=1)
return months
@@ -359,9 +621,61 @@ class HrmsEmployeeDashboard(http.Controller):
result[day] = {
"worked_hours": worked_hours,
"break_hours": break_hours,
+ "entries": [{
+ "check_in": fields.Datetime.to_string(fields.Datetime.context_timestamp(request.env.user, attendance.check_in)) if attendance.check_in else "",
+ "check_out": fields.Datetime.to_string(fields.Datetime.context_timestamp(request.env.user, attendance.check_out)) if attendance.check_out else _("Open"),
+ "worked_display": self._format_hours(attendance.worked_hours or 0.0),
+ } for attendance in sorted_attendances],
}
return result
+ def _calendar_day_details(self, day, attendance_data, leave_data, holiday_label, events, expected_hours, worked_hours, break_hours, status):
+ details = [
+ {"label": _("Date"), "value": fields.Date.to_string(day)},
+ {"label": _("Status"), "value": self._calendar_status_label(status)},
+ {"label": _("Expected"), "value": self._format_hours(expected_hours)},
+ {"label": _("Worked"), "value": self._format_hours(worked_hours)},
+ {"label": _("Break"), "value": self._format_hours(break_hours)},
+ ]
+ if holiday_label:
+ details.append({"label": _("Holiday"), "value": holiday_label})
+ if leave_data:
+ details.append({"label": _("Time Off"), "value": _("Half Day") if leave_data.get("is_half_day") else _("Full Day")})
+ for event in events or []:
+ details.append({
+ "label": _("Meeting/Event"),
+ "value": "%s%s" % (event["name"], (" - %s" % event["display_time"]) if event["display_time"] else ""),
+ })
+ for index, entry in enumerate(attendance_data.get("entries", []), start=1):
+ details.append({
+ "label": _("Attendance %s") % index,
+ "value": "%s - %s (%s)" % (entry["check_in"], entry["check_out"], entry["worked_display"]),
+ })
+ return details
+
+ def _calendar_status_label(self, status):
+ labels = {
+ "present": _("Present"),
+ "absent": _("Absent"),
+ "leave": _("Time Off"),
+ "holiday": _("Public Holiday"),
+ "weekend": _("Weekend"),
+ "future": _("Upcoming"),
+ "empty": _("Outside Period"),
+ }
+ return labels.get(status, status.title() if status else "")
+
+ def _calendar_day_actions(self, day, status):
+ actions = [
+ {"key": "create_meeting", "label": _("Create Meeting"), "icon": "fa fa-users", "primary": True},
+ {"key": "apply_leave", "label": _("Apply Leave"), "icon": "fa fa-calendar-plus-o", "primary": False},
+ {"key": "add_todo", "label": _("Add To-do"), "icon": "fa fa-check-square-o", "primary": False},
+ {"key": "open_calendar", "label": _("Open Calendar"), "icon": "fa fa-calendar", "primary": False},
+ ]
+ if status in ("present", "absent"):
+ actions.insert(2, {"key": "open_attendance", "label": _("Attendance"), "icon": "fa fa-clock-o", "primary": False})
+ return actions
+
def _expected_hours_for_day(self, employee, day):
calendar = employee.resource_calendar_id or employee.company_id.resource_calendar_id
if not calendar:
@@ -410,6 +724,7 @@ class HrmsEmployeeDashboard(http.Controller):
attendances,
leaves,
public_holidays,
+ False,
)
counts = defaultdict(int)
@@ -453,13 +768,344 @@ class HrmsEmployeeDashboard(http.Controller):
"remaining_display": self._format_signed_hours(remaining_hours),
}
+ def _dashboard_menus(self, employee, date_from, date_to):
+ user = request.env.user
+ settings = self._dashboard_settings()
+ tiles = [
+ self._menu_tile(
+ key="leave",
+ title=_("Time Off"),
+ subtitle=_("Requests and balances"),
+ icon="fa fa-calendar-plus-o",
+ color="green",
+ count=self._safe_search_count("hr.leave", [
+ ("employee_id", "=", employee.id),
+ ("state", "in", ["confirm", "validate1", "validate"]),
+ ("request_date_from", "<=", date_to),
+ ("request_date_to", ">=", date_from),
+ ]),
+ count_label=_("requests"),
+ model="hr.leave",
+ domain=[["employee_id", "=", employee.id]],
+ views=[[False, "list"], [False, "form"], [False, "calendar"]],
+ context={"default_employee_id": employee.id},
+ create_action={
+ "type": "ir.actions.act_window",
+ "name": _("Apply Time Off"),
+ "res_model": "hr.leave",
+ "views": [[False, "form"]],
+ "target": "new",
+ "context": {"default_employee_id": employee.id},
+ },
+ ),
+ self._employee_optional_tile(
+ settings["show_on_duty"],
+ key="on_duty",
+ title=_("On-Duty"),
+ subtitle=_("Duty outside office"),
+ icon="fa fa-map-marker",
+ color="cyan",
+ model="on.duty.form",
+ domain=[["employee_id", "=", employee.id]],
+ count_domain=[("employee_id", "=", employee.id)],
+ create_action={
+ "type": "ir.actions.act_window",
+ "name": _("New On-Duty Request"),
+ "res_model": "on.duty.form",
+ "views": [[False, "form"]],
+ "target": "new",
+ "context": {"default_employee_id": employee.id},
+ },
+ ),
+ self._employee_optional_tile(
+ settings["show_late_coming"],
+ key="late_coming",
+ title=_("Late Coming"),
+ subtitle=_("Late and early-out approvals"),
+ icon="fa fa-clock-o",
+ color="amber",
+ model="late.coming.request",
+ domain=[["employee_id", "=", employee.id]],
+ count_domain=[("employee_id", "=", employee.id)],
+ create_action={
+ "type": "ir.actions.act_window",
+ "name": _("New Late Coming Request"),
+ "res_model": "late.coming.request",
+ "views": [[False, "form"]],
+ "target": "new",
+ "context": {"default_employee_id": employee.id},
+ },
+ ),
+ self._employee_optional_tile(
+ settings["show_overtime"],
+ key="overtime",
+ title=_("Overtime"),
+ subtitle=_("Extra hours approval"),
+ icon="fa fa-hourglass-half",
+ color="violet",
+ model="overtime.request",
+ domain=[["employee_id", "=", employee.id]],
+ count_domain=[("employee_id", "=", employee.id)],
+ create_action={
+ "type": "ir.actions.act_window",
+ "name": _("New Overtime Request"),
+ "res_model": "overtime.request",
+ "views": [[False, "form"]],
+ "target": "new",
+ "context": {"default_employee_id": employee.id},
+ },
+ ),
+ self._employee_optional_tile(
+ settings["show_shift_swap"],
+ key="shift_swap",
+ title=_("Shift Swap"),
+ subtitle=_("Swap rostered shifts"),
+ icon="fa fa-exchange",
+ color="rose",
+ model="shift.swap.request",
+ domain=[["employee_id", "=", employee.id]],
+ count_domain=[("employee_id", "=", employee.id)],
+ create_action={
+ "type": "ir.actions.act_window",
+ "name": _("New Shift Swap Request"),
+ "res_model": "shift.swap.request",
+ "views": [[False, "form"]],
+ "target": "new",
+ "context": {"default_employee_id": employee.id},
+ },
+ ),
+ self._menu_tile(
+ key="attendance",
+ title=_("Attendance"),
+ subtitle=_("Entries and working time"),
+ icon="fa fa-clock-o",
+ color="blue",
+ count=self._safe_search_count("hr.attendance", [("employee_id", "=", employee.id)]),
+ count_label=_("entries"),
+ model="hr.attendance",
+ domain=[["employee_id", "=", employee.id]],
+ views=[[False, "list"], [False, "form"]],
+ create_action={
+ "type": "ir.actions.act_window",
+ "name": _("Add Attendance"),
+ "res_model": "hr.attendance",
+ "views": [[False, "form"]],
+ "target": "new",
+ "context": {"default_employee_id": employee.id},
+ },
+ ),
+ self._menu_tile(
+ key="todo",
+ title=_("To-do"),
+ subtitle=_("Personal tasks"),
+ icon="fa fa-check-square-o",
+ color="amber",
+ count=self._safe_search_count("project.task", [("project_id","=",False),("user_ids", "in", [user.id]), ("active", "=", True)]),
+ count_label=_("open"),
+ xml_id="project_todo.project_task_preload_action_todo",
+ fallback_model="project.task",
+ fallback_domain=[["user_ids", "in", [user.id]], ["active", "=", True]],
+ fallback_views=[[False, "kanban"], [False, "list"], [False, "form"]],
+ context={"default_user_ids": [(4, user.id)]},
+ create_action={
+ "type": "ir.actions.act_window",
+ "name": _("New To-do"),
+ "res_model": "project.task",
+ "views": [[False, "form"]],
+ "target": "new",
+ "context": {"default_user_ids": [(4, user.id)]},
+ },
+ ),
+ self._menu_tile(
+ key="employees",
+ title=_("Employees"),
+ subtitle=_("Company directory"),
+ icon="fa fa-users",
+ color="cyan",
+ count=self._safe_search_count("hr.employee", [("active", "=", True)]),
+ count_label=_("people"),
+ xml_id="hr.open_view_employee_list_my",
+ fallback_model="hr.employee",
+ fallback_domain=[["active", "=", True]],
+ fallback_views=[[False, "kanban"], [False, "list"], [False, "form"]],
+ create_action={
+ "type": "ir.actions.act_window",
+ "name": _("New Employee"),
+ "res_model": "hr.employee",
+ "views": [[False, "form"]],
+ "target": "new",
+ },
+ ),
+ self._resignation_tile(employee),
+ self._menu_tile(
+ key="knowledge",
+ title=_("Knowledge"),
+ subtitle=_("Policies and articles"),
+ icon="fa fa-book",
+ color="violet",
+ show_count=False,
+ xml_id="knowledge.ir_actions_server_knowledge_home_page",
+ fallback_model="knowledge.article",
+ fallback_views=[[False, "kanban"], [False, "list"], [False, "form"]],
+ ),
+ self._menu_tile(
+ key="calendar",
+ title=_("Calendar"),
+ subtitle=_("Meetings and events"),
+ icon="fa fa-calendar",
+ color="slate",
+ count=self._safe_search_count("calendar.event", [
+ ("start", "<=", datetime.combine(date_to, time.max)),
+ ("stop", ">=", datetime.combine(date_from, time.min)),
+ ("partner_ids","in",[user.partner_id.id]),
+ ]),
+ count_label=_("events"),
+ xml_id="calendar.action_calendar_event",
+ fallback_model="calendar.event",
+ fallback_views=[[False, "calendar"], [False, "list"], [False, "form"]],
+ create_action={
+ "type": "ir.actions.act_window",
+ "name": _("New Calendar Event"),
+ "res_model": "calendar.event",
+ "views": [[False, "form"]],
+ "target": "new",
+ "context": {
+ "default_partner_ids": [(4, user.partner_id.id)] if user.partner_id else [],
+ },
+ },
+ ),
+ self._menu_tile(
+ key="website",
+ title=_("Website"),
+ subtitle=_("Open company site"),
+ icon="fa fa-globe",
+ color="rose",
+ show_count=False,
+ url="/",
+ ),
+ ]
+ return [tile for tile in tiles if tile]
+
+ def _dashboard_settings(self):
+ params = request.env["ir.config_parameter"].sudo()
+ return {
+ "show_on_duty": params.get_param("hrms_emp_dashboard.show_on_duty", "True") == "True",
+ "show_late_coming": params.get_param("hrms_emp_dashboard.show_late_coming", "True") == "True",
+ "show_overtime": params.get_param("hrms_emp_dashboard.show_overtime", "True") == "True",
+ "show_shift_swap": params.get_param("hrms_emp_dashboard.show_shift_swap", "True") == "True",
+ }
+
+ def _employee_optional_tile(self, enabled, key, title, subtitle, icon, color, model, domain, count_domain, create_action=False):
+ if not enabled or model not in request.env:
+ return False
+ return self._menu_tile(
+ key=key,
+ title=title,
+ subtitle=subtitle,
+ icon=icon,
+ color=color,
+ count=self._safe_search_count(model, count_domain),
+ count_label=_("requests"),
+ model=model,
+ domain=domain,
+ views=[[False, "list"], [False, "form"]],
+ create_action=create_action,
+ )
+
+ def _resignation_tile(self, employee):
+ if "hr.resignation" not in request.env:
+ return False
+ domain = [("employee_id", "=", employee.id)]
+ latest = request.env["hr.resignation"].sudo().search(domain, order="id desc", limit=1)
+ stage = ""
+ if latest:
+ stage = (
+ dict(latest._fields["state"].selection).get(latest.state)
+ or dict(latest._fields["normal_resignation_status"].selection).get(latest.normal_resignation_status)
+ or latest.state
+ or ""
+ )
+ return self._menu_tile(
+ key="resignation",
+ title=_("Resignation"),
+ subtitle=stage or _("Exit request workflow"),
+ icon="fa fa-sign-out",
+ color="red",
+ count=self._safe_search_count("hr.resignation", domain),
+ count_label=_("requests"),
+ xml_id="hr_resignation.hr_resignation_action",
+ fallback_model="hr.resignation",
+ fallback_domain=[["employee_id", "=", employee.id]],
+ fallback_views=[[False, "list"], [False, "form"]],
+ context={"default_employee_id": employee.id},
+ badge=stage,
+ create_action={
+ "type": "ir.actions.act_window",
+ "name": _("New Resignation"),
+ "res_model": "hr.resignation",
+ "views": [[False, "form"]],
+ "target": "new",
+ "context": {"default_employee_id": employee.id},
+ },
+ )
+
+ def _menu_tile(self, key, title, subtitle, icon, color, count=0, count_label="", model=False, domain=False, views=False, context=False, xml_id=False, fallback_model=False, fallback_domain=False, fallback_views=False, show_count=True, url=False, badge=False, create_action=False):
+ action = False
+ if url:
+ action = {"type": "ir.actions.act_url", "url": url, "target": "self"}
+ elif xml_id and request.env.ref(xml_id, raise_if_not_found=False):
+ action = {"xml_id": xml_id}
+ elif model and model in request.env:
+ action = {
+ "type": "ir.actions.act_window",
+ "name": title,
+ "res_model": model,
+ "views": views or [[False, "list"], [False, "form"]],
+ "domain": domain or [],
+ "context": context or {},
+ "target": "current",
+ }
+ elif fallback_model and fallback_model in request.env:
+ action = {
+ "type": "ir.actions.act_window",
+ "name": title,
+ "res_model": fallback_model,
+ "views": fallback_views or [[False, "list"], [False, "form"]],
+ "domain": fallback_domain or [],
+ "context": context or {},
+ "target": "current",
+ }
+ if not action:
+ return False
+ return {
+ "key": key,
+ "title": title,
+ "subtitle": subtitle,
+ "icon": icon,
+ "color": color,
+ "count": count,
+ "count_label": count_label,
+ "show_count": show_count,
+ "badge": badge or "",
+ "action": action,
+ "create_action": create_action or False,
+ }
+
+ def _safe_search_count(self, model_name, domain):
+ if model_name not in request.env:
+ return 0
+ try:
+ return request.env[model_name].sudo().search_count(domain)
+ except Exception:
+ return 0
+
def _holiday_list(self, holidays):
return [{
"id": holiday.id,
"name": holiday.name or _("Public Holiday"),
"date_from": fields.Datetime.to_string(holiday.date_from),
"date_to": fields.Datetime.to_string(holiday.date_to),
- } for holiday in holidays[:10]]
+ } for holiday in holidays[:50]]
def _expense_data(self, employee, date_from, date_to):
labels = []
@@ -514,6 +1160,7 @@ class HrmsEmployeeDashboard(http.Controller):
"category": item.category_id.name if item.category_id else "",
"serial": item.serial_no or getattr(item, "comp_serial_no", "") or "",
"assign_date": self._date_string(item.assign_date),
+ "service_open_count": item.maintenance_open_count or 0,
} for item in equipment]
def _latest_payslip(self, employee):
@@ -527,3 +1174,277 @@ class HrmsEmployeeDashboard(http.Controller):
"date_from": self._date_string(payslip.date_from) if payslip else "",
"date_to": self._date_string(payslip.date_to) if payslip else "",
}
+
+ def _manager_dashboard(self, employee, date_from, date_to):
+ if not request.env.user.has_group("hr.group_hr_user"):
+ return {}
+ user = request.env.user
+ team = request.env["hr.employee"].sudo().search([
+ ("parent_id", "=", employee.id),
+ ("active", "=", True),
+ ], order="name")
+ team_ids = team.ids
+ today = fields.Date.context_today(user)
+ today_start = datetime.combine(today, time.min)
+ today_end = datetime.combine(today, time.max)
+ present_today = self._safe_search_count("hr.attendance", [
+ ("employee_id", "in", team_ids),
+ ("check_in", ">=", today_start),
+ ("check_in", "<=", today_end),
+ ]) if team_ids else 0
+
+ approval_tiles = [
+ self._approval_tile("manager_leave", _("Time Off Approvals"), "hr.leave", [
+ ("employee_id", "in", team_ids),
+ ("state", "in", ["confirm", "validate1"]),
+ ], "fa fa-calendar-check-o", "green"),
+ self._approval_tile("manager_on_duty", _("On-Duty Requests"), "on.duty.form", [
+ ("employee_id", "in", team_ids),
+ ("state", "=", "submitted"),
+ ], "fa fa-map-marker", "blue"),
+ self._approval_tile("manager_late", _("Late Coming"), "late.coming.request", [
+ ("manager_id", "=", employee.id),
+ ("state", "=", "submitted"),
+ ], "fa fa-clock-o", "amber"),
+ self._approval_tile("manager_ot", _("Overtime"), "overtime.request", [
+ ("manager_id", "=", employee.id),
+ ("state", "=", "submitted"),
+ ], "fa fa-hourglass-half", "cyan"),
+ self._approval_tile("manager_shift", _("Shift Swaps"), "shift.swap.request", [
+ ("manager_id", "=", employee.id),
+ ("state", "=", "submitted"),
+ ], "fa fa-exchange", "violet"),
+ self._approval_tile("manager_travel", _("Travel Approvals"), "travel.trip", [
+ ("manager_id", "=", employee.id),
+ ("state", "=", "submitted"),
+ ], "fa fa-plane", "rose"),
+ ]
+ approval_tiles = [tile for tile in approval_tiles if tile]
+ pending_count = sum(tile["count"] for tile in approval_tiles)
+ return {
+ "hero": {
+ "title": _("Manager Self Service"),
+ "subtitle": _("Team approvals, availability, travel and daily exceptions in one place."),
+ },
+ "kpis": [
+ {"label": _("Team Members"), "value": len(team), "tone": "blue"},
+ {"label": _("Present Today"), "value": present_today, "tone": "green"},
+ {"label": _("Pending Approvals"), "value": pending_count, "tone": "amber"},
+ {"label": _("Open Team Tasks"), "value": self._safe_search_count("project.task", [("user_ids", "in", team.mapped("user_id").ids), ("active", "=", True)]) if team else 0, "tone": "violet"},
+ ],
+ "menus": [
+ self._menu_tile(_("Team Directory").lower().replace(" ", "_"), _("Team Directory"), _("Employee records reporting to you"), "fa fa-users", "blue", count=len(team), count_label=_("people"), model="hr.employee", domain=[["id", "in", team_ids]], views=[[False, "kanban"], [False, "list"], [False, "form"]]),
+ self._menu_tile("team_attendance", _("Team Attendance"), _("Today and historical attendance"), "fa fa-id-card-o", "green", count=present_today, count_label=_("today"), model="hr.attendance", domain=[["employee_id", "in", team_ids]], views=[[False, "list"], [False, "form"]]),
+ self._menu_tile("team_tasks", _("Team Tasks"), _("Assigned work and deadlines"), "fa fa-check-square-o", "amber", count=self._safe_search_count("project.task", [("user_ids", "in", team.mapped("user_id").ids), ("active", "=", True)]) if team else 0, count_label=_("open"), model="project.task", domain=[["user_ids", "in", team.mapped("user_id").ids], ["active", "=", True]], views=[[False, "kanban"], [False, "list"], [False, "form"]]),
+ self._menu_tile("team_calendar", _("Team Calendar"), _("Meetings and schedules"), "fa fa-calendar", "slate", show_count=False, xml_id="calendar.action_calendar_event", fallback_model="calendar.event", fallback_views=[[False, "calendar"], [False, "list"], [False, "form"]]),
+ ],
+ "approval_tiles": approval_tiles,
+ "approval_queue": self._manager_approval_queue(employee, team_ids),
+ "team": self._team_snapshot(team, today_start, today_end),
+ }
+
+ def _hr_dashboard(self, date_from, date_to):
+ if not request.env.user.has_group("hr.group_hr_manager"):
+ return {}
+ employees_count = self._safe_search_count("hr.employee", [("active", "=", True)])
+ pending_tiles = [
+ self._approval_tile("hr_leave", _("Time Off"), "hr.leave", [("state", "in", ["confirm", "validate1"])], "fa fa-calendar-check-o", "green"),
+ self._approval_tile("hr_on_duty", _("On-Duty"), "on.duty.form", [("state", "=", "submitted")], "fa fa-map-marker", "blue"),
+ self._approval_tile("hr_late", _("Late Coming"), "late.coming.request", [("state", "=", "submitted")], "fa fa-clock-o", "amber"),
+ self._approval_tile("hr_ot", _("Overtime"), "overtime.request", [("state", "=", "submitted")], "fa fa-hourglass-half", "cyan"),
+ self._approval_tile("hr_shift", _("Shift Swaps"), "shift.swap.request", [("state", "=", "submitted")], "fa fa-exchange", "violet"),
+ self._approval_tile("hr_travel", _("Travel"), "travel.trip", [("state", "=", "submitted")], "fa fa-plane", "rose"),
+ self._approval_tile("hr_resignation", _("Resignations"), "hr.resignation", [("state", "not in", ["cancel", "cancelled", "done", "approved", "refuse", "rejected"])], "fa fa-sign-out", "red"),
+ ]
+ pending_tiles = [tile for tile in pending_tiles if tile]
+ return {
+ "hero": {
+ "title": _("HR Self Service"),
+ "subtitle": _("Workforce administration, pending approvals, payroll and HR operations."),
+ },
+ "kpis": [
+ {"label": _("Active Employees"), "value": employees_count, "tone": "blue"},
+ {"label": _("Departments"), "value": self._safe_search_count("hr.department", []), "tone": "green"},
+ {"label": _("Pending HR Items"), "value": sum(tile["count"] for tile in pending_tiles), "tone": "amber"},
+ {"label": _("Open Positions"), "value": self._safe_search_count("hr.job", [("active", "=", True)]), "tone": "violet"},
+ ],
+ "menus": [
+ self._menu_tile("hr_employees", _("Employees"), _("Manage employee master data"), "fa fa-users", "blue", count=employees_count, count_label=_("active"), xml_id="hr.open_view_employee_list_my", fallback_model="hr.employee", fallback_domain=[["active", "=", True]], fallback_views=[[False, "kanban"], [False, "list"], [False, "form"]]),
+ self._menu_tile("hr_contracts", _("Contracts"), _("Employment contracts"), "fa fa-file-text-o", "green", count=self._safe_search_count("hr.contract", []), count_label=_("records"), model="hr.contract", views=[[False, "list"], [False, "form"]]),
+ self._menu_tile("hr_payroll", _("Payroll"), _("Payslips and batches"), "fa fa-money", "amber", count=self._safe_search_count("hr.payslip", [("state", "in", ["draft", "verify"])]), count_label=_("to process"), xml_id="hr_payroll.action_view_hr_payslip_month_form", fallback_model="hr.payslip", fallback_views=[[False, "list"], [False, "form"]]),
+ self._menu_tile("hr_recruitment", _("Recruitment"), _("Jobs and applicants"), "fa fa-briefcase", "cyan", count=self._safe_search_count("hr.applicant", [("active", "=", True)]), count_label=_("applicants"), xml_id="hr_recruitment.crm_case_categ0_act_job", fallback_model="hr.applicant", fallback_views=[[False, "kanban"], [False, "list"], [False, "form"]]),
+ self._menu_tile("hr_appraisal", _("Appraisals"), _("Cycles and reviews"), "fa fa-star", "violet", count=self._safe_search_count("hr.notice.appraisal", [("state", "in", ["draft", "sent", "postponed"])]), count_label=_("cycles"), model="hr.notice.appraisal", views=[[False, "list"], [False, "form"]]),
+ self._menu_tile("hr_salary_advance", _("Salary Advances"), _("Advances and deductions"), "fa fa-credit-card", "rose", count=self._safe_search_count("hr.salary.advance", [("state", "=", "open")]), count_label=_("running"), model="hr.salary.advance", views=[[False, "list"], [False, "form"]]),
+ ],
+ "approval_tiles": pending_tiles,
+ "approval_queue": self._hr_approval_queue(),
+ "workforce": self._workforce_snapshot(date_from, date_to),
+ }
+
+ def _approval_tile(self, key, title, model, domain, icon, color):
+ if model not in request.env:
+ return False
+ return self._menu_tile(
+ key=key,
+ title=title,
+ subtitle=_("Review and approve pending work"),
+ icon=icon,
+ color=color,
+ count=self._safe_search_count(model, domain),
+ count_label=_("pending"),
+ model=model,
+ domain=[list(item) for item in domain],
+ views=[[False, "list"], [False, "form"]],
+ )
+
+ def _manager_approval_queue(self, manager, team_ids):
+ specs = [
+ ("hr.leave", [("employee_id", "in", team_ids), ("state", "in", ["confirm", "validate1"])], _("Time Off"), "request_date_from"),
+ ("on.duty.form", [("employee_id", "in", team_ids), ("state", "=", "submitted")], _("On-Duty"), "start_date"),
+ ("late.coming.request", [("manager_id", "=", manager.id), ("state", "=", "submitted")], _("Late Coming"), "attendance_date"),
+ ("overtime.request", [("manager_id", "=", manager.id), ("state", "=", "submitted")], _("Overtime"), "attendance_date"),
+ ("shift.swap.request", [("manager_id", "=", manager.id), ("state", "=", "submitted")], _("Shift Swap"), "roster_date"),
+ ("travel.trip", [("manager_id", "=", manager.id), ("state", "=", "submitted")], _("Travel"), "start_date"),
+ ]
+ return self._approval_records(specs)
+
+ def _hr_approval_queue(self):
+ specs = [
+ ("hr.leave", [("state", "in", ["confirm", "validate1"])], _("Time Off"), "request_date_from"),
+ ("on.duty.form", [("state", "=", "submitted")], _("On-Duty"), "start_date"),
+ ("late.coming.request", [("state", "=", "submitted")], _("Late Coming"), "attendance_date"),
+ ("overtime.request", [("state", "=", "submitted")], _("Overtime"), "attendance_date"),
+ ("shift.swap.request", [("state", "=", "submitted")], _("Shift Swap"), "roster_date"),
+ ("travel.trip", [("state", "=", "submitted")], _("Travel"), "start_date"),
+ ]
+ return self._approval_records(specs)
+
+ def _approval_records(self, specs, limit=18):
+ rows = []
+ for model, domain, category, date_field in specs:
+ if model not in request.env:
+ continue
+ try:
+ records = request.env[model].sudo().search(domain, order="id desc", limit=6)
+ except Exception:
+ continue
+ for record in records:
+ rows.append(self._approval_record(record, category, date_field))
+ return sorted(rows, key=lambda item: item["sort_date"] or "", reverse=True)[:limit]
+
+ def _approval_record(self, record, category, date_field):
+ employee = record.employee_id if "employee_id" in record._fields else False
+ date_value = record[date_field] if date_field in record._fields else False
+ title = getattr(record, "display_name", "") or getattr(record, "name", "") or category
+ return {
+ "id": record.id,
+ "model": record._name,
+ "category": category,
+ "title": title,
+ "employee": employee.name if employee else "",
+ "department": employee.department_id.name if employee and employee.department_id else "",
+ "date": self._date_string(date_value),
+ "sort_date": self._date_string(date_value),
+ "state": record.state if "state" in record._fields else "",
+ "state_label": self._selection_label(record, "state"),
+ "can_approve": hasattr(record, "action_approve") or hasattr(record, "action_validate"),
+ "can_reject": hasattr(record, "action_reject") or hasattr(record, "action_refuse"),
+ "action": {
+ "type": "ir.actions.act_window",
+ "name": category,
+ "res_model": record._name,
+ "views": [[False, "form"]],
+ "res_id": record.id,
+ "target": "current",
+ },
+ }
+
+ def _selection_label(self, record, field_name):
+ if field_name not in record._fields:
+ return ""
+ selection = dict(record._fields[field_name].selection)
+ return selection.get(record[field_name], record[field_name] or "")
+
+ def _team_snapshot(self, team, today_start, today_end):
+ rows = []
+ attendance_model = request.env["hr.attendance"].sudo() if "hr.attendance" in request.env else False
+ leave_model = request.env["hr.leave"].sudo() if "hr.leave" in request.env else False
+ for employee in team[:12]:
+ present = False
+ on_leave = False
+ if attendance_model:
+ present = bool(attendance_model.search_count([
+ ("employee_id", "=", employee.id),
+ ("check_in", ">=", today_start),
+ ("check_in", "<=", today_end),
+ ]))
+ if leave_model:
+ today = today_start.date()
+ on_leave = bool(leave_model.search_count([
+ ("employee_id", "=", employee.id),
+ ("request_date_from", "<=", today),
+ ("request_date_to", ">=", today),
+ ("state", "=", "validate"),
+ ]))
+ rows.append({
+ "id": employee.id,
+ "name": employee.name,
+ "job": employee.job_id.name if employee.job_id else "",
+ "department": employee.department_id.name if employee.department_id else "",
+ "image_url": "/web/image/hr.employee/%s/image_1920" % employee.id,
+ "status": _("On Leave") if on_leave else (_("Present") if present else _("Not Checked In")),
+ "tone": "leave" if on_leave else ("present" if present else "absent"),
+ "action": {
+ "type": "ir.actions.act_window",
+ "name": _("Employee"),
+ "res_model": "hr.employee",
+ "views": [[False, "form"]],
+ "res_id": employee.id,
+ "target": "current",
+ },
+ })
+ return rows
+
+ def _workforce_snapshot(self, date_from, date_to):
+ return [
+ {"label": _("New Joiners"), "value": self._safe_search_count("hr.employee", [("create_date", ">=", datetime.combine(date_from, time.min)), ("create_date", "<=", datetime.combine(date_to, time.max))]), "icon": "fa fa-user-plus"},
+ {"label": _("Open Leave Requests"), "value": self._safe_search_count("hr.leave", [("state", "in", ["confirm", "validate1"])]), "icon": "fa fa-calendar-check-o"},
+ {"label": _("Running Salary Advances"), "value": self._safe_search_count("hr.salary.advance", [("state", "=", "open")]), "icon": "fa fa-credit-card"},
+ {"label": _("Active Appraisal Cycles"), "value": self._safe_search_count("hr.notice.appraisal", [("state", "in", ["sent", "postponed"])]), "icon": "fa fa-star"},
+ ]
+
+
+ @http.route('/hrms_emp_dashboard/leave_requests', type='json', auth='user')
+ def leave_requests(self, limit=5, offset=0, **kwargs):
+ try:
+ employee = request.env.user.employee_id
+ if not employee:
+ return {'success': False}
+
+ domain = [('employee_id', '=', employee.id)]
+ total = request.env['hr.leave'].sudo().search_count(domain)
+
+ leaves = request.env['hr.leave'].sudo().search(
+ domain,
+ order='request_date_from desc',
+ limit=limit,
+ offset=offset,
+ )
+
+ state_selection = dict(leaves._fields['state'].selection) if leaves else {}
+
+ return {
+ 'success': True,
+ 'leaves': [{
+ 'id': leave.id,
+ 'name': leave.holiday_status_id.name or '',
+ 'date_from': fields.Datetime.to_string(leave.request_date_from),
+ 'date_to': fields.Datetime.to_string(leave.request_date_to),
+ 'duration': round(leave.number_of_days or 0, 1),
+ 'state': leave.state or 'draft',
+ 'state_label': state_selection.get(leave.state, leave.state or 'Draft'),
+ } for leave in leaves],
+ 'total': total,
+ }
+ except Exception as e:
+ return {'success': False, 'error': str(e)}
diff --git a/addons_extensions/hrms_emp_dashboard/models/__init__.py b/addons_extensions/hrms_emp_dashboard/models/__init__.py
new file mode 100644
index 000000000..0deb68c46
--- /dev/null
+++ b/addons_extensions/hrms_emp_dashboard/models/__init__.py
@@ -0,0 +1 @@
+from . import res_config_settings
diff --git a/addons_extensions/hrms_emp_dashboard/models/res_config_settings.py b/addons_extensions/hrms_emp_dashboard/models/res_config_settings.py
new file mode 100644
index 000000000..d8f14dcb3
--- /dev/null
+++ b/addons_extensions/hrms_emp_dashboard/models/res_config_settings.py
@@ -0,0 +1,26 @@
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = "res.config.settings"
+
+ hrms_dashboard_show_on_duty = fields.Boolean(
+ string="On-Duty Requests",
+ config_parameter="hrms_emp_dashboard.show_on_duty",
+ default=True,
+ )
+ hrms_dashboard_show_late_coming = fields.Boolean(
+ string="Late Coming Requests",
+ config_parameter="hrms_emp_dashboard.show_late_coming",
+ default=True,
+ )
+ hrms_dashboard_show_overtime = fields.Boolean(
+ string="Overtime Requests",
+ config_parameter="hrms_emp_dashboard.show_overtime",
+ default=True,
+ )
+ hrms_dashboard_show_shift_swap = fields.Boolean(
+ string="Shift Swap Requests",
+ config_parameter="hrms_emp_dashboard.show_shift_swap",
+ default=True,
+ )
diff --git a/addons_extensions/hrms_emp_dashboard/static/src/css/hrms_emp_dashboard.css b/addons_extensions/hrms_emp_dashboard/static/src/css/hrms_emp_dashboard.css
index b6e9b2d32..b28c2ac4f 100644
--- a/addons_extensions/hrms_emp_dashboard/static/src/css/hrms_emp_dashboard.css
+++ b/addons_extensions/hrms_emp_dashboard/static/src/css/hrms_emp_dashboard.css
@@ -2,9 +2,9 @@
height: calc(100vh - 84px);
overflow-y: auto;
overflow-x: hidden;
- padding: 20px;
- background: #f4f7fb;
- color: #111827;
+ padding: 18px;
+ background: #f5f8fd;
+ color: #071b4f;
}
.hrms-loading {
@@ -14,21 +14,264 @@
border-radius: 8px;
}
-.hrms-employee-card,
-.hrms-panel,
-.hrms-kpi {
- background: #fff;
- border: 1px solid #e5e7eb;
+.hrms-dashboard-tabs {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 14px;
+ padding: 6px;
+ border: 1px solid #dfe8f6;
border-radius: 8px;
- box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
+ background: #fff;
+ box-shadow: 0 10px 24px rgba(26, 58, 118, 0.05);
+ overflow-x: auto;
}
+.hrms-dashboard-tab {
+ min-height: 42px;
+ padding: 9px 14px;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ background: transparent;
+ color: #50658f;
+ font-weight: 800;
+ white-space: nowrap;
+}
+
+.hrms-dashboard-tab i { margin-right: 6px; }
+
+.hrms-dashboard-tab.active {
+ background: #0f766e;
+ border-color: #0f766e;
+ color: #fff;
+}
+
+.hrms-panel,
+.hrms-menu,
+.hrms-kpi {
+ background: #fff;
+ border: 1px solid #dfe8f6;
+ border-radius: 8px;
+ box-shadow: 0 14px 34px rgba(26, 58, 118, 0.07);
+}
+
+.hrms-service-dashboard {
+ display: grid;
+ gap: 16px;
+}
+
+.hrms-service-hero {
+ min-height: 156px;
+ padding: 22px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18px;
+ color: #fff;
+ box-shadow: 0 14px 34px rgba(26, 58, 118, 0.1);
+}
+
+.hrms-service-hero.manager { background: linear-gradient(135deg, #0f766e 0%, #1d4ed8 100%); }
+.hrms-service-hero.hr { background: linear-gradient(135deg, #1e3a8a 0%, #be123c 100%); }
+
+.hrms-service-hero span {
+ display: block;
+ color: rgba(255, 255, 255, 0.78);
+ font-size: 12px;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.hrms-service-hero h1 {
+ margin: 6px 0 4px;
+ color: #fff;
+ font-size: 28px;
+ font-weight: 800;
+}
+
+.hrms-service-hero p {
+ max-width: 720px;
+ margin: 0;
+ color: rgba(255, 255, 255, 0.88);
+}
+
+.hrms-service-kpis {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.hrms-service-kpi {
+ padding: 14px;
+ border: 1px solid #dfe8f6;
+ border-radius: 8px;
+ background: #fff;
+ box-shadow: 0 10px 24px rgba(26, 58, 118, 0.05);
+}
+
+.hrms-service-kpi span {
+ display: block;
+ color: #64748b;
+ font-size: 12px;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.hrms-service-kpi strong {
+ display: block;
+ margin-top: 8px;
+ color: #0f172a;
+ font-size: 28px;
+ line-height: 1;
+}
+
+.hrms-service-kpi.blue strong { color: #2563eb; }
+.hrms-service-kpi.green strong { color: #16a34a; }
+.hrms-service-kpi.amber strong { color: #d97706; }
+.hrms-service-kpi.violet strong { color: #7c3aed; }
+
+.hrms-service-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.35fr) minmax(340px, 0.65fr);
+ gap: 16px;
+ align-items: start;
+}
+
+.hrms-service-menu-row {
+ grid-template-columns: none;
+ margin-bottom: 0;
+}
+
+.hrms-approval-list,
+.hrms-team-list,
+.hrms-workforce-list {
+ display: grid;
+ gap: 8px;
+ max-height: 520px;
+ overflow-y: auto;
+ scrollbar-width: thin;
+}
+
+.hrms-approval-row {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 12px;
+ align-items: center;
+ padding: 10px;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ background: #f8fafc;
+}
+
+.hrms-approval-main {
+ min-width: 0;
+ border: 0;
+ background: transparent;
+ text-align: left;
+ color: #0f172a;
+}
+
+.hrms-approval-main strong,
+.hrms-team-row strong {
+ display: block;
+ color: #0f172a;
+ font-size: 13px;
+ font-weight: 800;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.hrms-approval-main small,
+.hrms-team-row small {
+ display: block;
+ margin-top: 2px;
+ color: #64748b;
+ font-size: 11px;
+}
+
+.hrms-approval-category {
+ display: block;
+ color: #2563eb;
+ font-size: 10px;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.hrms-approval-actions {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.hrms-team-row {
+ display: grid;
+ grid-template-columns: 38px minmax(0, 1fr) auto;
+ gap: 10px;
+ align-items: center;
+ padding: 9px;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ background: #fff;
+ text-align: left;
+}
+
+.hrms-team-row img {
+ width: 38px;
+ height: 38px;
+ border-radius: 8px;
+ object-fit: cover;
+ background: #e2e8f0;
+}
+
+.hrms-team-row em {
+ padding: 3px 8px;
+ border-radius: 999px;
+ font-size: 10px;
+ font-style: normal;
+ font-weight: 800;
+ white-space: nowrap;
+}
+
+.hrms-team-row em.present { background: #dcfce7; color: #15803d; }
+.hrms-team-row em.absent { background: #f1f5f9; color: #64748b; }
+.hrms-team-row em.leave { background: #ffedd5; color: #c2410c; }
+
+.hrms-workforce-row {
+ display: grid;
+ grid-template-columns: 36px minmax(0, 1fr) auto;
+ gap: 10px;
+ align-items: center;
+ padding: 12px;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ background: #f8fafc;
+}
+
+.hrms-workforce-row i {
+ width: 36px;
+ height: 36px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #eff6ff;
+ color: #2563eb;
+}
+
+.hrms-workforce-row span { color: #334155; font-weight: 800; }
+.hrms-workforce-row strong { color: #0f172a; font-size: 22px; }
+
+/* ── Employee Card ── */
.hrms-employee-card {
display: flex;
justify-content: space-between;
gap: 20px;
padding: 20px;
margin-bottom: 14px;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
background: linear-gradient(135deg, #172554 0%, #0f766e 100%);
color: #fff;
}
@@ -68,15 +311,8 @@
color: #e0f2fe;
}
-.hrms-employee-grid i {
- width: 18px;
- color: #ffffff;
-}
-
-.hrms-employee-grid b {
- color: #ffffff;
- font-weight: 700;
-}
+.hrms-employee-grid i { width: 18px; color: #fff; }
+.hrms-employee-grid b { color: #fff; font-weight: 700; }
.hrms-employee-actions {
display: flex;
@@ -103,34 +339,11 @@
font-weight: 700;
}
-.hrms-icon-button i {
- display: block;
- font-size: 22px;
- margin-bottom: 4px;
-}
-
-/*.hrms-icon-button.primary {*/
-/* background: #16a34a;*/
-/* border-color: #22c55e;*/
-/*}*/
-
-.hrms-icon-button.checkin {
- background: #16a34a;
- color: #fff;
-}
-
-.hrms-icon-button.checkout {
- background: #dc2626;
- color: #fff;
-}
-
-.hrms-icon-button.checkout:hover {
- background: #b91c1c;
-}
-
-.hrms-icon-button.checkin:hover {
- background: #15803d;
-}
+.hrms-icon-button i { display: block; font-size: 22px; margin-bottom: 4px; }
+.hrms-icon-button.checkin { background: #16a34a; color: #fff; }
+.hrms-icon-button.checkout { background: #dc2626; color: #fff; }
+.hrms-icon-button.checkout:hover { background: #b91c1c; }
+.hrms-icon-button.checkin:hover { background: #15803d; }
.hrms-filter-box {
width: 100%;
@@ -149,6 +362,14 @@
text-transform: uppercase;
}
+.hrms-filter-box .form-emp-card-select {
+ color: white;
+}
+
+.hrms-filter-box .form-emp-card-select option {
+ color: black; /* Keeps dropdown items readable on most browsers */
+}
+
.hrms-custom-dates {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -156,17 +377,164 @@
margin-top: 8px;
}
-.hrms-kpi-row {
+/* ── Menu Row ── */
+.hrms-menus-row {
display: grid;
- grid-template-columns: repeat(5, minmax(140px, 1fr));
+ grid-auto-flow: column;
+ grid-auto-columns: minmax(174px, 1fr);
+ grid-template-columns: repeat(8, minmax(174px, 1fr));
gap: 12px;
- margin-bottom: 14px;
+ margin-bottom: 16px;
+ overflow-x: auto;
+ padding-bottom: 4px;
+ scrollbar-width: thin;
}
-.hrms-kpi {
- padding: 14px;
+.hrms-menu {
+ min-height: 112px;
+ padding: 12px;
+ border: 1px solid #dfe8f6;
+ display: grid;
+ grid-template-columns: 34px minmax(0, 1fr) auto;
+ grid-template-rows: auto 1fr;
+ gap: 10px;
+ align-items: start;
+ text-align: left;
+ color: #071b4f;
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+ transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}
+.hrms-menu:hover {
+ border-color: #b9cdf0;
+ box-shadow: 0 18px 36px rgba(26, 58, 118, 0.12);
+ transform: translateY(-2px);
+}
+
+.hrms-menu.green { background: linear-gradient(135deg, #ffffff 0%, #ecfdf5 100%); }
+.hrms-menu.blue { background: linear-gradient(135deg, #ffffff 0%, #eff6ff 100%); }
+.hrms-menu.amber { background: linear-gradient(135deg, #ffffff 0%, #fff7ed 100%); }
+.hrms-menu.cyan { background: linear-gradient(135deg, #ffffff 0%, #ecfeff 100%); }
+.hrms-menu.red { background: linear-gradient(135deg, #ffffff 0%, #fff1f2 100%); }
+.hrms-menu.violet { background: linear-gradient(135deg, #ffffff 0%, #f5f3ff 100%); }
+.hrms-menu.slate { background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%); }
+.hrms-menu.rose { background: linear-gradient(135deg, #ffffff 0%, #fdf2f8 100%); }
+
+.hrms-menu-glow {
+ position: absolute;
+ right: -28px;
+ top: -34px;
+ width: 94px;
+ height: 94px;
+ border-radius: 50%;
+ background: rgba(37, 99, 235, 0.08);
+ pointer-events: none;
+}
+
+.hrms-menu-icon {
+ width: 34px;
+ height: 34px;
+ border-radius: 8px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ background: #2563eb;
+ color: #fff;
+}
+
+.hrms-menu.green .hrms-menu-icon { background: #10b981; }
+.hrms-menu.blue .hrms-menu-icon { background: #3b82f6; }
+.hrms-menu.amber .hrms-menu-icon { background: #f59e0b; }
+.hrms-menu.cyan .hrms-menu-icon { background: #0891b2; }
+.hrms-menu.red .hrms-menu-icon { background: #e11d48; }
+.hrms-menu.violet .hrms-menu-icon { background: #8b5cf6; }
+.hrms-menu.slate .hrms-menu-icon { background: #1e3a8a; }
+.hrms-menu.rose .hrms-menu-icon { background: #db2777; }
+
+.hrms-menu-content {
+ grid-column: 1 / 4;
+ min-width: 0;
+ position: relative;
+}
+
+.hrms-menu-title {
+ display: block;
+ color: #071b4f;
+ font-size: 14px;
+ font-weight: 800;
+}
+
+.hrms-menu-content small,
+.hrms-menu-count small {
+ display: block;
+ margin-top: 3px;
+ color: #50658f;
+ font-size: 11px;
+ font-weight: 700;
+ line-height: 1.25;
+}
+
+.hrms-menu-count {
+ min-width: 54px;
+ text-align: right;
+ position: relative;
+ z-index: 1;
+}
+
+.hrms-menu-count strong {
+ display: block;
+ color: #071b4f;
+ font-size: 22px;
+ line-height: 1;
+}
+
+.hrms-menu-badge {
+ max-width: 96px;
+ padding: 5px 8px;
+ border-radius: 999px;
+ background: rgba(37, 99, 235, 0.08);
+ color: #1d4ed8;
+ font-size: 11px;
+ font-weight: 800;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.hrms-menu-arrow { color: #2563eb; }
+
+.hrms-menu-plus {
+ border: 1px solid #cbd5e1;
+ background: #fff;
+ border-radius: 6px;
+ color: #64748b;
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ display: none !important;
+}
+
+.hrms-menu-create:hover {
+ background: #eff6ff;
+ transform: scale(1.04);
+}
+
+/* ── KPI Row: 5 columns full width ── */
+.hrms-kpi-row {
+ display: grid;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: 10px;
+ margin-bottom: 16px;
+}
+
+.hrms-kpi { padding: 12px; }
+
.hrms-kpi span {
display: block;
color: #64748b;
@@ -183,33 +551,56 @@
}
.hrms-kpi.present strong { color: #16a34a; }
-.hrms-kpi.absent strong { color: #dc2626; }
-.hrms-kpi.break strong { color: #2563eb; }
+.hrms-kpi.absent strong { color: #dc2626; }
+.hrms-kpi.break strong { color: #2563eb; }
.hrms-kpi.leave strong,
.hrms-kpi.holiday strong { color: #f97316; }
.hrms-kpi.remaining.positive strong { color: #16a34a; }
.hrms-kpi.remaining.negative strong { color: #dc2626; }
+/* ── Middle Grid: Calendar + Right Stack ── */
.hrms-grid {
display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 14px;
+ grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
+ gap: 16px;
+ align-items: start;
+ margin-bottom: 16px;
}
-.hrms-panel {
- padding: 16px;
+.hrms-employee-columns {
+ display: grid;
+ grid-template-columns: minmax(0, 1.6fr) minmax(340px, 1fr);
+ gap: 16px;
+ align-items: start;
+}
+
+.hrms-employee-main-column,
+.hrms-employee-side-column {
+ display: grid;
+ gap: 16px;
+ align-content: start;
min-width: 0;
}
-.hrms-panel.wide {
- grid-column: span 2;
+/* Right stack: Leave Balance on top, Public Holidays below */
+.hrms-right-stack {
+ display: grid;
+ grid-template-rows: auto auto;
+ gap: 16px;
+ align-content: start;
+}
+
+/* ── Panels ── */
+.hrms-panel {
+ padding: 14px;
+ min-width: 0;
}
.hrms-panel h2 {
- margin: 0 0 12px;
+ margin: 0;
font-size: 16px;
font-weight: 800;
- color: #0f172a;
+ color: #071b4f;
}
.hrms-panel-header {
@@ -220,6 +611,16 @@
margin-bottom: 10px;
}
+.hrms-panel-count {
+ padding: 4px 8px;
+ border-radius: 999px;
+ background: #f1f5f9;
+ color: #475569;
+ font-size: 11px;
+ font-weight: 800;
+}
+
+/* ── Calendar ── */
.hrms-month-controls {
display: flex;
align-items: center;
@@ -227,18 +628,16 @@
flex-wrap: wrap;
}
-.hrms-calendar-select {
- width: 112px;
-}
+.hrms-calendar-select { width: 112px; }
.hrms-calendar {
display: grid;
grid-template-columns: repeat(7, minmax(130px, 1fr));
gap: 0;
overflow-x: auto;
- border: 1px solid #dbe3ed;
+ border: 1px solid #dbe8f7;
border-radius: 8px;
- background: #dbe3ed;
+ background: #dbe8f7;
}
.hrms-calendar-weekdays {
@@ -246,24 +645,22 @@
grid-template-columns: repeat(7, minmax(130px, 1fr));
gap: 0;
overflow-x: auto;
- border: 1px solid #dbe3ed;
+ border: 1px solid #dbe8f7;
border-bottom: 0;
border-radius: 8px 8px 0 0;
- background: #f8fafc;
+ background: #f7faff;
}
.hrms-calendar-weekdays span {
padding: 10px 8px;
- color: #0f172a;
+ color: #071b4f;
font-size: 13px;
font-weight: 800;
text-align: center;
border-right: 1px solid #dbe3ed;
}
-.hrms-calendar-weekdays span:last-child {
- border-right: 0;
-}
+.hrms-calendar-weekdays span:last-child { border-right: 0; }
.hrms-calendar.yearly {
grid-template-columns: repeat(4, minmax(160px, 1fr));
@@ -276,14 +673,24 @@
min-height: 132px;
padding: 10px;
border: 0;
- border-right: 1px solid #dbe3ed;
- border-bottom: 1px solid #dbe3ed;
+ border-right: 1px solid #dbe8f7;
+ border-bottom: 1px solid #dbe8f7;
border-radius: 0;
- background: #fff;
+ background: #fbfdff;
position: relative;
overflow: hidden;
+ text-align: left;
+ cursor: pointer;
}
+.hrms-day:hover:not(:disabled) {
+ z-index: 1;
+ outline: 2px solid #3b82f6;
+ outline-offset: -2px;
+}
+
+.hrms-day:disabled { cursor: default; }
+
.hrms-calendar.yearly .hrms-day {
min-height: 126px;
border: 1px solid #e5e7eb;
@@ -291,10 +698,7 @@
}
.hrms-day span,
-.hrms-day small {
- color: #64748b;
- font-size: 12px;
-}
+.hrms-day small { color: #64748b; font-size: 12px; }
.hrms-day-top {
display: flex;
@@ -304,25 +708,32 @@
min-height: 24px;
}
-.hrms-day-top strong {
- color: #0f172a;
- font-size: 13px;
- line-height: 1.2;
-}
+.hrms-day-top strong { color: #071b4f; font-size: 13px; line-height: 1.2; }
+.hrms-day-top strong span { display: inline; margin-left: 4px; font-size: 11px; font-weight: 700; }
-.hrms-day-top strong span {
- display: inline;
- margin-left: 4px;
- font-size: 11px;
- font-weight: 700;
-}
+/*.hrms-day-badge {*/
+/* max-width: 76px;*/
+/* padding: 2px 6px;*/
+/* border-radius: 6px;*/
+/* background: #eef2ff;*/
+/* color: #4338ca;*/
+/* font-size: 10px;*/
+/* font-weight: 800;*/
+/* overflow: hidden;*/
+/* text-overflow: ellipsis;*/
+/* white-space: nowrap;*/
+/*}*/
-.hrms-day-badge {
- max-width: 76px;
- padding: 2px 6px;
- border-radius: 6px;
- background: #eef2ff;
- color: #4338ca;
+.hrms-day-signals { display: grid; gap: 4px; margin-top: 8px; }
+
+.hrms-day-signal {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ min-height: 20px;
+ max-width: 100%;
+ padding: 3px 6px;
+ border-radius: 7px;
font-size: 10px;
font-weight: 800;
overflow: hidden;
@@ -330,25 +741,24 @@
white-space: nowrap;
}
-.hrms-day-balance {
- height: 22px;
- line-height: 22px;
- margin: 10px 0 8px;
- border-radius: 999px;
- text-align: center;
- font-size: 12px;
- font-weight: 800;
-}
+.hrms-day-signal i { flex: 0 0 auto; font-size: 10px; }
+.hrms-day-signal.event { background: #dbeafe; color: #1d4ed8; }
+.hrms-day-signal.holiday { background: #fef3c7; color: #b45309; }
+.hrms-day-signal.leave { background: #ffedd5; color: #c2410c; }
+.hrms-day-signal.more { background: #eef2ff; color: #4338ca; }
-.hrms-day-balance.positive {
- background: #dcfce7;
- color: #16a34a;
-}
+/*.hrms-day-balance {*/
+/* height: 22px;*/
+/* line-height: 22px;*/
+/* margin: 10px 0 8px;*/
+/* border-radius: 999px;*/
+/* text-align: center;*/
+/* font-size: 12px;*/
+/* font-weight: 800;*/
+/*}*/
-.hrms-day-balance.negative {
- background: #fee2e2;
- color: #dc2626;
-}
+/*.hrms-day-balance.positive { background: #dcfce7; color: #16a34a; }*/
+/*.hrms-day-balance.negative { background: #fee2e2; color: #dc2626; }*/
.hrms-day-message {
min-height: 76px;
@@ -362,249 +772,325 @@
text-align: center;
}
-.hrms-day-message-icon {
- color: inherit;
- font-size: 16px;
-}
+.hrms-day-message-icon { color: inherit; font-size: 16px; }
+.hrms-day-message strong { color: inherit; font-size: 13px; line-height: 1.25; overflow-wrap: anywhere; }
-.hrms-day-message strong {
- color: inherit;
- font-size: 13px;
- line-height: 1.25;
- overflow-wrap: anywhere;
-}
+/*.hrms-day-metrics {*/
+/* display: grid;*/
+/* grid-template-columns: repeat(3, minmax(0, 1fr));*/
+/* gap: 6px;*/
+/*}*/
-.hrms-day-metrics {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 6px;
-}
+/*.hrms-day-metrics div { min-width: 0; }*/
+/*.hrms-day-metrics span { display: block; color: #64748b; font-size: 10px; font-weight: 800; }*/
+/*.hrms-day-metrics strong { display: block; margin-top: 3px; color: #0f172a; font-size: 13px; line-height: 1.15; word-break: keep-all; }*/
-.hrms-day-metrics div {
- min-width: 0;
-}
-
-.hrms-day-metrics span {
- display: block;
- color: #64748b;
- font-size: 10px;
- font-weight: 800;
-}
-
-.hrms-day-metrics strong {
- display: block;
- margin-top: 3px;
- color: #0f172a;
- font-size: 13px;
- line-height: 1.15;
- word-break: keep-all;
-}
-
-.hrms-calendar-month-stats {
- display: grid;
- gap: 5px;
- margin-top: 10px;
-}
-
-.hrms-calendar-month-stats small {
- display: block;
- font-size: 12px;
-}
+.hrms-calendar-month-stats { display: grid; gap: 5px; margin-top: 10px; }
+.hrms-calendar-month-stats small { display: block; font-size: 12px; }
.hrms-day.present { box-shadow: inset 0 3px 0 #22c55e; }
-.hrms-day.absent { box-shadow: inset 0 3px 0 #ef4444; }
-.hrms-day.leave {
- background: #fff5f5;
- box-shadow: inset 0 3px 0 #ef4444;
-}
-.hrms-day.leave .hrms-day-message {
- background: #fee2e2;
- color: #b91c1c;
- border: 1px solid #fecaca;
-}
-.hrms-day.holiday {
- background: #f8fafc;
- box-shadow: inset 0 3px 0 #94a3b8;
-}
-.hrms-day.holiday .hrms-day-message {
- background: #e5e7eb;
- color: #334155;
- border: 1px solid #cbd5e1;
-}
-.hrms-day.weekend {
- background: #f8fafc;
- box-shadow: none;
-}
-.hrms-day.weekend .hrms-day-message {
- min-height: 58px;
- background: transparent;
- color: #94a3b8;
- border: 1px dashed #e2e8f0;
-}
+.hrms-day.absent { box-shadow: inset 0 3px 0 #fecaca; }
+.hrms-day.leave { background: #fff5f5; box-shadow: inset 0 3px 0 #ef4444; }
+.hrms-day.leave .hrms-day-message { background: #fee2e2; color: #b91c1c; border: 1px solid #fecaca; }
+.hrms-day.holiday { background: #fffaf0; box-shadow: inset 0 3px 0 #f59e0b; }
+.hrms-day.holiday .hrms-day-message { background: #e5e7eb; color: #334155; border: 1px solid #cbd5e1; }
+.hrms-day.weekend { background: #f4f8ff; box-shadow: none; }
+.hrms-day.weekend .hrms-day-message { min-height: 58px; background: transparent; color: #94a3b8; border: 1px dashed #e2e8f0; }
.hrms-day.future { opacity: 0.55; }
-.hrms-day.empty {
- background: #f8fafc;
- opacity: 0.45;
+.hrms-day.empty { background: #f8fafc; opacity: 0.45; }
+
+/* ── Leave Balance: Simple bars, fixed height, scroll ── */
+.hrms-leave-scroll {
+ max-height: 300px;
+ overflow-y: auto;
+ scrollbar-width: thin;
}
-.hrms-leave-summary {
+.hrms-leave-scroll::-webkit-scrollbar { width: 5px; }
+.hrms-leave-scroll::-webkit-scrollbar-track { background: transparent; }
+.hrms-leave-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
+
+
+.hrms-leave-item:last-child { margin-bottom: 0; }
+
+/* ── Leave Balance: 2-column grid cards ── */
+.hrms-balance-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 8px;
+}
+
+.hrms-balance-card {
display: flex;
- flex-wrap: wrap;
+ align-items: center;
gap: 10px;
- margin-bottom: 12px;
-}
-
-.hrms-leave-tile {
- min-width: 180px;
- flex: 1 1 180px; /* grow, shrink, basis */
- max-width: 250px;
-
- padding: 10px;
+ padding: 10px 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #f8fafc;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
-.hrms-leave-tile strong {
- display: block;
- color: #0f172a;
- font-size: 13px;
- margin-bottom: 8px;
+.hrms-balance-card:hover {
+ border-color: #cbd5e1;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
-.hrms-leave-tile span {
- display: inline-block;
- min-width: 64px;
- margin-right: 10px;
- color: #64748b;
- font-size: 11px;
-}
-
-.hrms-leave-tile b {
- display: block;
- color: #0f172a;
- font-size: 18px;
-}
-
-.hrms-leave-graph {
- display: grid;
- gap: 12px;
-}
-
-.hrms-leave-graph-row {
- display: grid;
- grid-template-columns: minmax(140px, 190px) minmax(180px, 1fr);
- gap: 10px 14px;
- align-items: center;
-}
-
-.hrms-leave-graph-label strong {
- display: block;
- color: #0f172a;
- font-size: 13px;
-}
-
-.hrms-leave-graph-label span {
- color: #64748b;
- font-size: 11px;
-}
-
-.hrms-leave-bar {
- display: flex;
- width: 100%;
- height: 18px;
- overflow: hidden;
+.hrms-balance-card-icon {
+ width: 34px;
+ height: 34px;
border-radius: 8px;
- background: #e5e7eb;
+ background: #eff6ff;
+ color: #2563eb;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ flex-shrink: 0;
}
-.hrms-leave-bar span {
+.hrms-balance-card-info {
min-width: 0;
}
-.hrms-leave-bar-remaining {
- background: #16a34a;
+.hrms-balance-card-info strong {
+ display: block;
+ color: #0f172a;
+ font-size: 12px;
+ font-weight: 700;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.hrms-leave-bar-taken {
- background: #2563eb;
+.hrms-balance-card-days {
+ display: block;
+ color: #16a34a;
+ font-size: 16px;
+ font-weight: 800;
+ margin-top: 1px;
}
-.hrms-leave-bar-planned {
- background: #f59e0b;
-}
-
-.hrms-leave-legend {
- grid-column: 2;
+/* ── Leave Divider ── */
+.hrms-leave-divider {
display: flex;
- flex-wrap: wrap;
- gap: 8px 14px;
+ align-items: center;
+ gap: 10px;
+ margin: 14px 0 10px;
color: #64748b;
font-size: 11px;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
}
-.hrms-leave-legend span {
- display: inline-flex;
+.hrms-leave-divider::before,
+.hrms-leave-divider::after {
+ content: '';
+ flex: 1;
+ height: 1px;
+ background: #e5e7eb;
+}
+
+/* ── Leave Requests ── */
+.hrms-leave-empty-req {
+ text-align: center;
+ padding: 12px 0;
+}
+
+.hrms-leave-req-row {
+ display: flex;
align-items: center;
- gap: 5px;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 8px 10px;
+ border: 1px solid #f1f5f9;
+ border-radius: 8px;
+ background: #fafbfc;
+ margin-bottom: 6px;
+ transition: border-color 0.15s ease;
}
-.hrms-leave-legend i {
- width: 9px;
- height: 9px;
- border-radius: 2px;
+.hrms-leave-req-row:hover {
+ border-color: #e2e8f0;
}
-.hrms-leave-legend i.remaining {
- background: #16a34a;
+.hrms-leave-req-info {
+ min-width: 0;
+ flex: 1;
}
-.hrms-leave-legend i.taken {
- background: #2563eb;
+.hrms-leave-req-info strong {
+ display: block;
+ color: #0f172a;
+ font-size: 12px;
+ font-weight: 700;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.hrms-leave-legend i.planned {
- background: #f59e0b;
+.hrms-leave-req-dates {
+ display: block;
+ color: #94a3b8;
+ font-size: 11px;
+ margin-top: 2px;
}
-.hrms-leave-legend b {
+.hrms-leave-req-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+}
+
+.hrms-leave-req-duration {
+ color: #64748b;
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.hrms-leave-req-state {
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 10px;
+ font-weight: 800;
+ text-transform: capitalize;
+ white-space: nowrap;
+}
+
+.hrms-leave-req-state.draft { background: #f1f5f9; color: #64748b; }
+.hrms-leave-req-state.confirm { background: #fff7ed; color: #c2410c; }
+.hrms-leave-req-state.validate1 { background: #eff6ff; color: #2563eb; }
+.hrms-leave-req-state.validate { background: #dcfce7; color: #15803d; }
+.hrms-leave-req-state.refuse { background: #fef2f2; color: #dc2626; }
+.hrms-leave-req-state.cancel { background: #f1f5f9; color: #94a3b8; }
+
+/* ── Load More Button ── */
+.hrms-load-more-btn {
+ display: block;
+ width: 100%;
+ padding: 8px;
+ margin-top: 8px;
+ border: 1px dashed #cbd5e1;
+ border-radius: 8px;
+ background: #fff;
+ color: #475569;
+ font-size: 12px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.hrms-load-more-btn:hover {
+ background: #f8fafc;
+ border-color: #94a3b8;
color: #0f172a;
}
-.hrms-two-charts {
- display: grid;
- grid-template-columns: 1.3fr 0.7fr;
- gap: 12px;
+
+/* ── Public Holidays: Fixed height, scroll ── */
+.hrms-holidays-scroll {
+ max-height: 300px;
+ overflow-y: auto;
+ scrollbar-width: thin;
}
-.hrms-list {
- display: grid;
- gap: 10px;
-}
+.hrms-holidays-scroll::-webkit-scrollbar { width: 5px; }
+.hrms-holidays-scroll::-webkit-scrollbar-track { background: transparent; }
+.hrms-holidays-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
+
+.btn-group .btn { border-radius: 0 !important; }
+.btn-group .btn:first-child { border-radius: 4px 0 0 4px !important; }
+.btn-group .btn:last-child { border-radius: 0 4px 4px 0 !important; }
+
+/* ── List rows ── */
+.hrms-list { display: grid; gap: 10px; }
.hrms-list-row {
display: flex;
gap: 10px;
padding: 10px;
background: #f8fafc;
+ border: 1px solid #e5e7eb;
border-radius: 8px;
}
-.hrms-list-row i {
- color: #f97316;
- margin-top: 3px;
+.hrms-list-row i { color: #f97316; margin-top: 3px; }
+.hrms-list-row span,
+.hrms-muted { display: block; color: #64748b; font-size: 12px; }
+
+/* ── Bottom Row: Expenses + Equipment ── */
+.hrms-bottom-row {
+ display: grid;
+ grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
+ gap: 16px;
+ align-items: start;
}
-.hrms-list-row span,
-.hrms-muted {
- display: block;
- color: #64748b;
- font-size: 12px;
+/* ── Expenses ── */
+.hrms-expense-body {
+ max-height: 380px;
+ overflow-y: auto;
+ scrollbar-width: thin;
}
+.hrms-expense-body::-webkit-scrollbar { width: 5px; }
+.hrms-expense-body::-webkit-scrollbar-track { background: transparent; }
+.hrms-expense-body::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
+
+.hrms-two-charts {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+}
+
+#hrmsExpenseChart,
+#hrmsExpenseStateChart { min-height: 340px; }
+
+.expense-empty-state {
+ min-height: 240px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ border: 1px dashed #d9d9d9;
+ border-radius: 12px;
+ background: #fafafa;
+ padding: 28px;
+}
+
+.expense-empty-state .empty-icon {
+ width: 90px;
+ height: 90px;
+ border-radius: 50%;
+ background: #f3f4f6;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 42px;
+ color: #9ca3af;
+ margin-bottom: 20px;
+}
+
+.expense-empty-state h3 { font-size: 24px; font-weight: 600; color: #374151; margin-bottom: 12px; }
+.expense-empty-state p { width: 500px; max-width: 95%; color: #6b7280; line-height: 1.7; margin-bottom: 25px; }
+
+/* ── Equipment: Fixed height, scroll ── */
+.hrms-equipment-scroll {
+ max-height: 340px;
+ overflow-y: auto;
+ scrollbar-width: thin;
+}
+
+.hrms-equipment-scroll::-webkit-scrollbar { width: 5px; }
+.hrms-equipment-scroll::-webkit-scrollbar-track { background: transparent; }
+.hrms-equipment-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
+
.hrms-equipment-grid {
display: grid;
- grid-template-columns: repeat(4, minmax(160px, 1fr));
- gap: 10px;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 8px;
}
.hrms-equipment {
@@ -616,128 +1102,872 @@
color: #0f172a;
}
-.hrms-equipment i {
- color: #2563eb;
- font-size: 20px;
-}
-
+.hrms-equipment i { color: #2563eb; font-size: 20px; }
.hrms-equipment strong,
.hrms-equipment span,
-.hrms-equipment small {
- display: block;
- margin-top: 4px;
-}
-
+.hrms-equipment small { display: block; margin-top: 4px; }
.hrms-equipment span,
-.hrms-equipment small {
- color: #64748b;
+.hrms-equipment small { color: #64748b; }
+
+/* ── Day Modal ── */
+.hrms-day-modal-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 1050;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+ background: rgba(15, 23, 42, 0.4);
+ backdrop-filter: blur(2px);
}
+.hrms-day-modal {
+ width: min(520px, 100%);
+ border-radius: 12px;
+ background: #fff;
+ box-shadow: 0 20px 60px rgba(15, 23, 42, 0.2);
+ overflow: hidden;
+}
+
+.hrms-modal-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 16px 18px 12px;
+ background: #f8fafc;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.hrms-modal-title-group {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.hrms-modal-title-group h2 {
+ margin: 0;
+ color: #0f172a;
+ font-size: 18px;
+ font-weight: 800;
+}
+
+.hrms-day-modal header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 20px 20px 12px;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.hrms-day-modal h2 { margin: 2px 0 0; color: #0f172a; font-size: 20px; font-weight: 800; }
+.hrms-modal-kicker { color: #64748b; font-size: 11px; font-weight: 800; text-transform: uppercase; }
+
+.hrms-modal-close {
+ width: 30px;
+ height: 30px;
+ border: 1px solid #e2e8f0;
+ border-radius: 8px;
+ background: #fff;
+ color: #64748b;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 13px;
+ flex-shrink: 0;
+ transition: all 0.15s ease;
+}
+
+.hrms-modal-close:hover {
+ background: #f1f5f9;
+ color: #0f172a;
+}
+
+.hrms-day-modal-status {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 14px 20px 0;
+ color: #334155;
+}
+
+
+/* Modal body */
+.hrms-modal-body {
+ padding: 14px 18px;
+}
+
+/* Detail chips - compact flex layout */
+.hrms-detail-chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.hrms-detail-chip {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ background: #f8fafc;
+ border: 1px solid #f1f5f9;
+ border-radius: 8px;
+ min-width: 0;
+ transition: border-color 0.15s ease;
+}
+
+.hrms-detail-chip:hover {
+ border-color: #e2e8f0;
+}
+
+.hrms-detail-chip span {
+ color: #94a3b8;
+ font-size: 10px;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.4px;
+ white-space: nowrap;
+}
+
+.hrms-detail-chip strong {
+ color: #0f172a;
+ font-size: 14px;
+ font-weight: 700;
+ white-space: nowrap;
+}
+
+.hrms-modal-event {
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid #f1f5f9;
+}
+
+.hrms-modal-events-label {
+ display: block;
+ font-size: 10px;
+ font-weight: 800;
+ color: #94a3b8;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 8px;
+}
+
+.hrms-modal-events-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.hrms-event-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 5px 10px;
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+
+.hrms-event-tag i {
+ font-size: 11px;
+}
+
+.hrms-event-tag.event { background: #dbeafe; color: #1d4ed8; }
+.hrms-event-tag.holiday { background: #fef3c7; color: #92400e; }
+.hrms-event-tag.leave { background: #ffedd5; color: #c2410c; }
+.hrms-event-tag.more { background: #eef2ff; color: #4338ca; }
+
+/* Modal footer */
+.hrms-day-modal footer {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 6px;
+ padding: 10px 18px 14px;
+ border-top: 1px solid #f1f5f9;
+ background: #fafbfc;
+}
+
+
+.hrms-status-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 3px 10px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 700;
+ text-transform: capitalize;
+ background: #f1f5f9;
+ color: #475569;
+}
+
+.hrms-status-pill::before {
+ content: '';
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: #94a3b8;
+}
+
+.hrms-status-pill.present { background: #dcfce7; color: #15803d; }
+.hrms-status-pill.present::before { background: #22c55e; }
+.hrms-status-pill.absent { background: #fee2e2; color: #dc2626; }
+.hrms-status-pill.absent::before { background: #ef4444; }
+.hrms-status-pill.leave { background: #ffedd5; color: #c2410c; }
+.hrms-status-pill.leave::before { background: #f97316; }
+.hrms-status-pill.holiday { background: #fef3c7; color: #92400e; }
+.hrms-status-pill.holiday::before { background: #f59e0b; }
+.hrms-status-pill.weekend { background: #f1f5f9; color: #64748b; }
+.hrms-status-pill.weekend::before { background: #94a3b8; }
+.hrms-status-pill.future { background: #e0f2fe; color: #0369a1; }
+.hrms-status-pill.future::before { background: #38bdf8; }
+
+
+.hrms-status-dot { width: 10px; height: 10px; border-radius: 50%; background: #94a3b8; }
+.hrms-status-dot.present { background: #22c55e; }
+.hrms-status-dot.absent { background: #ef4444; }
+.hrms-status-dot.leave { background: #f97316; }
+.hrms-status-dot.holiday { background: #64748b; }
+.hrms-status-dot.weekend { background: #94a3b8; }
+.hrms-status-dot.future { background: #38bdf8; }
+
+.hrms-day-detail-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+ padding: 16px 20px 20px;
+}
+
+.hrms-day-detail {
+ min-height: 72px;
+ padding: 12px;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ background: #f8fafc;
+}
+
+.hrms-day-detail span { display: block; color: #64748b; font-size: 11px; font-weight: 800; text-transform: uppercase; }
+.hrms-day-detail strong { display: block; margin-top: 5px; color: #0f172a; font-size: 13px; line-height: 1.35; overflow-wrap: anywhere; }
+
+.hrms-day-modal footer {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 8px;
+ padding: 14px 20px 20px;
+ border-top: 1px solid #e5e7eb;
+}
+
+/* ── Responsive ── */
@media (max-width: 1200px) {
.hrms-employee-card,
- .hrms-employee-main {
- flex-direction: column;
- }
+ .hrms-employee-main { flex-direction: column; }
- .hrms-employee-grid,
- .hrms-grid,
- .hrms-leave-graph-row,
- .hrms-two-charts {
- grid-template-columns: 1fr;
- }
+ .hrms-employee-grid { grid-template-columns: 1fr; }
- .hrms-leave-legend {
- grid-column: 1;
- }
+ .hrms-kpi-row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
- .hrms-panel.wide {
- grid-column: span 1;
- }
+ .hrms-grid { grid-template-columns: 1fr; }
- .hrms-kpi-row,
- .hrms-equipment-grid,
- .hrms-calendar.yearly {
- grid-template-columns: repeat(2, minmax(140px, 1fr));
- }
+ .hrms-employee-columns { grid-template-columns: 1fr; }
+
+ .hrms-bottom-row { grid-template-columns: 1fr; }
+
+ .hrms-equipment-grid { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
}
@media (max-width: 700px) {
- .hrms-kpi-row,
+ .hrms-kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+
.hrms-equipment-grid,
.hrms-calendar.yearly,
- .hrms-leave-summary,
- .hrms-custom-dates {
- grid-template-columns: 1fr;
- }
+ .hrms-custom-dates { grid-template-columns: 1fr; }
.hrms-calendar,
- .hrms-calendar-weekdays {
- grid-template-columns: repeat(7, minmax(118px, 1fr));
- }
+ .hrms-calendar-weekdays { grid-template-columns: repeat(7, minmax(118px, 1fr)); }
.hrms-panel-header,
.hrms-panel-actions,
- .hrms-action-buttons {
- align-items: stretch;
- flex-direction: column;
- }
+ .hrms-action-buttons { align-items: stretch; flex-direction: column; }
+
+ .hrms-day-detail-grid { grid-template-columns: 1fr; }
}
-.expense-empty-state{
- min-height:420px;
+/* ── Menu Plus Button: Top Right ── */
+.hrms-menu-plus-btn {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 26px;
+ height: 26px;
+ border: 1px solid #e2e8f0;
+ border-radius: 6px;
+ background: #fff;
+ color: #64748b;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11px;
+ z-index: 2;
+ transition: all 0.15s ease;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
+}
+
+.hrms-menu-plus-btn:hover {
+ background: #2563eb;
+ color: #fff;
+ border-color: #2563eb;
+ transform: scale(1.08);
+}
+
+/* Remove old plus style if exists */
+.hrms-menu-plus {
+ display: none !important;
+}
+
+/* ── Calendar Day: Clean professional look ── */
+/* ── Calendar Day ── */
+.hrms-day {
+ min-height: 110px;
+ padding: 10px;
+ border: 0;
+ border-right: 1px solid #dbe8f7;
+ border-bottom: 1px solid #dbe8f7;
+ border-radius: 0;
+ background: #fbfdff;
+ position: relative;
+ overflow: hidden;
+ text-align: left;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.hrms-day:hover:not(:disabled) {
+ z-index: 1;
+ outline: 2px solid #3b82f6;
+ outline-offset: -2px;
+}
+
+.hrms-day:disabled { cursor: default; }
+
+.hrms-calendar.yearly .hrms-day {
+ min-height: 126px;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+}
+
+.hrms-day-top {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+}
+
+.hrms-day-top strong {
+ color: #071b4f;
+ font-size: 15px;
+ font-weight: 800;
+ line-height: 1;
+}
+
+.hrms-day-top span {
+ color: #94a3b8;
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+}
+
+/* Worked hours pill */
+.hrms-day-worked {
+ display: inline-flex;
+ align-items: center;
+ padding: 3px 8px;
+ border-radius: 6px;
+ background: #f0fdf4;
+ color: #16a34a;
+ font-size: 13px;
+ font-weight: 800;
+ width: fit-content;
+ margin-top: 2px;
+}
+
+/* Zero hours - red */
+.hrms-day-worked.zero {
+ background: #fef2f2;
+ color: #dc2626;
+}
+
+/* Day signals */
+.hrms-day-signals {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ margin-top: auto;
+}
+
+.hrms-day-signal {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 2px 6px;
+ border-radius: 5px;
+ font-size: 10px;
+ font-weight: 700;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ line-height: 1.4;
+ border: none;
+ background: none;
+ cursor: default;
+ text-align: left;
+ width: 100%;
+}
+
+.hrms-day-signal i {
+ flex: 0 0 auto;
+ font-size: 9px;
+ width: 14px;
+ height: 14px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 3px;
+ background: rgba(255,255,255,0.7);
+}
+
+.hrms-day-signal.event { background: #dbeafe; color: #1d4ed8; }
+.hrms-day-signal.holiday { background: #fef3c7; color: #b45309; }
+.hrms-day-signal.leave { background: #ffedd5; color: #c2410c; }
+.hrms-day-signal.more { background: #eef2ff; color: #4338ca; }
+
+/* Clickable "+X more" button */
+.hrms-more-btn {
+ cursor: pointer !important;
+ border: 1px dashed #a5b4fc !important;
+ background: #eef2ff !important;
+ justify-content: center;
+ transition: all 0.15s ease;
+}
+
+.hrms-more-btn:hover {
+ background: #c7d2fe !important;
+ color: #3730a3 !important;
+ border-color: #818cf8 !important;
+}
+
+.hrms-more-btn i {
+ background: rgba(99, 102, 241, 0.15);
+ color: #6366f1;
+}
+
+/* Day message */
+.hrms-day-message {
+ flex: 1;
+ margin-top: 6px;
+ padding: 8px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ text-align: center;
+}
+
+.hrms-day-message strong {
+ font-size: 11px;
+ line-height: 1.3;
+ color: inherit;
+ overflow-wrap: anywhere;
+}
+
+.hrms-day-message-icon { font-size: 14px; color: inherit; }
+
+/* Day status */
+.hrms-day.present { box-shadow: inset 0 3px 0 #22c55e; }
+.hrms-day.absent { box-shadow: inset 0 3px 0 #fecaca; }
+.hrms-day.leave { background: #fff7ed; box-shadow: inset 0 3px 0 #fb923c; }
+.hrms-day.leave .hrms-day-message { background: #ffedd5; color: #c2410c; border: 1px solid #fed7aa; }
+.hrms-day.holiday { background: #fffbeb; box-shadow: inset 0 3px 0 #fbbf24; }
+.hrms-day.holiday .hrms-day-message { background: #fef3c7; color: #92400e; border: 1px solid #fde68a; }
+.hrms-day.weekend { background: #f8fafc; box-shadow: none; }
+.hrms-day.weekend .hrms-day-message { background: transparent; color: #94a3b8; border: 1px dashed #e2e8f0; }
+.hrms-day.future { opacity: 0.5; }
+.hrms-day.empty { background: #f8fafc; opacity: 0.35; }
+
+/* Month stats for yearly view */
+.hrms-calendar-month-stats { display: grid; gap: 5px; margin-top: 10px; }
+.hrms-calendar-month-stats small { display: block; font-size: 12px; }
+
+/* Day message for leave/holiday/weekend */
+.hrms-day-message {
+ flex: 1;
+ margin-top: 6px;
+ padding: 8px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ text-align: center;
+}
+
+.hrms-day-message strong {
+ font-size: 11px;
+ line-height: 1.3;
+}
+
+.hrms-day-message-icon { font-size: 14px; }
+
+/* Day status colors - cleaner top border */
+.hrms-day.present { box-shadow: inset 0 3px 0 #22c55e; }
+.hrms-day.absent { box-shadow: inset 0 3px 0 #fecaca; }
+.hrms-day.leave { background: #fff7ed; box-shadow: inset 0 3px 0 #fb923c; }
+.hrms-day.leave .hrms-day-message { background: #ffedd5; color: #c2410c; border: 1px solid #fed7aa; }
+.hrms-day.holiday { background: #fffbeb; box-shadow: inset 0 3px 0 #fbbf24; }
+.hrms-day.holiday .hrms-day-message { background: #fef3c7; color: #92400e; border: 1px solid #fde68a; }
+.hrms-day.weekend { background: #f8fafc; box-shadow: none; }
+.hrms-day.weekend .hrms-day-message { background: transparent; color: #94a3b8; border: 1px dashed #e2e8f0; }
+.hrms-day.future { opacity: 0.5; }
+.hrms-day.empty { background: #f8fafc; opacity: 0.35; }
+
+/* ── Modal: Professional popup ── */
+.hrms-modal-status-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 0 20px 16px;
+ border-bottom: 1px solid #f1f5f9;
+}
+
+.hrms-modal-status-row strong {
+ font-size: 15px;
+ color: #334155;
+ font-weight: 700;
+}
+
+.hrms-modal-signals {
+ padding: 16px 20px;
+ border-top: 1px solid #f1f5f9;
+}
+
+.hrms-modal-signals > strong {
+ display: block;
+ font-size: 12px;
+ font-weight: 800;
+ color: #64748b;
+ text-transform: uppercase;
+ margin-bottom: 10px;
+ letter-spacing: 0.5px;
+}
+
+.hrms-modal-signals > div {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.hrms-modal-signals .hrms-day-signal {
+ padding: 6px 12px;
+ font-size: 12px;
+ border-radius: 8px;
+}
+
+.hrms-modal-signals .hrms-day-signal i {
+ width: 18px;
+ height: 18px;
+ font-size: 11px;
+}
+
+.hrms-day-detail {
+ min-height: 64px;
+ padding: 14px;
+ border: 1px solid #f1f5f9;
+ border-radius: 10px;
+ background: #fafbfc;
+ transition: border-color 0.15s ease;
+}
+
+.hrms-day-detail:hover {
+ border-color: #e2e8f0;
+}
+
+.hrms-day-detail span {
+ display: block;
+ color: #94a3b8;
+ font-size: 10px;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.hrms-day-detail strong {
+ display: block;
+ margin-top: 6px;
+ color: #0f172a;
+ font-size: 15px;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+/* ── Leave empty state ── */
+.hrms-empty-message {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 32px 16px;
+ color: #94a3b8;
+ font-size: 13px;
+ font-weight: 600;
+ text-align: center;
+}
+
+.hrms-empty-message i {
+ font-size: 28px;
+ color: #cbd5e1;
+}
+
+/* ── Leave Requests Section ── */
+.hrms-leave-divider {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin: 14px 0 10px;
+ color: #64748b;
+ font-size: 11px;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.hrms-leave-divider::before,
+.hrms-leave-divider::after {
+ content: '';
+ flex: 1;
+ height: 1px;
+ background: #e5e7eb;
+}
+
+.hrms-leave-empty-req {
+ text-align: center;
+ padding: 12px 0;
+}
+
+.hrms-leave-req-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 8px 10px;
+ border: 1px solid #f1f5f9;
+ border-radius: 8px;
+ background: #fafbfc;
+ margin-bottom: 6px;
+ transition: border-color 0.15s ease;
+}
+
+.hrms-leave-req-row:hover {
+ border-color: #e2e8f0;
+}
+
+.hrms-leave-req-info {
+ min-width: 0;
+ flex: 1;
+}
+
+.hrms-leave-req-info strong {
+ display: block;
+ color: #0f172a;
+ font-size: 12px;
+ font-weight: 700;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.hrms-leave-req-dates {
+ display: block;
+ color: #94a3b8;
+ font-size: 11px;
+ margin-top: 2px;
+}
+
+.hrms-leave-req-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+}
+
+.hrms-leave-req-duration {
+ color: #64748b;
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.hrms-leave-req-state {
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 10px;
+ font-weight: 800;
+ text-transform: capitalize;
+ white-space: nowrap;
+}
+
+.hrms-leave-req-state.draft { background: #f1f5f9; color: #64748b; }
+.hrms-leave-req-state.confirm { background: #fff7ed; color: #c2410c; }
+.hrms-leave-req-state.validate1 { background: #eff6ff; color: #2563eb; }
+.hrms-leave-req-state.validate { background: #dcfce7; color: #15803d; }
+.hrms-leave-req-state.refuse { background: #fef2f2; color: #dc2626; }
+.hrms-leave-req-state.cancel { background: #f1f5f9; color: #94a3b8; }
+
+.hrms-load-more-btn {
+ display: block;
+ width: 100%;
+ padding: 8px;
+ margin-top: 8px;
+ border: 1px dashed #cbd5e1;
+ border-radius: 8px;
+ background: #fff;
+ color: #475569;
+ font-size: 12px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.hrms-load-more-btn:hover {
+ background: #f8fafc;
+ border-color: #94a3b8;
+ color: #0f172a;
+}
+
+.hrms-equipment-timeline{
display:flex;
flex-direction:column;
- justify-content:center;
- align-items:center;
- text-align:center;
-
- border:1px dashed #d9d9d9;
- border-radius:12px;
-
- background:#fafafa;
- padding:40px;
+ gap:18px;
+ padding-top:8px;
}
-.expense-empty-state .empty-icon{
- width:90px;
- height:90px;
+.hrms-equipment-row{
+ display:grid;
+ grid-template-columns:110px 40px 1fr;
+ gap:18px;
+ align-items:flex-start;
+}
- border-radius:50%;
- background:#f3f4f6;
+.hrms-equipment-date{
+ text-align:right;
+ padding-top:12px;
+ font-size:13px;
+ color:#64748b;
+ font-weight:600;
+}
+.hrms-equipment-line{
+ position:relative;
display:flex;
justify-content:center;
- align-items:center;
-
- font-size:42px;
- color:#9ca3af;
-
- margin-bottom:20px;
+ min-height:130px;
}
-.expense-empty-state h3{
- font-size:24px;
- font-weight:600;
- color:#374151;
- margin-bottom:12px;
+.timeline-dot{
+ width:16px;
+ height:16px;
+ border-radius:50%;
+ background:#14b8a6;
+ border:4px solid white;
+ box-shadow:0 0 0 2px #14b8a6;
+ z-index:2;
}
-.expense-empty-state p{
- width:500px;
- max-width:95%;
- color:#6b7280;
- line-height:1.7;
- margin-bottom:25px;
+.timeline-line{
+ position:absolute;
+ top:18px;
+ width:2px;
+ bottom:-18px;
+ background:#d6dee8;
}
-.hrms-two-charts{
- display:grid;
- grid-template-columns:1fr 1fr;
- gap:20px;
+.hrms-equipment-row:last-child .timeline-line{
+ display:none;
}
-#hrmsExpenseChart,
-#hrmsExpenseStateChart{
- min-height:340px;
+.hrms-equipment-card {
+ position: relative;
+ width: 100%;
+ border: none;
+ text-align: left;
+ background: white;
+ border-radius: 14px;
+ padding: 22px;
+ border: 1px solid #e5e7eb;
+ box-shadow: 0 8px 22px rgba(0, 0, 0, .05);
+ transition: .25s;
}
+
+.hrms-equipment-card:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 14px 30px rgba(0, 0, 0, .12);
+ border-color: #14b8a6;
+}
+
+.equipment-service-count {
+ position: absolute;
+ right: 18px;
+ top: 18px;
+ padding: 6px 12px;
+ border-radius: 30px;
+ background: #ecfeff;
+ color: #0f766e;
+ font-weight: 700;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.equipment-title {
+ font-size: 18px;
+ font-weight: 700;
+ color: #0f172a;
+ margin-bottom: 6px;
+}
+
+.equipment-category {
+ color: #64748b;
+ font-size: 14px;
+ margin-bottom: 8px;
+}
+
+.equipment-serial {
+ color: #475569;
+ font-size: 13px;
+}
+
+@media (max-width: 768px) {
+
+ .hrms-equipment-row {
+ grid-template-columns:1fr;
+ }
+
+ .hrms-equipment-date {
+ text-align: left;
+ padding-left: 48px;
+ }
+
+ .hrms-equipment-line {
+ position: absolute;
+ left: 20px;
+ }
+}
\ No newline at end of file
diff --git a/addons_extensions/hrms_emp_dashboard/static/src/js/hrms_emp_dashboard.js b/addons_extensions/hrms_emp_dashboard/static/src/js/hrms_emp_dashboard.js
index 6cfeb97a6..9eaf4a9de 100644
--- a/addons_extensions/hrms_emp_dashboard/static/src/js/hrms_emp_dashboard.js
+++ b/addons_extensions/hrms_emp_dashboard/static/src/js/hrms_emp_dashboard.js
@@ -13,21 +13,23 @@ class HrmsEmployeeDashboard extends Component {
const today = new Date();
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0);
- const weekStart = new Date(today);
- weekStart.setDate(today.getDate() - ((today.getDay() + 6) % 7));
- const weekEnd = new Date(weekStart);
- weekEnd.setDate(weekStart.getDate() + 6);
this.state = useState({
loading: true,
error: null,
data: null,
period: "this_month",
- calendarView: "weekly",
+ calendarView: "monthly",
dateFrom: this.formatDate(monthStart),
dateTo: this.formatDate(monthEnd),
- calendarDateFrom: this.formatDate(weekStart),
- calendarDateTo: this.formatDate(weekEnd),
+ calendarDateFrom: this.formatDate(monthStart),
+ calendarDateTo: this.formatDate(monthEnd),
+ selectedDay: null,
+ activeTab: this.getStoredActiveTab(),
+ holidayTab: 'current',
+ leaveRequests: [],
+ leaveRequestsTotal: 0,
+ leaveRequestsLimit: 5,
});
this.rpc = rpc;
this.action = useService("action");
@@ -42,7 +44,6 @@ class HrmsEmployeeDashboard extends Component {
});
});
onWillDestroy(() => this.destroyCharts());
-
}
formatDate(date) {
@@ -52,6 +53,22 @@ class HrmsEmployeeDashboard extends Component {
return `${year}-${month}-${day}`;
}
+ getStoredActiveTab() {
+ try {
+ return window.localStorage.getItem("hrms_emp_dashboard.activeTab") || "employee";
+ } catch {
+ return "employee";
+ }
+ }
+
+ storeActiveTab(tab = this.state.activeTab) {
+ try {
+ window.localStorage.setItem("hrms_emp_dashboard.activeTab", tab || "employee");
+ } catch {
+ // Local storage can be unavailable in restricted browser contexts.
+ }
+ }
+
async loadData() {
this.state.loading = true;
this.state.error = null;
@@ -63,17 +80,45 @@ class HrmsEmployeeDashboard extends Component {
calendar_date_to: this.state.calendarDateTo,
calendar_view: this.state.calendarView,
});
- if (!response.success) {
+ if (!response.success) {
this.state.error = response.error || "Unable to load employee dashboard.";
return;
}
this.state.data = response;
+ if (!["employee", "manager", "hr"].includes(this.state.activeTab)) {
+ this.state.activeTab = "employee";
+ }
+ if (this.state.activeTab === "manager" && !response.access?.manager) {
+ this.state.activeTab = "employee";
+ }
+ if (this.state.activeTab === "hr" && !response.access?.hr) {
+ this.state.activeTab = response.access?.manager ? "manager" : "employee";
+ }
+ this.storeActiveTab();
+
+ // --- FILTER HOLIDAYS FOR TABS (Frontend Logic) ---
+ // If your backend doesn't separate them yet, we do it here based on 'date_from'
+ const today = new Date();
+ const currentMonth = today.getMonth();
+ const currentYear = today.getFullYear();
+
+ // If backend returns public_holidays[]
+ const allHolidays = response.public_holidays || [];
+ const yearlyHolidays = response.all_public_holidays || [];
+
+ // "This Month" tab = holidays within the selected period filter (backend already filters)
+ this.state.data.current_holidays = allHolidays;
+
+ // "Upcoming" tab = all yearly holidays strictly after today
+ this.state.data.upcoming_holidays = yearlyHolidays.filter(h => {
+ const hDate = new Date(h.date_from);
+ return hDate > today;
+ });
+ // -------------------------------------------------
+ this.loadLeaveRequests(true);
+
const expenses = response.expenses || {};
- const total =
- (expenses.series || []).reduce(
- (sum, value) => sum + Number(value || 0),
- 0
- );
+ const total = (expenses.series || []).reduce((sum, value) => sum + Number(value || 0), 0);
this.state.expenseCount = total;
setTimeout(() => this.initCharts(), 100);
} catch (error) {
@@ -85,6 +130,30 @@ class HrmsEmployeeDashboard extends Component {
}
}
+ async loadLeaveRequests(reset = false) {
+ if (reset) {
+ this.state.leaveRequestsLimit = 5;
+ this.state.leaveRequests = [];
+ }
+ try {
+ const response = await this.rpc("/hrms_emp_dashboard/leave_requests", {
+ limit: this.state.leaveRequestsLimit,
+ offset: 0,
+ });
+ if (response.success) {
+ this.state.leaveRequests = response.leaves;
+ this.state.leaveRequestsTotal = response.total;
+ }
+ } catch (e) {
+ console.warn("Could not load leave requests:", e);
+ }
+ }
+
+ loadMoreLeaves() {
+ this.state.leaveRequestsLimit += 5;
+ this.loadLeaveRequests();
+ }
+
destroyCharts() {
for (const chart of this.charts) {
try {
@@ -301,6 +370,170 @@ class HrmsEmployeeDashboard extends Component {
});
}
+ openDashboardMenu(menu) {
+ if (!menu || !menu.action) {
+ return;
+ }
+ this.storeActiveTab();
+ if (menu.action.xml_id) {
+ this.action.doAction(menu.action.xml_id);
+ return;
+ }
+ this.action.doAction(menu.action);
+ }
+
+ switchTab(tab) {
+ this.state.activeTab = tab;
+ this.storeActiveTab(tab);
+ setTimeout(() => this.initCharts(), 80);
+ }
+
+ openWorkAction(action) {
+ if (action) {
+ this.storeActiveTab();
+ this.action.doAction(action);
+ }
+ }
+
+ async runApproval(record, operation) {
+ try {
+ const response = await this.rpc("/hrms_emp_dashboard/approval_action", {
+ model: record.model,
+ record_id: record.id,
+ operation,
+ });
+ if (!response.success) {
+ this.notification.add(response.error || "Unable to update request", { type: "danger" });
+ return;
+ }
+ this.notification.add(response.message || "Request updated", { type: "success" });
+ await this.loadData();
+ } catch (error) {
+ console.error(error);
+ this.notification.add("Unable to update request", { type: "danger" });
+ }
+ }
+
+ createFromDashboardMenu(menu) {
+ if (!menu || !menu.create_action) {
+ return;
+ }
+ this.storeActiveTab();
+ this.action.doAction(menu.create_action);
+ }
+
+ runDayAction(action) {
+ const day = this.state.selectedDay;
+ if (!day || !action) {
+ return;
+ }
+ const handlers = {
+ apply_leave: () => this.applyLeaveForDate(day.date),
+ add_todo: () => this.addTodoForDate(day.date),
+ open_attendance: () => this.openAttendancesForDate(day.date),
+ open_calendar: () => this.openCalendarForDate(day.date, day.type),
+ create_meeting: () => this.createMeetingForDate(day.date),
+ };
+ const handler = handlers[action.key];
+ if (handler) {
+ handler();
+ this.closeDayDetails();
+ }
+ }
+
+ applyLeaveForDate(date) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: "Apply Leave",
+ res_model: "hr.leave",
+ views: [[false, "form"]],
+ target: "new",
+ context: {
+ default_employee_id: this.state.data.employee.id,
+ default_request_date_from: date,
+ default_request_date_to: date,
+ },
+ });
+ }
+
+ addTodoForDate(date) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: "Add To-do",
+ res_model: "project.task",
+ views: [[false, "form"]],
+ target: "new",
+ context: {
+ default_name: "To-do for " + date,
+ default_date_deadline: date + " 18:00:00",
+ default_user_ids: [[4, this.state.data.employee.user_id]],
+ },
+ });
+ }
+
+ openAttendancesForDate(date) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: "Attendance - " + date,
+ res_model: "hr.attendance",
+ views: [[false, "list"], [false, "form"]],
+ domain: [
+ ["employee_id", "=", this.state.data.employee.id],
+ ["check_in", ">=", date + " 00:00:00"],
+ ["check_in", "<=", date + " 23:59:59"],
+ ],
+ target: "current",
+ });
+ }
+
+ openCalendarForDate(date, type = "day") {
+ const domain = [];
+ if (type !== "month") {
+ domain.push(["start", "<=", date + " 23:59:59"]);
+ domain.push(["stop", ">=", date + " 00:00:00"]);
+ }
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: "Calendar",
+ res_model: "calendar.event",
+ views: [[false, "calendar"], [false, "list"], [false, "form"]],
+ domain,
+ target: "current",
+ context: {
+ default_start: date + " 09:00:00",
+ default_stop: date + " 10:00:00",
+ },
+ });
+ }
+
+ createMeetingFromCalendar() {
+ this.createMeetingForDate(this.state.calendarDateFrom || this.formatDate(new Date()));
+ }
+
+ createMeetingForDate(date) {
+ if (date && date.length === 7) {
+ date = date + "-01";
+ }
+ const start = date + " 09:00:00";
+ const stop = date + " 10:00:00";
+ const context = {
+ default_name: "Meeting",
+ default_start: start,
+ default_stop: stop,
+ };
+ if (this.state.data.employee.partner_id) {
+ context.default_partner_ids = [[4, this.state.data.employee.partner_id]];
+ }
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: "New Meeting",
+ res_model: "calendar.event",
+ views: [[false, "form"]],
+ target: "new",
+ context,
+ });
+ }
+
// addExpense() {
// this.action.doAction({
// type: "ir.actions.act_window",
@@ -418,6 +651,71 @@ class HrmsEmployeeDashboard extends Component {
});
}
+ buildDayDetails(day) {
+ if (!day || day.status === 'empty') return [];
+ const details = [];
+ if (day.worked_display) details.push({ label: "Worked", value: day.worked_display });
+ if (day.break_display) details.push({ label: "Break", value: day.break_display });
+ if (day.expected_display) details.push({ label: "Expected", value: day.expected_display });
+ if (day.balance_display) details.push({ label: "Balance", value: day.balance_display });
+ if (day.leave && parseFloat(day.leave) > 0) details.push({ label: "Leave", value: day.leave + " hrs" });
+ if (day.holiday && parseFloat(day.holiday) > 0) details.push({ label: "Holiday", value: day.holiday + " day(s)" });
+ if (day.event_count) details.push({ label: "Events", value: day.event_count });
+ if (day.date) details.push({ label: "Date", value: day.date });
+ return details;
+ }
+
+ async openDayDetails(day) {
+ if (!day || day.status === 'empty') return;
+
+ let fullDay = day;
+ let gotBackendSignals = false;
+
+ // Try backend first (for tasks, extra leaves, holidays)
+ if (day.date && day.type !== 'month') {
+ try {
+ const response = await this.rpc("/hrms_emp_dashboard/day_details", {
+ date: day.date,
+ });
+ if (response.success && response.day && response.day.signals && response.day.signals.length > 0) {
+ fullDay = { ...day, ...response.day };
+ gotBackendSignals = true;
+ }
+ } catch (e) {
+ console.warn("Backend details unavailable, using calendar data:", e);
+ }
+ }
+
+ let finalSignals;
+
+ if (gotBackendSignals) {
+ // Backend returned full signals — just strip any leftover "more"
+ finalSignals = (fullDay.signals || []).filter(s => s.type !== 'more');
+ } else {
+ // Rebuild ALL signals from day.events (contains every event, no truncation)
+ const existingSignals = fullDay.signals || [];
+ const nonEventSignals = existingSignals.filter(s => s.type !== 'more' && s.type !== 'event');
+ const eventSignals = (fullDay.events || []).map(event => ({
+ type: 'event',
+ label: event.display_time
+ ? event.display_time + ' ' + event.name
+ : event.name,
+ icon: 'fa fa-users',
+ }));
+ finalSignals = [...nonEventSignals, ...eventSignals];
+ }
+
+ this.state.selectedDay = {
+ ...fullDay,
+ signals: finalSignals,
+ details: this.buildDayDetails(fullDay),
+ };
+ }
+
+ closeDayDetails() {
+ this.state.selectedDay = null;
+ }
+
get statusText() {
return this.state.data?.attendance_state === "checked_in" ? "Check Out" : "Check In";
}
diff --git a/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml b/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml
index 5c60d1564..969071671 100644
--- a/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml
+++ b/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml
@@ -6,6 +6,27 @@
Loading employee dashboard...
+
+
+
+
+
+
+
![Employee]()
@@ -27,21 +48,15 @@
-
-
-
-
+
-
-
+
+
+
Expected Hours
Worked Hours
@@ -84,14 +127,18 @@
-
-
+
+
+
+
+
+
+
+
Manager Workspace
+
+
+
+
+
+
+
+
+
+
+
+
No pending manager approvals.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No team members reporting to you.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No pending HR approvals.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Events & Activities
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/hrms_emp_dashboard/views/hrms_emp_dashboard_views.xml b/addons_extensions/hrms_emp_dashboard/views/hrms_emp_dashboard_views.xml
index a86e71d83..ee1308bbc 100644
--- a/addons_extensions/hrms_emp_dashboard/views/hrms_emp_dashboard_views.xml
+++ b/addons_extensions/hrms_emp_dashboard/views/hrms_emp_dashboard_views.xml
@@ -1,13 +1,13 @@
- Employee Dashboard
+ Dashboard
hrms_emp_dashboard
current