diff --git a/portal_timesheet_input/README.rst b/portal_timesheet_input/README.rst new file mode 100644 index 000000000..b92c6286e --- /dev/null +++ b/portal_timesheet_input/README.rst @@ -0,0 +1,79 @@ +==================================== +Portal Timesheet Input (Weekly Grid) +==================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1eb6bbd525cd36ac479604fe6d523e91261004070d1612beb7881f6bb2800f71 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Ftimesheet-lightgray.png?logo=github + :target: https://github.com/OCA/timesheet/tree/18.0/portal_timesheet_input + :alt: OCA/timesheet +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/timesheet-18-0/timesheet-18-0-portal_timesheet_input + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/timesheet&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Allows portal users to enter timesheets using a weekly grid view, +similar to the Enterprise Timesheet Grid. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Log in as a portal user. +2. Go to the "Timesheets" menu (configured in your portal). +3. Access the weekly grid view to enter your time. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Acysos S.L. + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/timesheet `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/portal_timesheet_input/__init__.py b/portal_timesheet_input/__init__.py new file mode 100644 index 000000000..909cdd8fb --- /dev/null +++ b/portal_timesheet_input/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2025 Acysos S.L. (https://www.acysos.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import controllers diff --git a/portal_timesheet_input/__manifest__.py b/portal_timesheet_input/__manifest__.py new file mode 100644 index 000000000..b73cf7559 --- /dev/null +++ b/portal_timesheet_input/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2025 Acysos S.L. (https://www.acysos.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Portal Timesheet Input (Weekly Grid)", + "summary": "Portal Weekly Timesheet Grid Entry", + "author": "Acysos S.L., Odoo Community Association (OCA)", + "website": "https://github.com/OCA/timesheet", + "category": "Operations/Timesheets", + "version": "18.0.1.0.0", + "depends": ["portal", "hr_timesheet", "project"], + "data": [ + "security/ir.model.access.csv", + "security/security.xml", + "views/portal_templates.xml", + "views/hr_employee_views.xml", + ], + "assets": { + "web.assets_frontend": [ + "portal_timesheet_input/static/src/js/timesheet_input.esm.js", + ], + }, + "license": "AGPL-3", + "installable": True, + "application": False, + "auto_install": False, +} diff --git a/portal_timesheet_input/controllers/__init__.py b/portal_timesheet_input/controllers/__init__.py new file mode 100644 index 000000000..140e9130e --- /dev/null +++ b/portal_timesheet_input/controllers/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2025 Acysos S.L. (https://www.acysos.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import main diff --git a/portal_timesheet_input/controllers/main.py b/portal_timesheet_input/controllers/main.py new file mode 100644 index 000000000..4e36412f9 --- /dev/null +++ b/portal_timesheet_input/controllers/main.py @@ -0,0 +1,200 @@ +# Copyright 2025 Acysos S.L. (https://www.acysos.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from datetime import date, datetime, timedelta + +from odoo import _, http +from odoo.http import request + +from odoo.addons.hr_timesheet.controllers.portal import TimesheetCustomerPortal + + +class PortalTimesheetInput(TimesheetCustomerPortal): + """Portal Timesheet Input Controller.""" + + @http.route(["/my/timesheet/input"], type="http", auth="user", website=True) + def timesheet_input_grid(self, week_start=None, **kwargs): + """Render the timesheet input grid page.""" + # 1. Determine Week Range + today = date.today() + if week_start: + try: + start_date = datetime.strptime(week_start, "%Y-%m-%d").date() + except ValueError: + start_date = today - timedelta(days=today.weekday()) + else: + # Monday of current week + start_date = today - timedelta(days=today.weekday()) + + end_date = start_date + timedelta(days=6) + + # 2. Prepare Days Header + days = [] + current = start_date + while current <= end_date: + days.append( + { + "date": current, + "name": current.strftime("%a %d"), + "day_str": current.strftime("%Y-%m-%d"), + } + ) + current += timedelta(days=1) + + # 3. Fetch Data + partner = request.env.user.partner_id + employee = ( + request.env["hr.employee"] + .sudo() + .search([("user_id", "=", request.env.user.id)], limit=1) + ) + if not employee: + return request.render( + "portal_timesheet_input.timesheet_error", + { + "page_name": "timesheet_input", + "error_message": _( + "You must be linked to an employee to enter timesheets." + ), + }, + ) + + domain = [ + ("employee_id", "=", employee.id), + ("date", ">=", start_date), + ("date", "<=", end_date), + # Ensure project is set + ("project_id", "!=", False), + ] + timesheets = request.env["account.analytic.line"].sudo().search(domain) + grid_data = {} + + # Key can be 'task_ID' or 'proj_ID_no_task' + def get_key(line): + if line.task_id: + return f"task_{line.task_id.id}" + return f"proj_{line.project_id.id}" + + # Pre-fill with existing entries + for line in timesheets: + key = get_key(line) + if key not in grid_data: + grid_data[key] = { + "type": "task" if line.task_id else "project", + "id": (line.task_id.id if line.task_id else line.project_id.id), + "name": ( + line.task_id.name if line.task_id else line.project_id.name + ), + "project_name": line.project_id.name, + "project_id": line.project_id.id, + "days": {}, + } + + day_str = line.date.strftime("%Y-%m-%d") + grid_data[key]["days"][day_str] = ( + grid_data[key]["days"].get(day_str, 0.0) + line.unit_amount + ) + + # Convert grid_data to sorted list + rows = sorted(grid_data.values(), key=lambda x: (x["project_name"], x["name"])) + + # Pass available projects/tasks for the "Add Line" feature + # Standard portal domain for projects + projects = ( + request.env["project.project"] + .sudo() + .search( + [ + ("privacy_visibility", "=", "portal"), + ( + "message_partner_ids", + "child_of", + [partner.commercial_partner_id.id], + ), + ] + ) + ) + + values = { + "page_name": "timesheet_input_grid", + "week_start": start_date.strftime("%Y-%m-%d"), + "prev_week": (start_date - timedelta(days=7)).strftime("%Y-%m-%d"), + "next_week": (start_date + timedelta(days=7)).strftime("%Y-%m-%d"), + "days": days, + "rows": rows, + "projects": projects, + } + return request.render("portal_timesheet_input.timesheet_input_grid", values) + + @http.route(["/my/timesheet/save_grid"], type="json", auth="user", website=True) + def save_grid_inputs(self, inputs, week_start): + """ + inputs: list of { + 'project_id': int, 'task_id': int (opt), + 'date': str, 'unit_amount': float + } + """ + employee = ( + request.env["hr.employee"] + .sudo() + .search([("user_id", "=", request.env.user.id)], limit=1) + ) + if not employee: + return {"error": "User not linked to employee"} + + # We will process cell by cell. + timesheet_sudo = request.env["account.analytic.line"].sudo() + + for entry in inputs: + try: + date_input = entry.get("date") + date_obj = datetime.strptime(date_input, "%Y-%m-%d").date() + amount = float(entry.get("unit_amount") or 0.0) + project_id = int(entry.get("project_id")) + task_id = int(entry.get("task_id")) if entry.get("task_id") else False + + domain = [ + ("employee_id", "=", employee.id), + ("date", "=", date_obj), + ("project_id", "=", project_id), + ("task_id", "=", task_id), + ] + + existing = timesheet_sudo.search(domain) + + if amount == 0.0: + if existing: + existing.unlink() + else: + if existing: + current_total = sum(existing.mapped("unit_amount")) + if current_total != amount: + diff = amount - current_total + existing[-1].write( + {"unit_amount": existing[-1].unit_amount + diff} + ) + + else: + vals = { + "project_id": project_id, + "task_id": task_id, + "date": date_obj, + "unit_amount": amount, + "employee_id": employee.id, + "name": "/", # Default description + } + timesheet_sudo.create(vals) + except (ValueError, IndexError, TypeError) as e: + return {"error": str(e)} + + return {"success": True} + + @http.route(["/my/timesheet/get_tasks"], type="json", auth="user", website=True) + def get_project_tasks(self, project_id): + """Retrieve tasks for a specific project.""" + domain = [ + ("project_id", "=", int(project_id)), + # Add visibility checks similar to search + ("project_id.privacy_visibility", "=", "portal"), + ] + tasks = request.env["project.task"].sudo().search(domain) + return [{"id": t.id, "name": t.name} for t in tasks] diff --git a/portal_timesheet_input/i18n/es.po b/portal_timesheet_input/i18n/es.po new file mode 100644 index 000000000..ced2fb6ee --- /dev/null +++ b/portal_timesheet_input/i18n/es.po @@ -0,0 +1,88 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * portal_timesheet_entry +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-12-24 00:00+0000\n" +"PO-Revision-Date: 2024-12-24 00:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Weekly Timesheet Grid" +msgstr "Cuadrícula de Parte de Horas Semanal" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Weekly Timesheet" +msgstr "Parte de Horas Semanal" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "< Previous" +msgstr "< Anterior" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Next >" +msgstr "Siguiente >" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Save Changes" +msgstr "Guardar Cambios" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Project / Task" +msgstr "Proyecto / Tarea" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Total" +msgstr "Total" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Add Line:" +msgstr "Añadir Línea:" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Select Project..." +msgstr "Seleccionar Proyecto..." + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Select Task..." +msgstr "Seleccionar Tarea..." + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Add" +msgstr "Añadir" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Week Total" +msgstr "Total Semanal" + + +#. module: portal_timesheet_input +#: model_terms:ir.ui.view,arch_db:portal_timesheet_input.portal_my_home_timesheet_input +#: model_terms:ir.ui.view,arch_db:portal_timesheet_input.portal_my_home_menu_timesheet_input +msgid "Timesheet Input" +msgstr "Partes de Trabajo" + +#. module: portal_timesheet_input +#: model_terms:ir.ui.view,arch_db:portal_timesheet_input.portal_my_home_timesheet_input +msgid "Weekly timesheet input" +msgstr "Registro semanal de horas" diff --git a/portal_timesheet_input/i18n/portal_timesheet_input.pot b/portal_timesheet_input/i18n/portal_timesheet_input.pot new file mode 100644 index 000000000..fa2a1d5ed --- /dev/null +++ b/portal_timesheet_input/i18n/portal_timesheet_input.pot @@ -0,0 +1,93 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * portal_timesheet_entry +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-12-26 12:00+0000\n" +"PO-Revision-Date: 2025-12-26 12:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Weekly Timesheet Grid" +msgstr "" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Weekly Timesheet" +msgstr "" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "< Previous" +msgstr "" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Next >" +msgstr "" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Save Changes" +msgstr "" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Project / Task" +msgstr "" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Total" +msgstr "" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Add Line:" +msgstr "" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Select Project..." +msgstr "" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Select Task..." +msgstr "" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Add" +msgstr "" + +#. module: portal_timesheet_entry +#: model_terms:ir.ui.view,arch_db:portal_timesheet_entry.timesheet_entry_grid +msgid "Week Total" +msgstr "" + +#. module: portal_timesheet_entry +#. odoo-python +#: code:addons/portal_timesheet_entry/controllers/main.py:0 +msgid "You must be linked to an employee to enter timesheets." +msgstr "" + +#. module: portal_timesheet_input +#: model_terms:ir.ui.view,arch_db:portal_timesheet_input.portal_my_home_timesheet_input +#: model_terms:ir.ui.view,arch_db:portal_timesheet_input.portal_my_home_menu_timesheet_input +msgid "Timesheet Input" +msgstr "" + +#. module: portal_timesheet_input +#: model_terms:ir.ui.view,arch_db:portal_timesheet_input.portal_my_home_timesheet_input +msgid "Weekly timesheet input" +msgstr "" diff --git a/portal_timesheet_input/pyproject.toml b/portal_timesheet_input/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/portal_timesheet_input/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/portal_timesheet_input/readme/DESCRIPTION.md b/portal_timesheet_input/readme/DESCRIPTION.md new file mode 100644 index 000000000..b2203c466 --- /dev/null +++ b/portal_timesheet_input/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +Allows portal users to enter timesheets using a weekly grid view, +similar to the Enterprise Timesheet Grid. diff --git a/portal_timesheet_input/readme/USAGE.md b/portal_timesheet_input/readme/USAGE.md new file mode 100644 index 000000000..8acdeeccf --- /dev/null +++ b/portal_timesheet_input/readme/USAGE.md @@ -0,0 +1,3 @@ +1. Log in as a portal user. +2. Go to the "Timesheets" menu (configured in your portal). +3. Access the weekly grid view to enter your time. diff --git a/portal_timesheet_input/security/ir.model.access.csv b/portal_timesheet_input/security/ir.model.access.csv new file mode 100644 index 000000000..965cb2008 --- /dev/null +++ b/portal_timesheet_input/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_analytic_line_portal_user,account.analytic.line portal,account.model_account_analytic_line,base.group_portal,1,1,1,0 diff --git a/portal_timesheet_input/security/security.xml b/portal_timesheet_input/security/security.xml new file mode 100644 index 000000000..4fbbccee6 --- /dev/null +++ b/portal_timesheet_input/security/security.xml @@ -0,0 +1,9 @@ + + + + Portal Timesheet Input: own lines only + + [('user_id', '=', user.id)] + + + diff --git a/portal_timesheet_input/static/description/icon.png b/portal_timesheet_input/static/description/icon.png new file mode 100644 index 000000000..73977994e Binary files /dev/null and b/portal_timesheet_input/static/description/icon.png differ diff --git a/portal_timesheet_input/static/description/index.html b/portal_timesheet_input/static/description/index.html new file mode 100644 index 000000000..094eed5bc --- /dev/null +++ b/portal_timesheet_input/static/description/index.html @@ -0,0 +1,423 @@ + + + + + +Portal Timesheet Input (Weekly Grid) + + + +
+

Portal Timesheet Input (Weekly Grid)

+ + +

Beta License: AGPL-3 OCA/timesheet Translate me on Weblate Try me on Runboat

+

Allows portal users to enter timesheets using a weekly grid view, +similar to the Enterprise Timesheet Grid.

+

Table of contents

+ +
+

Usage

+
    +
  1. Log in as a portal user.
  2. +
  3. Go to the “Timesheets” menu (configured in your portal).
  4. +
  5. Access the weekly grid view to enter your time.
  6. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Acysos S.L.
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/timesheet project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/portal_timesheet_input/static/src/js/timesheet_input.esm.js b/portal_timesheet_input/static/src/js/timesheet_input.esm.js new file mode 100644 index 000000000..43049087d --- /dev/null +++ b/portal_timesheet_input/static/src/js/timesheet_input.esm.js @@ -0,0 +1,188 @@ +/* global window */ +/* Copyright 2025 Acysos S.L. (https://www.acysos.com) + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ +import publicWidget from "@web/legacy/js/public/public_widget"; +import {rpc} from "@web/core/network/rpc"; + +publicWidget.registry.TimesheetInputGrid = publicWidget.Widget.extend({ + selector: ".o_portal_timesheet_input_grid", + events: { + "change .timesheet-input": "_onInputChange", + "click #btn_save_timesheet": "_onSave", + "change #new_line_project_id": "_onProjectChange", + "click #btn_add_line": "_onAddLine", + }, + + start: function () { + this._super.apply(this, arguments); + this._computeTotals(); + }, + + _onInputChange: function () { + this._computeTotals(); + }, + + _computeTotals: function () { + var grandTotal = 0.0; + var colTotals = {}; + + // Reset col totals + this.$("tfoot .col-total").each(function () { + colTotals[$(this).attr("data-date")] = 0.0; + }); + + // Loop rows + this.$("tbody tr") + .not(".add-line-row") + .each(function () { + var $row = $(this); + var rowTotal = 0.0; + + $row.find(".timesheet-input").each(function () { + var val = parseFloat($(this).val()) || 0.0; + var date = $(this).attr("data-date"); + + rowTotal += val; + if (colTotals[date] !== undefined) { + colTotals[date] += val; + } + }); + + $row.find(".row-total").text(rowTotal.toFixed(2)); + grandTotal += rowTotal; + }); + + // Update footer + this.$("tfoot .col-total").each(function () { + var date = $(this).attr("data-date"); + $(this).text((colTotals[date] || 0.0).toFixed(2)); + }); + this.$(".grand-total").text(grandTotal.toFixed(2)); + }, + + _onSave: async function () { + var inputs = []; + var week_start = this.$("#week_start_date").val(); + + // Collect data + this.$("tbody tr") + .not(".add-line-row") + .each(function () { + var $row = $(this); + var projectId = $row.attr("data-project-id"); + var taskId = $row.attr("data-task-id"); + + $row.find(".timesheet-input").each(function () { + // Keep as string to detect empty vs 0? No, treating 0 as delete. + var val = $(this).val(); + var date = $(this).attr("data-date"); + // We send everything, let backend handle 0s + inputs.push({ + project_id: projectId, + task_id: taskId, + date: date, + unit_amount: val, + }); + }); + }); + + try { + const result = await rpc("/my/timesheet/save_grid", { + inputs: inputs, + week_start: week_start, + }); + if (result && result.success) { + window.location.reload(); + } else { + this.env.services.notification.add( + "Error saving: " + + (result && result.error ? result.error : "Unknown error"), + {type: "danger"} + ); + } + } catch (err) { + this.env.services.notification.add( + "Error saving: " + (err && err.message ? err.message : err), + {type: "danger"} + ); + } + }, + + _onProjectChange: function (ev) { + var projectId = $(ev.currentTarget).val(); + var $taskSelect = this.$("#new_line_task_id"); + $taskSelect.empty().append(''); + + if (projectId) { + rpc("/my/timesheet/get_tasks", {project_id: projectId}).then( + function (tasks) { + $.each(tasks, function (i, task) { + $taskSelect.append( + $("