OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
96
app/Domain/Timesheets/Controllers/AddTime.php
Normal file
96
app/Domain/Timesheets/Controllers/AddTime.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Clients\Services\Clients as ClientService;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Timesheets\Permissions\TimesheetsPermissions;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AddTime extends Controller
|
||||
{
|
||||
private TimesheetService $timesheetService;
|
||||
|
||||
private ProjectService $projectService;
|
||||
|
||||
private ClientService $clientService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(
|
||||
TimesheetService $timesheetService,
|
||||
ProjectService $projectService,
|
||||
ClientService $clientService
|
||||
): void {
|
||||
$this->timesheetService = $timesheetService;
|
||||
$this->projectService = $projectService;
|
||||
$this->clientService = $clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the add time form.
|
||||
*
|
||||
* This is the creation entry point, so it gates on CREATE (matching post()) rather than VIEW —
|
||||
* a view-only role should not be able to load the time-logging form. Grant-equivalent for every
|
||||
* built-in role (the set holding timesheets.create is exactly editor+, the prior authOrRedirect).
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::CREATE, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$this->tpl->assign('values', $this->timesheetService->getDefaultTimeValues());
|
||||
$this->tpl->assign('info', '');
|
||||
$this->assignTemplateVars();
|
||||
|
||||
return $this->tpl->display('timesheets.addTime');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles time entry creation.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::CREATE, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$info = '';
|
||||
$values = $this->timesheetService->getDefaultTimeValues();
|
||||
|
||||
if (isset($_POST['save']) || isset($_POST['saveNew'])) {
|
||||
$values = $this->timesheetService->parseAddTimePostValues($_POST, $values);
|
||||
$info = $this->timesheetService->validateAndSaveTime($values);
|
||||
|
||||
if (isset($_POST['saveNew'])) {
|
||||
$values = $this->timesheetService->getDefaultTimeValues();
|
||||
}
|
||||
}
|
||||
|
||||
$this->tpl->assign('values', $values);
|
||||
$this->tpl->assign('info', $info);
|
||||
$this->assignTemplateVars();
|
||||
|
||||
return $this->tpl->display('timesheets.addTime');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns common template variables.
|
||||
*/
|
||||
private function assignTemplateVars(): void
|
||||
{
|
||||
$this->tpl->assign('allClients', $this->clientService->getAll());
|
||||
$this->tpl->assign('allProjects', $this->projectService->getAll(showClosedProjects: false));
|
||||
// Scope the picker to the current user's own timesheets; an unscoped call would require
|
||||
// timesheets.manage and over-fetch every user's entries.
|
||||
$this->tpl->assign('allTickets', $this->timesheetService->getAll(
|
||||
dateFrom: dtHelper()->userNow()->subYears(10)->setToDbTimezone(),
|
||||
dateTo: dtHelper()->userNow()->addYears(10)->setToDbTimezone(),
|
||||
userId: (int) session('userdata.id'),
|
||||
));
|
||||
$this->tpl->assign('kind', $this->timesheetService->getLoggableHourTypes());
|
||||
}
|
||||
}
|
||||
75
app/Domain/Timesheets/Controllers/DelTime.php
Normal file
75
app/Domain/Timesheets/Controllers/DelTime.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Timesheets\Permissions\TimesheetsPermissions;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DelTime extends Controller
|
||||
{
|
||||
private TimesheetService $timesheetService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(TimesheetService $timesheetService): void
|
||||
{
|
||||
$this->timesheetService = $timesheetService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the delete time confirmation dialog.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::DELETE, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->displayPartial('errors.error403');
|
||||
}
|
||||
|
||||
$this->tpl->assign('id', (int) $params['id']);
|
||||
|
||||
return $this->tpl->displayPartial('timesheets.delTime');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles time entry deletion.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::DELETE, global: true, entityScoped: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->displayPartial('errors.error403');
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
|
||||
if (isset($_POST['del'])) {
|
||||
$result = $this->timesheetService->deleteTime($id);
|
||||
|
||||
if ($result === true) {
|
||||
$this->tpl->setNotification('notifications.time_deleted_successfully', 'success');
|
||||
|
||||
if (session()->exists('lastPage')) {
|
||||
return Frontcontroller::redirect(session('lastPage'));
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/timesheets/showMyList');
|
||||
}
|
||||
|
||||
$this->tpl->setNotification('notifications.no_permission_delete', 'error');
|
||||
}
|
||||
|
||||
$this->tpl->assign('id', $id);
|
||||
|
||||
return $this->tpl->displayPartial('timesheets.delTime');
|
||||
}
|
||||
}
|
||||
118
app/Domain/Timesheets/Controllers/EditTime.php
Normal file
118
app/Domain/Timesheets/Controllers/EditTime.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Clients\Services\Clients as ClientService;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
|
||||
use Leantime\Domain\Timesheets\Permissions\TimesheetsPermissions;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EditTime extends Controller
|
||||
{
|
||||
private TimesheetService $timesheetService;
|
||||
|
||||
private ProjectService $projectService;
|
||||
|
||||
private TicketService $ticketService;
|
||||
|
||||
private ClientService $clientService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(
|
||||
TimesheetService $timesheetService,
|
||||
ProjectService $projectService,
|
||||
TicketService $ticketService,
|
||||
ClientService $clientService
|
||||
): void {
|
||||
$this->timesheetService = $timesheetService;
|
||||
$this->projectService = $projectService;
|
||||
$this->ticketService = $ticketService;
|
||||
$this->clientService = $clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the edit time form.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::EDIT, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->displayPartial('errors.error403');
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
// getTimesheetForEdit routes through the gated getTimesheet (own → view, another user's →
|
||||
// manage), returning null for an entry the user may not access — this fences ownership,
|
||||
// replacing the former manual role/owner check.
|
||||
$values = $this->timesheetService->getTimesheetForEdit($id);
|
||||
|
||||
if ($values === null) {
|
||||
return $this->tpl->displayPartial('errors.error403');
|
||||
}
|
||||
|
||||
$this->tpl->assign('values', $values);
|
||||
$this->tpl->assign('info', '');
|
||||
$this->assignTemplateVars();
|
||||
|
||||
return $this->tpl->displayPartial('timesheets.editTime');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles time entry update.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::EDIT, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->displayPartial('errors.error403');
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
// Fences ownership via the gated read; the save itself goes through the gated
|
||||
// validateAndUpdateTime → updateTime (own → edit, another user's → manage).
|
||||
$values = $this->timesheetService->getTimesheetForEdit($id);
|
||||
|
||||
if ($values === null) {
|
||||
return $this->tpl->displayPartial('errors.error403');
|
||||
}
|
||||
|
||||
if (isset($_POST['saveForm'])) {
|
||||
$values = $this->timesheetService->applyEditTimePostUpdates($_POST, $values);
|
||||
$values = $this->timesheetService->processEditTimeInvoiceFields($_POST, $values);
|
||||
|
||||
$result = $this->timesheetService->validateAndUpdateTime($id, $values);
|
||||
$values = $result['values'];
|
||||
|
||||
if ($result['notification'] !== null) {
|
||||
$this->tpl->setNotification($result['notification']['message'], $result['notification']['type']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->tpl->assign('values', $values);
|
||||
$this->tpl->assign('info', '');
|
||||
$this->assignTemplateVars();
|
||||
|
||||
return $this->tpl->displayPartial('timesheets.editTime');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns common template variables for the edit form.
|
||||
*/
|
||||
private function assignTemplateVars(): void
|
||||
{
|
||||
$this->tpl->assign('allClients', $this->clientService->getAll());
|
||||
$this->tpl->assign('allProjects', $this->projectService->getAll(showClosedProjects: false));
|
||||
$this->tpl->assign('allTickets', $this->ticketService->getAll());
|
||||
$this->tpl->assign('kind', $this->timesheetService->getLoggableHourTypes());
|
||||
}
|
||||
}
|
||||
164
app/Domain/Timesheets/Controllers/ShowAll.php
Normal file
164
app/Domain/Timesheets/Controllers/ShowAll.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Controllers;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Clients\Services\Clients as ClientService;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
|
||||
use Leantime\Domain\Timesheets\Permissions\TimesheetsPermissions;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService;
|
||||
use Leantime\Domain\Users\Services\Users as UserService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ShowAll extends Controller
|
||||
{
|
||||
private ProjectService $projectService;
|
||||
|
||||
private ClientService $clientService;
|
||||
|
||||
private TimesheetService $timesheetService;
|
||||
|
||||
private TicketService $ticketService;
|
||||
|
||||
private UserService $userService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(
|
||||
ProjectService $projectService,
|
||||
TimesheetService $timesheetService,
|
||||
ClientService $clientService,
|
||||
TicketService $ticketService,
|
||||
UserService $userService
|
||||
): void {
|
||||
$this->timesheetService = $timesheetService;
|
||||
$this->projectService = $projectService;
|
||||
$this->clientService = $clientService;
|
||||
$this->ticketService = $ticketService;
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the list of all timesheets.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::MANAGE, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
session(['lastPage' => BASE_URL.'/timesheets/showAll']);
|
||||
|
||||
$this->assignTemplateVars(
|
||||
dateFrom: dtHelper()->userNow()->startOfWeek(CarbonInterface::MONDAY)->setToDbTimezone(),
|
||||
dateTo: dtHelper()->userNow()->endOfMonth()->setToDbTimezone(),
|
||||
kind: 'all',
|
||||
userId: null,
|
||||
invEmplCheck: '-1',
|
||||
invCompCheck: '0',
|
||||
paidCheck: '0',
|
||||
projectFilter: -1,
|
||||
ticketFilter: -1,
|
||||
clientId: -1,
|
||||
);
|
||||
|
||||
return $this->tpl->display('timesheets.showAll');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles timesheet filter changes and invoice saves.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::MANAGE, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
session(['lastPage' => BASE_URL.'/timesheets/showAll']);
|
||||
|
||||
if (isset($_POST['saveInvoice'])) {
|
||||
$this->timesheetService->updateInvoices(
|
||||
$_POST['invoicedEmpl'] ?? [],
|
||||
$_POST['invoicedComp'] ?? [],
|
||||
$_POST['paid'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
$filters = $this->timesheetService->buildShowAllFilters($_POST);
|
||||
|
||||
$this->assignTemplateVars(
|
||||
dateFrom: $filters['dateFrom'],
|
||||
dateTo: $filters['dateTo'],
|
||||
kind: $filters['kind'],
|
||||
userId: $filters['userId'],
|
||||
invEmplCheck: $filters['invEmplCheck'],
|
||||
invCompCheck: $filters['invCompCheck'],
|
||||
paidCheck: $filters['paidCheck'],
|
||||
projectFilter: $filters['projectFilter'],
|
||||
ticketFilter: $filters['ticketFilter'],
|
||||
clientId: $filters['clientId'],
|
||||
);
|
||||
|
||||
return $this->tpl->display('timesheets.showAll');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns common template variables for the timesheet list view.
|
||||
*/
|
||||
private function assignTemplateVars(
|
||||
CarbonInterface $dateFrom,
|
||||
CarbonInterface $dateTo,
|
||||
string $kind,
|
||||
?int $userId,
|
||||
string $invEmplCheck,
|
||||
string $invCompCheck,
|
||||
string $paidCheck,
|
||||
int|string $projectFilter,
|
||||
int|string $ticketFilter,
|
||||
int|string $clientId,
|
||||
): void {
|
||||
$selectedTicketProjectId = null;
|
||||
if ($ticketFilter != '' && $ticketFilter != -1) {
|
||||
$selectedTicket = $this->ticketService->getTicket($ticketFilter);
|
||||
if ($selectedTicket) {
|
||||
$selectedTicketProjectId = $selectedTicket->projectId;
|
||||
}
|
||||
}
|
||||
|
||||
$resolvedTicketFilter = $this->timesheetService->resolveShowAllTicketFilter(
|
||||
$projectFilter,
|
||||
$ticketFilter,
|
||||
$selectedTicketProjectId
|
||||
);
|
||||
|
||||
$this->tpl->assign('employeeFilter', $userId);
|
||||
$this->tpl->assign('employees', $this->userService->getAll());
|
||||
$this->tpl->assign('dateFrom', $dateFrom);
|
||||
$this->tpl->assign('dateTo', $dateTo);
|
||||
$this->tpl->assign('actKind', $kind);
|
||||
$this->tpl->assign('kind', $this->timesheetService->getBookedHourTypes());
|
||||
$this->tpl->assign('invComp', $invCompCheck);
|
||||
$this->tpl->assign('invEmpl', $invEmplCheck);
|
||||
$this->tpl->assign('paid', $paidCheck);
|
||||
$this->tpl->assign('allProjects', $this->projectService->getAll());
|
||||
$this->tpl->assign('projectFilter', $projectFilter);
|
||||
$this->tpl->assign('allTickets', ($projectFilter == -1) ? [] : $this->ticketService->getAll(['currentProject' => $projectFilter]));
|
||||
$this->tpl->assign('ticketFilter', $ticketFilter);
|
||||
$this->tpl->assign('clientFilter', $clientId);
|
||||
$this->tpl->assign('allClients', $this->clientService->getAll());
|
||||
$this->tpl->assign('allTimesheets', $this->timesheetService->getAll(
|
||||
$dateFrom,
|
||||
$dateTo,
|
||||
(int) $projectFilter,
|
||||
$kind,
|
||||
$userId,
|
||||
$invEmplCheck,
|
||||
$invCompCheck,
|
||||
$resolvedTicketFilter,
|
||||
$paidCheck,
|
||||
$clientId
|
||||
));
|
||||
}
|
||||
}
|
||||
97
app/Domain/Timesheets/Controllers/ShowMy.php
Normal file
97
app/Domain/Timesheets/Controllers/ShowMy.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Controllers;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Timesheets\Permissions\TimesheetsPermissions;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ShowMy extends Controller
|
||||
{
|
||||
private TimesheetService $timesheetService;
|
||||
|
||||
private ProjectService $projectService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(
|
||||
TimesheetService $timesheetService,
|
||||
ProjectService $projectService
|
||||
): void {
|
||||
$this->timesheetService = $timesheetService;
|
||||
$this->projectService = $projectService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the weekly timesheet view.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::VIEW, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$fromDate = dtHelper()->userNow()->startOfWeek(CarbonInterface::MONDAY)->setToDbTimezone();
|
||||
|
||||
$this->assignTemplateVars($fromDate);
|
||||
|
||||
return $this->tpl->display('timesheets.showMy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles weekly timesheet search and save operations.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::CREATE, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$fromDate = dtHelper()->userNow()->startOfWeek(CarbonInterface::MONDAY)->setToDbTimezone();
|
||||
|
||||
if (isset($_POST['search']) && ! empty($_POST['startDate'])) {
|
||||
$parsed = $this->timesheetService->parseWeeklyStartDate($_POST['startDate'], $fromDate);
|
||||
$fromDate = $parsed['date'];
|
||||
|
||||
if ($parsed['failed']) {
|
||||
$this->tpl->setNotification('Could not parse date', 'error', 'save_timesheet');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['saveTimeSheet'])) {
|
||||
$notifications = $this->timesheetService->saveWeeklyTimesheetEntries($_POST);
|
||||
foreach ($notifications as $notification) {
|
||||
$this->tpl->setNotification($notification['message'], $notification['type'], 'save_timesheet');
|
||||
}
|
||||
}
|
||||
|
||||
$this->assignTemplateVars($fromDate);
|
||||
|
||||
return $this->tpl->display('timesheets.showMy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns common template variables for the weekly timesheet view.
|
||||
*/
|
||||
private function assignTemplateVars(CarbonInterface $fromDate): void
|
||||
{
|
||||
$weekly = $this->timesheetService->getWeeklyTimesheetsWithTicketIds(-1, $fromDate, session('userdata.id'));
|
||||
|
||||
$this->tpl->assign('existingTicketIds', $weekly['existingTicketIds']);
|
||||
$this->tpl->assign('dateFrom', $fromDate);
|
||||
$this->tpl->assign('actKind', 'all');
|
||||
$this->tpl->assign('kind', $this->timesheetService->getLoggableHourTypes());
|
||||
$this->tpl->assign('allProjects', $this->projectService->getProjectsAssignedToUser(
|
||||
userId: session('userdata.id'),
|
||||
projectTypes: 'project'
|
||||
));
|
||||
$this->tpl->assign('allTickets', $this->timesheetService->getUsersTickets(
|
||||
userId: session('userdata.id'),
|
||||
limit: -1
|
||||
));
|
||||
$this->tpl->assign('allTimesheets', $weekly['timesheets']);
|
||||
}
|
||||
}
|
||||
88
app/Domain/Timesheets/Controllers/ShowMyList.php
Normal file
88
app/Domain/Timesheets/Controllers/ShowMyList.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Controllers;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Timesheets\Permissions\TimesheetsPermissions;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ShowMyList extends Controller
|
||||
{
|
||||
private TimesheetService $timesheetService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(TimesheetService $timesheetService): void
|
||||
{
|
||||
$this->timesheetService = $timesheetService;
|
||||
session(['lastPage' => BASE_URL.'/timesheets/showMyList']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the user's timesheet list.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::VIEW, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$kind = 'all';
|
||||
$dateFrom = dtHelper()->userNow()->startOfWeek(CarbonInterface::MONDAY)->setToDbTimezone();
|
||||
$dateTo = dtHelper()->userNow()->endOfWeek()->setToDbTimezone();
|
||||
|
||||
$this->assignTemplateVars($dateFrom, $dateTo, $kind);
|
||||
|
||||
return $this->tpl->display('timesheets.showMyList');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles timesheet list filter changes.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::VIEW, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$kind = ! empty($_POST['kind']) ? $_POST['kind'] : 'all';
|
||||
|
||||
$dateFrom = dtHelper()->userNow()->startOfWeek(CarbonInterface::MONDAY)->setToDbTimezone();
|
||||
$dateTo = dtHelper()->userNow()->endOfWeek()->setToDbTimezone();
|
||||
|
||||
if (! empty($_POST['dateFrom'])) {
|
||||
$dateFrom = dtHelper()->parseUserDateTime($_POST['dateFrom'])->setToDbTimezone();
|
||||
}
|
||||
|
||||
if (! empty($_POST['dateTo'])) {
|
||||
$dateTo = dtHelper()->parseUserDateTime($_POST['dateTo'])->setToDbTimezone();
|
||||
}
|
||||
|
||||
$this->assignTemplateVars($dateFrom, $dateTo, $kind);
|
||||
|
||||
return $this->tpl->display('timesheets.showMyList');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns common template variables.
|
||||
*/
|
||||
private function assignTemplateVars(CarbonInterface $dateFrom, CarbonInterface $dateTo, string $kind): void
|
||||
{
|
||||
$this->tpl->assign('dateFrom', $dateFrom);
|
||||
$this->tpl->assign('dateTo', $dateTo);
|
||||
$this->tpl->assign('actKind', $kind);
|
||||
$this->tpl->assign('kind', $this->timesheetService->getLoggableHourTypes());
|
||||
$this->tpl->assign('allTimesheets', $this->timesheetService->getAll(
|
||||
dateFrom: $dateFrom,
|
||||
dateTo: $dateTo,
|
||||
projectId: -1,
|
||||
kind: $kind,
|
||||
userId: session('userdata.id'),
|
||||
invEmpl: '-1',
|
||||
invComp: '-1',
|
||||
paid: '-1'
|
||||
));
|
||||
}
|
||||
}
|
||||
87
app/Domain/Timesheets/Hxcontrollers/Stopwatch.php
Normal file
87
app/Domain/Timesheets/Hxcontrollers/Stopwatch.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Hxcontrollers;
|
||||
|
||||
use Error;
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\HtmxController;
|
||||
use Leantime\Domain\Timesheets\Permissions\TimesheetsPermissions;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets;
|
||||
|
||||
class Stopwatch extends HtmxController
|
||||
{
|
||||
protected static string $view = 'timesheets::partials.stopwatch';
|
||||
|
||||
private Timesheets $timesheetService;
|
||||
|
||||
/**
|
||||
* Controller constructor
|
||||
*/
|
||||
public function init(Timesheets $timesheetService): void
|
||||
{
|
||||
$this->timesheetService = $timesheetService;
|
||||
}
|
||||
|
||||
/**
|
||||
* show stop watch
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::VIEW, global: true)]
|
||||
public function getStatus(): void
|
||||
{
|
||||
|
||||
$onTheClock = session()->exists('userdata') ? $this->timesheetService->isClocked(session('userdata.id')) : false;
|
||||
$this->tpl->assign('onTheClock', $onTheClock);
|
||||
}
|
||||
|
||||
/**
|
||||
* show stop watch
|
||||
*/
|
||||
#[RequiresPermission(TimesheetsPermissions::CREATE, global: true)]
|
||||
public function stopTimer(): void
|
||||
{
|
||||
if ($this->incomingRequest->getMethod() !== 'PATCH') {
|
||||
throw new Error('This endpoint only supports PATCH requests');
|
||||
}
|
||||
|
||||
$params = $this->incomingRequest->request->all();
|
||||
|
||||
if (isset($params['action']) && $params['action'] === 'stop') {
|
||||
$ticketId = (int) filter_var($params['ticketId'], FILTER_SANITIZE_NUMBER_INT);
|
||||
$hoursBooked = $this->timesheetService->punchOut($ticketId);
|
||||
}
|
||||
|
||||
$this->tpl->setHTMXEvent('timerUpdate');
|
||||
|
||||
$onTheClock = session()->exists('userdata') ? $this->timesheetService->isClocked(session('userdata.id')) : false;
|
||||
$this->tpl->assign('onTheClock', $onTheClock);
|
||||
}
|
||||
|
||||
#[RequiresPermission(TimesheetsPermissions::CREATE, global: true)]
|
||||
public function startTimer(): void
|
||||
{
|
||||
if ($this->incomingRequest->getMethod() !== 'PATCH') {
|
||||
throw new Error('This endpoint only supports PATCH requests');
|
||||
}
|
||||
|
||||
$params = $this->incomingRequest->request->all();
|
||||
|
||||
if (isset($params['action']) && $params['action'] === 'start') {
|
||||
$ticketId = (int) filter_var($params['ticketId'], FILTER_SANITIZE_NUMBER_INT);
|
||||
|
||||
if ($ticketId > 0) {
|
||||
$result = $this->timesheetService->punchIn($ticketId);
|
||||
|
||||
if ($result) {
|
||||
$this->tpl->setNotification(__('short_notifications.timer_started'), 'success');
|
||||
} else {
|
||||
$this->tpl->setNotification(__('short_notifications.timer_start_failed'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->tpl->setHTMXEvent('timerUpdate');
|
||||
|
||||
$onTheClock = session()->exists('userdata') ? $this->timesheetService->isClocked(session('userdata.id')) : false;
|
||||
$this->tpl->assign('onTheClock', $onTheClock);
|
||||
}
|
||||
}
|
||||
108
app/Domain/Timesheets/Js/timesheetsController.js
Normal file
108
app/Domain/Timesheets/Js/timesheetsController.js
Normal file
@@ -0,0 +1,108 @@
|
||||
leantime.timesheetsController = (function () {
|
||||
var closeModal = false;
|
||||
|
||||
var initTimesheetsTable = function (groupBy) {
|
||||
jQuery(document).ready(function () {
|
||||
var allTimesheets = jQuery("#allTimesheetsTable").DataTable({
|
||||
"language": {
|
||||
"decimal": leantime.i18n.__("datatables.decimal"),
|
||||
"emptyTable": leantime.i18n.__("datatables.emptyTable"),
|
||||
"info": leantime.i18n.__("datatables.info"),
|
||||
"infoEmpty": leantime.i18n.__("datatables.infoEmpty"),
|
||||
"infoFiltered": leantime.i18n.__("datatables.infoFiltered"),
|
||||
"infoPostFix": leantime.i18n.__("datatables.infoPostFix"),
|
||||
"thousands": leantime.i18n.__("datatables.thousands"),
|
||||
"lengthMenu": leantime.i18n.__("datatables.lengthMenu"),
|
||||
"loadingRecords": leantime.i18n.__("datatables.loadingRecords"),
|
||||
"processing": leantime.i18n.__("datatables.processing"),
|
||||
"search": leantime.i18n.__("datatables.search"),
|
||||
"zeroRecords": leantime.i18n.__("datatables.zeroRecords"),
|
||||
"paginate": {
|
||||
"first": leantime.i18n.__("datatables.first"),
|
||||
"last": leantime.i18n.__("datatables.last"),
|
||||
"next": leantime.i18n.__("datatables.next"),
|
||||
"previous": leantime.i18n.__("datatables.previous"),
|
||||
},
|
||||
"aria": {
|
||||
"sortAscending": leantime.i18n.__("datatables.sortAscending"),
|
||||
"sortDescending":leantime.i18n.__("datatables.sortDescending"),
|
||||
},
|
||||
"buttons": {
|
||||
colvis: leantime.i18n.__("datatables.buttons.colvis"),
|
||||
csv: leantime.i18n.__("datatables.buttons.download")
|
||||
}
|
||||
|
||||
},
|
||||
"dom": '<"top">rt<"bottom"ilp><"clear">',
|
||||
"searching": false,
|
||||
"stateSave": true,
|
||||
"displayLength":100,
|
||||
|
||||
});
|
||||
|
||||
var buttons = new jQuery.fn.dataTable.Buttons(allTimesheets, {
|
||||
buttons: [
|
||||
{
|
||||
extend: 'csvHtml5',
|
||||
title: leantime.i18n.__("label.filename_fileexport"),
|
||||
charset: 'utf-8',
|
||||
bom: true,
|
||||
exportOptions: {
|
||||
format: {
|
||||
body: function ( data, row, column, node ) {
|
||||
if ( typeof jQuery(node).data('order') !== 'undefined') {
|
||||
data = jQuery(node).data('order');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
extend: 'colvis',
|
||||
columns: ':not(.noVis)'
|
||||
}
|
||||
]
|
||||
}).container().appendTo(jQuery('#tableButtons'));
|
||||
|
||||
jQuery('#allTimesheetsTable').on('column-visibility.dt', function ( e, settings, column, state ) {
|
||||
allTimesheets.draw(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var initEditTimeModal = function () {
|
||||
var canvasoptions = {
|
||||
sizes: {
|
||||
minW: 700,
|
||||
minH: 1000,
|
||||
},
|
||||
resizable: true,
|
||||
autoSizable: true,
|
||||
callbacks: {
|
||||
beforeShowCont: function () {
|
||||
jQuery(".showDialogOnLoad").show();
|
||||
if (closeModal === true) {
|
||||
closeModal = false;
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
afterShowCont: function () {
|
||||
jQuery(".editTimeModal").nyroModal(canvasoptions);
|
||||
},
|
||||
beforeClose: function () {
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
titleFromIframe: true
|
||||
|
||||
};
|
||||
|
||||
jQuery(".editTimeModal").nyroModal(canvasoptions);
|
||||
};
|
||||
|
||||
// Make public what you want to have public, everything else is private
|
||||
return {
|
||||
initTimesheetsTable:initTimesheetsTable,
|
||||
initEditTimeModal:initEditTimeModal,
|
||||
};
|
||||
})();
|
||||
58
app/Domain/Timesheets/Permissions/TimesheetsPermissions.php
Normal file
58
app/Domain/Timesheets/Permissions/TimesheetsPermissions.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Permissions;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\Permission;
|
||||
use Leantime\Core\Auth\Permissions\ProvidesPermissions;
|
||||
|
||||
/**
|
||||
* The Timesheets permission vocabulary — company-wide (GLOBAL-scoped) verbs.
|
||||
*
|
||||
* Timesheets are GLOBAL-scoped (projectScoped = false): they represent the time an employee logs
|
||||
* across the whole company, and the controllers have always gated on the user's GLOBAL role
|
||||
* (Auth::authOrRedirect([...], forceGlobalRoleCheck: true)). So these verbs resolve against the
|
||||
* global role, not a project role.
|
||||
*
|
||||
* Two verb families:
|
||||
* - view/create/edit/delete — EDITOR+ for the user's OWN time (ownership is enforced in the
|
||||
* service: a non-manager may only read/write their own entries).
|
||||
* - manage — MANAGER+ only: act on OTHER users' time, mark invoiced/paid, and the cross-user/
|
||||
* cross-project reports.
|
||||
*
|
||||
* Because these are GLOBAL-scoped, the standard verbs do NOT auto-grant through the project-scoped
|
||||
* matrix rules — {@see \Leantime\Core\Auth\Permissions\DefaultRolePermissions} grants editor the
|
||||
* four standard keys and manager the `manage` key explicitly (admin/owner auto-grant via scope:any).
|
||||
*/
|
||||
final class TimesheetsPermissions implements ProvidesPermissions
|
||||
{
|
||||
/** View own timesheets; managers view anyone's. Ownership enforced in the service. */
|
||||
public const VIEW = 'timesheets.view';
|
||||
|
||||
/** Log time for own account; managers log for any user. Ownership enforced in the service. */
|
||||
public const CREATE = 'timesheets.create';
|
||||
|
||||
/** Edit own timesheets; managers edit anyone's. Ownership enforced in the service. */
|
||||
public const EDIT = 'timesheets.edit';
|
||||
|
||||
/** Delete own timesheets; managers delete anyone's. Ownership enforced in the service. */
|
||||
public const DELETE = 'timesheets.delete';
|
||||
|
||||
/** Manage timesheets company-wide: mark invoiced/paid, cross-user reports, act on others' time. Manager+. */
|
||||
public const MANAGE = 'timesheets.manage';
|
||||
|
||||
public function domain(): string
|
||||
{
|
||||
return 'timesheets';
|
||||
}
|
||||
|
||||
public function permissions(): array
|
||||
{
|
||||
return [
|
||||
new Permission(self::VIEW, 'View own timesheets (anyone\'s when manager+)', false),
|
||||
new Permission(self::CREATE, 'Log time for own account (others when manager+)', false),
|
||||
new Permission(self::EDIT, 'Edit own timesheets (others when manager+)', false),
|
||||
new Permission(self::DELETE, 'Delete own timesheets (others when manager+)', false),
|
||||
new Permission(self::MANAGE, 'Manage timesheets company-wide (invoice/pay/reports)', false),
|
||||
];
|
||||
}
|
||||
}
|
||||
1039
app/Domain/Timesheets/Repositories/Timesheets.php
Normal file
1039
app/Domain/Timesheets/Repositories/Timesheets.php
Normal file
File diff suppressed because it is too large
Load Diff
1236
app/Domain/Timesheets/Services/Timesheets.php
Normal file
1236
app/Domain/Timesheets/Services/Timesheets.php
Normal file
File diff suppressed because it is too large
Load Diff
205
app/Domain/Timesheets/Templates/addTime.blade.php
Normal file
205
app/Domain/Timesheets/Templates/addTime.blade.php
Normal file
@@ -0,0 +1,205 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$values = $values ?? [];
|
||||
@endphp
|
||||
|
||||
<div class="pageheader">
|
||||
<form action="index.php?act=tickets.showAll" method="post" class="searchbar">
|
||||
<x-global::forms.text-input name="term"
|
||||
placeholder="{{ __('input.placeholders.search_type_hit_enter') }}" />
|
||||
</form>
|
||||
|
||||
<div class="pageicon"><span class="fa-laptop"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{!! __('OVERVIEW') !!}</h5>
|
||||
<h1>{!! __('MY_TIMESHEETS') !!}</h1>
|
||||
</div>
|
||||
</div><!--pageheader-->
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
|
||||
<div class="fail">
|
||||
@if (($info ?? '') != '')
|
||||
<span class="info">{!! $tpl->displayNotification() !!}</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div id="loader"> </div>
|
||||
<form action="" method="post" class="stdform">
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
|
||||
|
||||
<div class="widget">
|
||||
<h4 class="widgettitle">{!! __('OVERVIEW') !!}</h4>
|
||||
<div class="widgetcontent" style="min-height: 460px">
|
||||
|
||||
<label for="clients">{!! __('label.client') !!}</label>
|
||||
<select name="clients" id="clients" onchange="filterProjectsByClient();">
|
||||
<option value="all">{!! __('headline.all_clients') !!}</option>
|
||||
@foreach ($allClients as $client)
|
||||
<option value="{{ $client['id'] }}">{{ $client['name'] }}</option>
|
||||
@endforeach
|
||||
</select> <br/>
|
||||
|
||||
<label for="projects">{!! __('PROJECT') !!}</label> <select
|
||||
name="projects" id="projects"
|
||||
onchange="removeOptions($('select#projects option:selected').val());">
|
||||
|
||||
|
||||
<option value="all">{!! __('ALL_PROJECTS') !!}</option>
|
||||
|
||||
<optgroup>
|
||||
@php $lastClientName = ''; @endphp
|
||||
@foreach ($allProjects as $row)
|
||||
@php $currentClientName = $row['clientName']; @endphp
|
||||
@if ($currentClientName != $lastClientName)
|
||||
</optgroup><optgroup label="{{ $currentClientName }}">
|
||||
@endif
|
||||
|
||||
<option value="{{ $row['id'] }}" data-client-id="{{ $row['clientId'] }}"
|
||||
@if ($row['id'] == $values['project'])
|
||||
selected="selected"
|
||||
@endif
|
||||
>{{ $row['name'] }}</option>
|
||||
|
||||
@php $lastClientName = $row['clientName']; @endphp
|
||||
@endforeach
|
||||
</optgroup>
|
||||
</select> <br/>
|
||||
|
||||
<label for="tickets">{!! __('TICKET') !!}</label>
|
||||
<select name="tickets" id="tickets">
|
||||
|
||||
@foreach ($allTickets as $row)
|
||||
<option class="{{ $row['projectId'] }}" value="{{ $row['projectId'] }}|{{ $row['id'] }}"
|
||||
@if ($row['id'] == $values['ticket'])
|
||||
selected="selected"
|
||||
@endif
|
||||
>{{ $row['headline'] }}</option>
|
||||
@endforeach
|
||||
|
||||
</select> <br/>
|
||||
<br/>
|
||||
<label for="kind">{!! __('KIND') !!}</label> <select id="kind"
|
||||
name="kind">
|
||||
@foreach ($kind as $row)
|
||||
<option value="{{ $row }}"
|
||||
@if ($row == $values['kind'])
|
||||
selected="selected"
|
||||
@endif
|
||||
>{!! __($row) !!}</option>
|
||||
@endforeach
|
||||
|
||||
</select><br/>
|
||||
<label for="date">{!! __('DATE') !!}</label> <input type="text" autocomplete="off"
|
||||
id="date" name="date"
|
||||
value="{{ $values['date'] }}"
|
||||
size="7"/>
|
||||
<br/>
|
||||
<label for="hours">{!! __('HOURS') !!}</label> <x-global::forms.text-input
|
||||
id="hours" name="hours"
|
||||
value="{{ $values['hours'] }}" size="7" /> <br/>
|
||||
<label for="description">{!! __('DESCRIPTION') !!}</label> <x-global::forms.textarea
|
||||
rows="5" cols="50" id="description"
|
||||
name="description">{{ $values['description'] }}</x-global::forms.textarea><br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<label for="invoicedEmpl">{!! __('INVOICED') !!}</label> <input
|
||||
type="checkbox" name="invoicedEmpl" id="invoicedEmpl"
|
||||
@if (isset($values['invoicedEmpl']) && $values['invoicedEmpl'] == '1')
|
||||
checked="checked"
|
||||
@endif />
|
||||
{!! __('ONDATE') !!} <input type="text"
|
||||
id="invoicedEmplDate" name="invoicedEmplDate"
|
||||
value="{{ $values['invoicedEmplDate'] }}"
|
||||
size="7"/><br/>
|
||||
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<br/>
|
||||
<label for="invoicedComp">{!! __('INVOICED_COMP') !!}</label> <input
|
||||
type="checkbox" name="invoicedComp" id="invoicedComp"
|
||||
@if ($values['invoicedComp'] == '1')
|
||||
checked="checked"
|
||||
@endif />
|
||||
{!! __('ONDATE') !!} <input type="text" autocomplete="off"
|
||||
id="invoicedCompDate"
|
||||
name="invoicedCompDate"
|
||||
value="{{ $values['invoicedCompDate'] }}"
|
||||
size="7"/><br/>
|
||||
|
||||
<label for="paid">{!! __('labels.paid') !!}</label> <input
|
||||
type="checkbox" name="paid" id="paid"
|
||||
@if ($values['paid'] == '1')
|
||||
checked="checked"
|
||||
@endif />
|
||||
{!! __('ONDATE') !!} <input type="text" autocomplete="off"
|
||||
id="paidDate"
|
||||
name="paidDate"
|
||||
value="{{ $values['paidDate'] }}"
|
||||
size="7"/><br/>
|
||||
|
||||
|
||||
|
||||
@endif <x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('SAVE')" name="save" /> <x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('SAVE_NEW')" name="saveNew" />
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
function filterProjectsByClient() {
|
||||
var selectedClientId = jQuery('#clients option:selected').val();
|
||||
var projectSelect = jQuery('#projects');
|
||||
|
||||
// Show all projects if "all" is selected
|
||||
if (selectedClientId === 'all') {
|
||||
projectSelect.find('option').show();
|
||||
projectSelect.find('optgroup').show();
|
||||
} else {
|
||||
// Hide all options first
|
||||
projectSelect.find('option[data-client-id]').hide();
|
||||
projectSelect.find('optgroup').hide();
|
||||
|
||||
// Show only projects matching the selected client
|
||||
projectSelect.find('option[data-client-id="' + selectedClientId + '"]').show();
|
||||
projectSelect.find('option[data-client-id="' + selectedClientId + '"]').parent('optgroup').show();
|
||||
}
|
||||
|
||||
// Reset project selection to "all"
|
||||
projectSelect.val('all');
|
||||
}
|
||||
|
||||
jQuery("#date, #invoicedCompDate, #invoicedEmplDate, #paidDate").datepicker({
|
||||
|
||||
dateFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"),
|
||||
dayNames: leantime.i18n.__("language.dayNames").split(","),
|
||||
dayNamesMin: leantime.i18n.__("language.dayNamesMin").split(","),
|
||||
dayNamesShort: leantime.i18n.__("language.dayNamesShort").split(","),
|
||||
monthNames: leantime.i18n.__("language.monthNames").split(","),
|
||||
currentText: leantime.i18n.__("language.currentText"),
|
||||
closeText: leantime.i18n.__("language.closeText"),
|
||||
buttonText: leantime.i18n.__("language.buttonText"),
|
||||
isRTL: leantime.i18n.__("language.isRTL") === "true" ? 1 : 0,
|
||||
nextText: leantime.i18n.__("language.nextText"),
|
||||
prevText: leantime.i18n.__("language.prevText"),
|
||||
weekHeader: leantime.i18n.__("language.weekHeader"),
|
||||
firstDay: leantime.i18n.__("language.firstDayOfWeek"),
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
13
app/Domain/Timesheets/Templates/delTime.blade.php
Normal file
13
app/Domain/Timesheets/Templates/delTime.blade.php
Normal file
@@ -0,0 +1,13 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<h4 class="widgettitle title-light">{!! __('headlines.delete_time') !!}</h4>
|
||||
|
||||
<form method="post" action="{{ BASE_URL }}/timesheets/delTime/{{ $id }}">
|
||||
<p>{!! __('text.confirm_delete_timesheet') !!}</p><br />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.yes_delete')" name="del" />
|
||||
<x-global::forms.button tag="a" contentRole="tertiary" link="{{ session('lastPage') }}">{!! __('buttons.back') !!}</x-global::forms.button>
|
||||
</form>
|
||||
|
||||
@endsection
|
||||
184
app/Domain/Timesheets/Templates/editTime.blade.php
Normal file
184
app/Domain/Timesheets/Templates/editTime.blade.php
Normal file
@@ -0,0 +1,184 @@
|
||||
@php
|
||||
use Leantime\Core\Support\FromFormat;
|
||||
@endphp
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
function filterProjectsByClient() {
|
||||
var selectedClientId = jQuery('#clients option:selected').val();
|
||||
var projectSelect = jQuery('#projects');
|
||||
|
||||
// Show all projects if "all" is selected
|
||||
if (selectedClientId === 'all') {
|
||||
projectSelect.find('option').show();
|
||||
} else {
|
||||
// Hide all options first (except the "all" option)
|
||||
projectSelect.find('option[data-client-id]').hide();
|
||||
|
||||
// Show only projects matching the selected client
|
||||
projectSelect.find('option[data-client-id="' + selectedClientId + '"]').show();
|
||||
}
|
||||
|
||||
// Reset project selection to "all" and trigger chosen update
|
||||
projectSelect.val('all');
|
||||
projectSelect.trigger("chosen:updated");
|
||||
}
|
||||
|
||||
jQuery(document).ready(function() {
|
||||
jQuery(".client-select").chosen();
|
||||
jQuery(".project-select").chosen();
|
||||
jQuery(".ticket-select").chosen();
|
||||
|
||||
jQuery(".project-select").change(function () {
|
||||
jQuery(".ticket-select").removeAttr("selected");
|
||||
jQuery(".ticket-select").val("");
|
||||
jQuery(".ticket-select").trigger("liszt:updated");
|
||||
|
||||
jQuery(".ticket-select option").show();
|
||||
jQuery("#ticketSelect .chosen-results li").show();
|
||||
|
||||
var selectedValue = jQuery(this).find("option:selected").val();
|
||||
jQuery("#ticketSelect .chosen-results li").not(".project_" + selectedValue).hide();
|
||||
});
|
||||
|
||||
jQuery(".ticket-select").change(function () {
|
||||
var selectedValue = jQuery(this).find("option:selected").attr("data-value");
|
||||
jQuery(".project-select option[value=" + selectedValue + "]").attr("selected", "selected");
|
||||
jQuery(".project-select").trigger("liszt:updated");
|
||||
});
|
||||
|
||||
jQuery(document).ready(function ($) {
|
||||
jQuery("#datepicker, #date, #invoicedCompDate, #invoicedEmplDate, #paidDate").datepicker({
|
||||
numberOfMonths: 1,
|
||||
dateFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"),
|
||||
dayNames: leantime.i18n.__("language.dayNames").split(","),
|
||||
dayNamesMin: leantime.i18n.__("language.dayNamesMin").split(","),
|
||||
dayNamesShort: leantime.i18n.__("language.dayNamesShort").split(","),
|
||||
monthNames: leantime.i18n.__("language.monthNames").split(","),
|
||||
currentText: leantime.i18n.__("language.currentText"),
|
||||
closeText: leantime.i18n.__("language.closeText"),
|
||||
buttonText: leantime.i18n.__("language.buttonText"),
|
||||
isRTL: leantime.i18n.__("language.isRTL") === "true" ? 1 : 0,
|
||||
nextText: leantime.i18n.__("language.nextText"),
|
||||
prevText: leantime.i18n.__("language.prevText"),
|
||||
weekHeader: leantime.i18n.__("language.weekHeader"),
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<h4 class="widgettitle title-light"><span class="fa-regular fa-clock"></span> {!! __('headlines.edit_time') !!}</h4>
|
||||
<form action="{{ BASE_URL }}/timesheets/editTime/{{ (int) $_GET['id'] }}" method="post" class="editTimeModal">
|
||||
|
||||
<label for="clients">{!! __('label.client') !!}</label>
|
||||
<select name="clients" id="clients" class="client-select" onchange="filterProjectsByClient();">
|
||||
<option value="all">{!! __('headline.all_clients') !!}</option>
|
||||
@foreach ($allClients as $client)
|
||||
<option value="{{ $client['id'] }}">{{ $client['name'] }}</option>
|
||||
@endforeach
|
||||
</select> <br />
|
||||
|
||||
<label for="projects">{!! __('label.project') !!}</label>
|
||||
<select name="projects" id="projects" class="project-select">
|
||||
<option value="all">{!! __('headline.all_projects') !!}</option>
|
||||
|
||||
@foreach ($allProjects as $row)
|
||||
<option value="{{ $row['id'] }}" data-client-id="{{ $row['clientId'] }}"
|
||||
@if ($row['id'] == $values['project'])
|
||||
selected="selected"
|
||||
@endif
|
||||
>{{ $row['name'] }}</option>
|
||||
@endforeach
|
||||
</select> <br />
|
||||
|
||||
<div id="ticketSelect">
|
||||
<label for="tickets">{!! __('label.ticket') !!}</label>
|
||||
<select name="tickets" id="tickets" class="ticket-select">
|
||||
|
||||
@foreach ($allTickets as $row)
|
||||
<option class="project_{{ $row['projectId'] }}" data-value="{{ $row['projectId'] }}" value="{{ $row['id'] }}"
|
||||
@if ($row['id'] == $values['ticket'])
|
||||
selected="selected"
|
||||
@endif
|
||||
>{{ $row['headline'] }}</option>
|
||||
@endforeach
|
||||
|
||||
</select> <br />
|
||||
</div>
|
||||
<label for="kind">{!! __('label.kind') !!}</label> <select id="kind"
|
||||
name="kind">
|
||||
@foreach ($kind as $key => $row)
|
||||
<option value="{{ $key }}"
|
||||
@if ($key == $values['kind'])
|
||||
selected="selected"
|
||||
@endif
|
||||
>{!! __($row) !!}</option>
|
||||
@endforeach
|
||||
|
||||
</select><br />
|
||||
<label for="date">{!! __('label.date') !!}</label> <input type="text" autocomplete="off"
|
||||
id="datepicker" name="date" value="{{ format(value: $values['date'], fromFormat: FromFormat::DbDate)->date() }}" size="7" />
|
||||
<br />
|
||||
<label for="hours">{!! __('label.hours') !!}</label> <x-global::forms.text-input
|
||||
id="hours" name="hours"
|
||||
value="{{ $values['hours'] }}" size="7" /> <br />
|
||||
<label for="description">{!! __('label.description') !!}</label> <x-global::forms.textarea
|
||||
rows="5" cols="50" id="description" name="description">{{ $values['description'] }}</x-global::forms.textarea><br />
|
||||
|
||||
|
||||
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<input style="float:left; margin-right:5px;"
|
||||
type="checkbox" name="invoicedEmpl" id="invoicedEmpl"
|
||||
@if (isset($values['invoicedEmpl']) && $values['invoicedEmpl'] == '1')
|
||||
checked="checked"
|
||||
@endif />
|
||||
|
||||
<label for="invoicedEmpl">{!! __('label.invoiced') !!}</label>
|
||||
|
||||
{!! __('label.date') !!} <input type="text" autocomplete="off"
|
||||
id="invoicedEmplDate" name="invoicedEmplDate"
|
||||
value="{{ format(value: $values['invoicedEmplDate'], fromFormat: FromFormat::DbDate)->date() }}"
|
||||
size="7"/><br/>
|
||||
|
||||
|
||||
<br/>
|
||||
<input style="float:left; margin-right:5px;"
|
||||
type="checkbox" name="invoicedComp" id="invoicedComp"
|
||||
@if ($values['invoicedComp'] == '1')
|
||||
checked="checked"
|
||||
@endif />
|
||||
|
||||
<label for="invoicedComp">{!! __('label.invoiced_comp') !!}</label>
|
||||
{!! __('label.date') !!} <input type="text" autocomplete="off"
|
||||
id="invoicedCompDate"
|
||||
name="invoicedCompDate"
|
||||
value="{{ format(value: $values['invoicedCompDate'], fromFormat: FromFormat::DbDate)->date() }}"
|
||||
size="7"/><br/>
|
||||
|
||||
<br/>
|
||||
<input style="float:left; margin-right:5px;"
|
||||
type="checkbox" name="paid" id="paid"
|
||||
@if ($values['paid'] == '1')
|
||||
checked="checked"
|
||||
@endif />
|
||||
|
||||
<label for="paid">{!! __('label.paid') !!}</label>
|
||||
{!! __('label.date') !!} <input type="text" autocomplete="off"
|
||||
id="paidDate"
|
||||
name="paidDate"
|
||||
value="{{ format(value: $values['paidDate'], fromFormat: FromFormat::DbDate)->date() }}"
|
||||
size="7"/><br/>
|
||||
@endif
|
||||
|
||||
|
||||
|
||||
<input type="hidden" name="saveForm" value="1"/>
|
||||
<p class="stdformbutton">
|
||||
<x-global::forms.button tag="a" class="delete editTimeModal pull-right" link="{{ BASE_URL }}/timesheets/delTime/{{ $tpl->escape($_GET['id']) }}" state="danger" variant="outline">{!! __('links.delete') !!}</x-global::forms.button>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="save" />
|
||||
</p>
|
||||
</form>
|
||||
36
app/Domain/Timesheets/Templates/partials/stopwatch.blade.php
Normal file
36
app/Domain/Timesheets/Templates/partials/stopwatch.blade.php
Normal file
@@ -0,0 +1,36 @@
|
||||
@if ($login::userIsAtLeast(\Leantime\Domain\Auth\Models\Roles::$editor, true))
|
||||
|
||||
<li class='timerHeadMenu' id='timerHeadMenu' hx-get="{{BASE_URL}}/timesheets/stopwatch/get-status" hx-trigger="timerUpdate from:body{{ $onTheClock !== false ? ', every 60s' : '' }}" hx-swap="outerHTML">
|
||||
|
||||
@if ($onTheClock !== false)
|
||||
<a
|
||||
href='javascript:void(0);'
|
||||
class='dropdown-toggle'
|
||||
data-toggle='dropdown'
|
||||
>{!! sprintf(
|
||||
__('text.timer_on_todo'),
|
||||
$onTheClock['totalTime'],
|
||||
substr($onTheClock['headline'], 0, 10)
|
||||
) !!}</a>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="#/tickets/showTicket/{{ $onTheClock['id'] }}">
|
||||
{!! __('links.view_todo') !!}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
class="punchOut"
|
||||
hx-patch="{{ BASE_URL }}/hx/timesheets/stopwatch/stop-timer/"
|
||||
hx-target="#timerHeadMenu"
|
||||
hx-vals='{"ticketId": "{{ $onTheClock['id'] }}", "action":"stop"}'
|
||||
hx-swap="outerHTML"
|
||||
>{!! __('links.stop_timer') !!}</a>
|
||||
</li>
|
||||
</ul>
|
||||
@endif
|
||||
</li>
|
||||
@endif
|
||||
|
||||
367
app/Domain/Timesheets/Templates/showAll.blade.php
Normal file
367
app/Domain/Timesheets/Templates/showAll.blade.php
Normal file
@@ -0,0 +1,367 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
function filterProjectsByClient() {
|
||||
var selectedClientId = jQuery('select[name="clientId"]').val();
|
||||
var projectSelect = jQuery('select[name="project"]');
|
||||
|
||||
if (selectedClientId === '-1') {
|
||||
projectSelect.find('option[data-client-id]').show();
|
||||
} else {
|
||||
projectSelect.find('option[data-client-id]').hide();
|
||||
projectSelect.find('option[data-client-id="' + selectedClientId + '"]').show();
|
||||
}
|
||||
|
||||
if (projectSelect.find('option:selected').is(':hidden')) {
|
||||
projectSelect.val('-1');
|
||||
}
|
||||
|
||||
filterTicketsByProject();
|
||||
}
|
||||
|
||||
function filterTicketsByProject() {
|
||||
var selectedProjectId = jQuery('select[name="project"]').val();
|
||||
var ticketSelect = jQuery('select[name="ticket"]');
|
||||
|
||||
if (ticketSelect.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedProjectId === '-1') {
|
||||
ticketSelect.find('option[data-project-id]').show();
|
||||
} else {
|
||||
ticketSelect.find('option[data-project-id]').hide();
|
||||
ticketSelect.find('option[data-project-id="' + selectedProjectId + '"]').show();
|
||||
}
|
||||
|
||||
if (ticketSelect.find('option:selected').is(':hidden')) {
|
||||
ticketSelect.val('-1');
|
||||
}
|
||||
}
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
jQuery("#checkAllEmpl").change(function(){
|
||||
jQuery(".invoicedEmpl").prop('checked', jQuery(this).prop("checked"));
|
||||
if (jQuery(this).prop("checked") == true) {
|
||||
jQuery(".invoicedEmpl").attr("checked", "checked");
|
||||
jQuery(".invoicedEmpl").parent().addClass("checked");
|
||||
} else {
|
||||
jQuery(".invoicedEmpl").removeAttr("checked");
|
||||
jQuery(".invoicedEmpl").parent().removeClass("checked");
|
||||
}
|
||||
});
|
||||
|
||||
jQuery("#checkAllComp").change(function(){
|
||||
jQuery(".invoicedComp").prop('checked', jQuery(this).prop("checked"));
|
||||
if (jQuery(this).prop("checked") == true) {
|
||||
jQuery(".invoicedComp").attr("checked", "checked");
|
||||
jQuery(".invoicedComp").parent().addClass("checked");
|
||||
} else {
|
||||
jQuery(".invoicedComp").removeAttr("checked");
|
||||
jQuery(".invoicedComp").parent().removeClass("checked");
|
||||
}
|
||||
});
|
||||
|
||||
jQuery("#checkAllPaid").change(function(){
|
||||
jQuery(".paid").prop('checked', jQuery(this).prop("checked"));
|
||||
if (jQuery(this).prop("checked") == true) {
|
||||
jQuery(".paid").attr("checked", "checked");
|
||||
jQuery(".paid").parent().addClass("checked");
|
||||
} else {
|
||||
jQuery(".paid").removeAttr("checked");
|
||||
jQuery(".paid").parent().removeClass("checked");
|
||||
}
|
||||
});
|
||||
|
||||
jQuery('select[name="clientId"]').change(filterProjectsByClient);
|
||||
jQuery('select[name="project"]').change(filterTicketsByProject);
|
||||
|
||||
leantime.timesheetsController.initTimesheetsTable();
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
leantime.timesheetsController.initEditTimeModal();
|
||||
@endif
|
||||
|
||||
leantime.dateController.initDateRangePicker(".dateFrom", ".dateTo", 1)
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
<!-- page header -->
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><span class="fa-solid fa-business-time"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h1>{!! __('headlines.all_timesheets') !!}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- page header -->
|
||||
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
<form action="{{ BASE_URL }}/timesheets/showAll" method="post" id="form" name="form">
|
||||
|
||||
<div class="pull-right">
|
||||
<div id="tableButtons" style="display:inline-block"></div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="headtitle" style="">
|
||||
|
||||
<table cellpadding="10" cellspacing="0" width="90%" class="table dataTable filterTable">
|
||||
<tr>
|
||||
<td>
|
||||
<label for="clients">{!! __('label.client') !!}</label>
|
||||
<select name="clientId">
|
||||
<option value="-1">{{ strip_tags(__('menu.all_clients')) }}</option>
|
||||
@foreach ($allClients as $client)
|
||||
<option value="{{ $client['id'] }}"
|
||||
@if ($clientFilter == $client['id'])
|
||||
selected="selected"
|
||||
@endif
|
||||
>{{ $client['name'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="projects">{!! __('label.project') !!}</label>
|
||||
<select name="project" style="max-width:120px;">
|
||||
<option value="-1">{{ strip_tags(__('menu.all_projects')) }}</option>
|
||||
@foreach ($allProjects as $project)
|
||||
<option value="{{ $project['id'] }}" data-client-id="{{ $project['clientId'] }}"
|
||||
@if ($projectFilter == $project['id'])
|
||||
selected="selected"
|
||||
@endif
|
||||
>{{ $project['name'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
@if (! empty($allTickets))
|
||||
<td>
|
||||
<label for="ticket">{!! __('label.ticket') !!}</label>
|
||||
<select name="ticket" style="max-width:120px;">
|
||||
<option value="-1">{{ strip_tags(__('menu.all_tickets')) }}</option>
|
||||
@foreach ($allTickets as $ticket)
|
||||
<option value="{{ $ticket['id'] }}" data-project-id="{{ $ticket['projectId'] }}"
|
||||
@if ($ticketFilter == $ticket['id'])
|
||||
selected="selected"
|
||||
@endif
|
||||
>{{ $ticket['headline'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
@endif
|
||||
|
||||
<td>
|
||||
<label for="dateFrom">{!! __('label.date_from') !!}</label>
|
||||
<input type="text" id="dateFrom" class="dateFrom" name="dateFrom" autocomplete="off"
|
||||
value="{{ format($dateFrom)->date() }}" size="5" style="max-width:100px; margin-bottom:10px"/></td>
|
||||
<td>
|
||||
<label for="dateTo">{!! __('label.date_to') !!}</label>
|
||||
<input type="text" id="dateTo" class="dateTo" name="dateTo" autocomplete="off"
|
||||
value="{{ format($dateTo)->date() }}" size="5" style="max-width:100px; margin-bottom:10px" /></td>
|
||||
<td>
|
||||
<label for="userId">{!! __('label.employee') !!}</label>
|
||||
<select name="userId" id="userId" onchange="submit();" style="max-width:120px;">
|
||||
<option value="all">{!! __('label.all_employees') !!}</option>
|
||||
|
||||
@foreach ($employees as $row)
|
||||
<option value="{{ $row['id'] }}"
|
||||
@if ($row['id'] == $employeeFilter)
|
||||
selected="selected"
|
||||
@endif
|
||||
>{{ sprintf(__('text.full_name'), $tpl->escape($row['firstname']), $tpl->escape($row['lastname'])) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="kind">{!! __('label.type') !!}</label>
|
||||
<select id="kind" name="kind" onchange="submit();" style="max-width:120px;">
|
||||
<option value="all">{!! __('label.all_types') !!}</option>
|
||||
@foreach ($kind as $key => $row)
|
||||
<option value="{{ $key }}"
|
||||
@if ($key == $actKind)
|
||||
selected="selected"
|
||||
@endif
|
||||
>{!! __($row) !!}</option>
|
||||
@endforeach
|
||||
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="invEmpl">{!! __('label.invoiced') !!}</label>
|
||||
<select name="invEmpl" id="invEmpl" style="max-width:120px;">
|
||||
<option value="all" @if ($invEmpl == 'all' || ! $invEmpl) selected="selected" @endif>{!! __('label.invoiced_all') !!}</option>
|
||||
<option value="1" @if ($invEmpl == '1') selected="selected" @endif>{!! __('label.invoiced') !!}</option>
|
||||
<option value="0" @if ($invEmpl == '0') selected="selected" @endif>{!! __('label.invoiced_not') !!}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" value="on" name="invComp" id="invComp" onclick="submit();"
|
||||
@if ($invComp == '1')
|
||||
checked="checked"
|
||||
@endif
|
||||
/>
|
||||
<label for="invEmpl">{!! __('label.invoiced_comp') !!}</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="checkbox" value="on" name="paid" id="paid" onclick="submit();"
|
||||
@if ($paid == '1')
|
||||
checked="checked"
|
||||
@endif
|
||||
/>
|
||||
<label for="paid">{!! __('label.paid') !!}</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" name='filterSubmit' value="1"/>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.search')" class="reload" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<table cellpadding="0" cellspacing="0" border="0" class="table table-bordered display" id="allTimesheetsTable">
|
||||
<colgroup>
|
||||
<col class="con0" width="100px"/>
|
||||
<col class="con1" />
|
||||
<col class="con0"/>
|
||||
<col class="con1" />
|
||||
<col class="con0"/>
|
||||
<col class="con1" />
|
||||
<col class="con0"/>
|
||||
<col class="con1" />
|
||||
<col class="con0"/>
|
||||
<col class="con1" />
|
||||
<col class="con0"/>
|
||||
<col class="con1"/>
|
||||
<col class="con0"/>
|
||||
<col class="con1"/>
|
||||
<col class="con0"/>
|
||||
<col class="con1"/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{!! __('label.id') !!}</th>
|
||||
<th>{!! __('label.date') !!}</th>
|
||||
<th>{!! __('label.hours') !!}</th>
|
||||
<th>{!! __('label.plan_hours') !!}</th>
|
||||
<th>{!! __('label.difference') !!}</th>
|
||||
<th>{!! __('label.ticket') !!}</th>
|
||||
<th>{!! __('label.project') !!}</th>
|
||||
<th>{!! __('label.client') !!}</th>
|
||||
<th>{!! __('label.employee') !!}</th>
|
||||
<th>{!! __('label.type') !!}</th>
|
||||
<th>{!! __('label.milestone') !!}</th>
|
||||
<th>{!! __('label.tags') !!}</th>
|
||||
<th>{!! __('label.description') !!}</th>
|
||||
<th>{!! __('label.invoiced') !!}</th>
|
||||
<th>{!! __('label.invoiced_comp') !!}</th>
|
||||
<th>{!! __('label.paid') !!}</th>
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@php
|
||||
$sum = 0;
|
||||
$billableSum = 0;
|
||||
@endphp
|
||||
|
||||
@foreach ($allTimesheets as $row)
|
||||
@php $sum = $sum + $row['hours']; @endphp
|
||||
<tr>
|
||||
<td data-order="{{ $row['id'] }}">
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<a href="{{ BASE_URL }}/timesheets/editTime/{{ $row['id'] }}" class="editTimeModal">#{{ $row['id'] . ' - ' . __('label.edit') }} </a>
|
||||
@else
|
||||
#{{ $row['id'] }}
|
||||
@endif
|
||||
</td>
|
||||
<td data-order="{{ $row['workDate'] }}">
|
||||
{{ format($row['workDate'])->date() }}
|
||||
</td>
|
||||
<td data-order="{{ $row['hours'] }}">{{ $row['hours'] }}</td>
|
||||
<td data-order="{{ $row['planHours'] }}">{{ $row['planHours'] }}</td>
|
||||
@php $diff = $row['planHours'] - $row['hours']; @endphp
|
||||
<td data-order="{{ $diff }}">{{ $diff }}</td>
|
||||
<td data-order="{{ $row['headline'] }}"><a href="#/tickets/showTicket/{{ $row['ticketId'] }}">{{ $row['headline'] }}</a></td>
|
||||
|
||||
<td data-order="{{ $row['name'] }}"><a href="{{ BASE_URL }}/projects/showProject/{{ $row['projectId'] }}">{{ $row['name'] }}</a></td>
|
||||
<td data-order="{{ $row['clientName'] }}"><a href="{{ BASE_URL }}/clients/showClient/{{ $row['clientId'] }}">{{ $row['clientName'] }}</a></td>
|
||||
|
||||
<td>{!! sprintf(__('text.full_name'), $tpl->escape($row['firstname']), $tpl->escape($row['lastname'])) !!}</td>
|
||||
<td>{!! __($kind[$row['kind'] ?? 'GENERAL_BILLABLE'] ?? $kind['GENERAL_BILLABLE']) !!}</td>
|
||||
|
||||
<td>{{ $row['milestone'] }}</td>
|
||||
<td>{{ $row['tags'] }}</td>
|
||||
|
||||
<td>{{ $row['description'] }}</td>
|
||||
<td data-order="@if ($row['invoicedEmpl'] == '1'){{ format($row['invoicedEmplDate'])->date() }}@endif">
|
||||
@if ($row['invoicedEmpl'] == '1')
|
||||
{{ format($row['invoicedEmplDate'])->date() }}
|
||||
@else
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<input type="checkbox" name="invoicedEmpl[]" class="invoicedEmpl"
|
||||
value="{{ $row['id'] }}" />
|
||||
@endif
|
||||
@endif
|
||||
</td>
|
||||
<td data-order="@if ($row['invoicedComp'] == '1'){{ format($row['invoicedCompDate'])->date() }}@endif">
|
||||
|
||||
@if ($row['invoicedComp'] == '1')
|
||||
{{ format($row['invoicedCompDate'])->date() }}
|
||||
@else
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<input type="checkbox" name="invoicedComp[]" class="invoicedComp" value="{{ $row['id'] }}" />
|
||||
@endif
|
||||
@endif
|
||||
</td>
|
||||
<td data-order="@if ($row['paid'] == '1'){{ format($row['paidDate'])->date() }}@endif">
|
||||
|
||||
@if ($row['paid'] == '1')
|
||||
{{ format($row['paidDate'])->date() }}
|
||||
@else
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<input type="checkbox" name="paid[]" class="paid" value="{{ $row['id'] }}" />
|
||||
@endif
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="2"><strong>{!! __('label.total_hours') !!}</strong></td>
|
||||
<td colspan="10"><strong>{{ $sum }}</strong></td>
|
||||
|
||||
<td>
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="saveInvoice" />
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<input type="checkbox" id="checkAllEmpl" style="vertical-align: baseline;"/> {!! __('label.select_all') !!}</td>
|
||||
@endif
|
||||
<td>
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<input type="checkbox" id="checkAllComp" style="vertical-align: baseline;"/> {!! __('label.select_all') !!}
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<input type="checkbox" id="checkAllPaid" style="vertical-align: baseline;"/> {!! __('label.select_all') !!}
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
394
app/Domain/Timesheets/Templates/showMy.blade.php
Normal file
394
app/Domain/Timesheets/Templates/showMy.blade.php
Normal file
@@ -0,0 +1,394 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
/** @var \Carbon\Carbon $currentDate */
|
||||
@endphp
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
var startDate;
|
||||
var endDate;
|
||||
var selectCurrentWeek = function () {
|
||||
window.setTimeout(function () {
|
||||
jQuery('.ui-weekpicker').find('.ui-datepicker-current-day a').addClass('ui-state-active').removeClass('ui-state-default');
|
||||
}, 1);
|
||||
};
|
||||
|
||||
var setDates = function (input) {
|
||||
console.log("setting dates");
|
||||
var $input = jQuery(input);
|
||||
var date = $input.datepicker('getDate');
|
||||
|
||||
if (date !== null) {
|
||||
var firstDay = 1
|
||||
var dayAdjustment = date.getDay() - firstDay;
|
||||
if (dayAdjustment < 0) {
|
||||
dayAdjustment += 7;
|
||||
}
|
||||
startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - dayAdjustment);
|
||||
endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - dayAdjustment + 6);
|
||||
|
||||
var inst = $input.data('datepicker');
|
||||
var dateFormat = inst.settings.dateFormat || jQuery.datepicker._defaults.dateFormat;
|
||||
jQuery('#startDate').datepicker("setDate", startDate);
|
||||
jQuery('#endDate').datepicker("setDate", endDate);
|
||||
jQuery('#startDate').val(jQuery.datepicker.formatDate(dateFormat, startDate, inst.settings));
|
||||
jQuery('#endDate').val(jQuery.datepicker.formatDate(dateFormat, endDate, inst.settings));
|
||||
}
|
||||
};
|
||||
|
||||
jQuery('.week-picker').datepicker({
|
||||
dateFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"),
|
||||
dayNames: leantime.i18n.__("language.dayNames").split(","),
|
||||
dayNamesMin: leantime.i18n.__("language.dayNamesMin").split(","),
|
||||
dayNamesShort: leantime.i18n.__("language.dayNamesShort").split(","),
|
||||
monthNames: leantime.i18n.__("language.monthNames").split(","),
|
||||
monthNamesShort: leantime.i18n.__("language.monthNamesShort").split(","),
|
||||
currentText: leantime.i18n.__("language.currentText"),
|
||||
closeText: leantime.i18n.__("language.closeText"),
|
||||
buttonText: leantime.i18n.__("language.buttonText"),
|
||||
isRTL: leantime.i18n.__("language.isRTL") === "true" ? 1 : 0,
|
||||
nextText: leantime.i18n.__("language.nextText"),
|
||||
prevText: leantime.i18n.__("language.prevText"),
|
||||
weekHeader: leantime.i18n.__("language.weekHeader"),
|
||||
firstDay: 1,
|
||||
autoSize: true,
|
||||
navigationAsDateFormat: true,
|
||||
beforeShow: function () {
|
||||
jQuery('#ui-datepicker-div').addClass('ui-weekpicker');
|
||||
selectCurrentWeek();
|
||||
},
|
||||
onClose: function () {
|
||||
jQuery('#ui-datepicker-div').removeClass('ui-weekpicker');
|
||||
},
|
||||
showOtherMonths: true,
|
||||
selectOtherMonths: true,
|
||||
onSelect: function (dateText, inst) {
|
||||
|
||||
setDates(this);
|
||||
selectCurrentWeek();
|
||||
jQuery(this).change();
|
||||
jQuery("#timesheetList").submit();
|
||||
},
|
||||
beforeShowDay: function (date) {
|
||||
var cssClass = '';
|
||||
if (date >= startDate && date <= endDate)
|
||||
cssClass = 'ui-datepicker-current-day';
|
||||
return [true, cssClass];
|
||||
},
|
||||
onChangeMonthYear: function (year, month, inst) {
|
||||
selectCurrentWeek();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
var $calendarTR = jQuery('.ui-weekpicker .ui-datepicker-calendar tr');
|
||||
$calendarTR.on('mousemove', function () {
|
||||
jQuery(this).find('td a').addClass('ui-state-hover');
|
||||
});
|
||||
$calendarTR.on('mouseleave', function () {
|
||||
jQuery(this).find('td a').removeClass('ui-state-hover');
|
||||
});
|
||||
|
||||
jQuery(".project-select").chosen();
|
||||
jQuery(".ticket-select").chosen();
|
||||
jQuery(".project-select").change(function(){
|
||||
jQuery(".ticket-select").removeAttr("selected");
|
||||
jQuery(".ticket-select").val("");
|
||||
jQuery(".ticket-select").trigger("liszt:updated");
|
||||
|
||||
jQuery(".ticket-select option").show();
|
||||
jQuery("#ticketSelect .chosen-results li").show();
|
||||
var selectedValue = jQuery(this).find("option:selected").val();
|
||||
jQuery(".ticket-select option").not(".project_"+selectedValue).hide();
|
||||
jQuery("#ticketSelect .chosen-results li").not(".project_"+selectedValue).hide();
|
||||
jQuery(".ticket-select").chosen("destroy").chosen();
|
||||
});
|
||||
|
||||
jQuery(".ticket-select").change(function() {
|
||||
var selectedValue = jQuery(this).find("option:selected").attr("data-value");
|
||||
jQuery(".project-select option[value="+selectedValue+"]").attr("selected", "selected");
|
||||
jQuery(".project-select").trigger("liszt:updated");
|
||||
jQuery(".ticket-select").chosen("destroy").chosen();
|
||||
});
|
||||
|
||||
jQuery("#nextWeek").click(function() {
|
||||
var date = jQuery("#endDate").datepicker('getDate');
|
||||
var endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 7);
|
||||
var startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1);
|
||||
|
||||
var inst = jQuery("#endDate").data('datepicker');
|
||||
var dateFormat = inst.settings.dateFormat || jQuery.datepicker._defaults.dateFormat;
|
||||
|
||||
jQuery('#startDate').val(jQuery.datepicker.formatDate(dateFormat, startDate, inst.settings));
|
||||
jQuery('#endDate').val(jQuery.datepicker.formatDate(dateFormat, endDate, inst.settings));
|
||||
jQuery("#timesheetList").submit();
|
||||
});
|
||||
|
||||
jQuery("#prevWeek").click(function() {
|
||||
|
||||
var date = jQuery("#startDate").datepicker('getDate');
|
||||
var endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1);
|
||||
var startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - 7);
|
||||
|
||||
var inst = jQuery("#startDate").data('datepicker');
|
||||
var dateFormat = inst.settings.dateFormat || jQuery.datepicker._defaults.dateFormat;
|
||||
jQuery('#startDate').val(jQuery.datepicker.formatDate(dateFormat, startDate, inst.settings));
|
||||
jQuery('#endDate').val(jQuery.datepicker.formatDate(dateFormat, endDate, inst.settings));
|
||||
jQuery("#timesheetList").submit();
|
||||
});
|
||||
|
||||
jQuery(".timesheetTable input").change(function(){
|
||||
let colSum1 = 0; let colSum2 = 0; let colSum3 = 0; let colSum4 = 0;
|
||||
let colSum5 = 0; let colSum6 = 0; let colSum7 = 0;
|
||||
|
||||
jQuery(".timesheetRow").each(function(i){
|
||||
var rowSum = 0;
|
||||
jQuery(this).find("input.hourCell").each(function(){
|
||||
var currentValue = parseFloat(jQuery(this).val());
|
||||
rowSum = Math.round((rowSum + currentValue)*100)/100;
|
||||
var currentClass = jQuery(this).parent().attr('class');
|
||||
if(currentClass.indexOf("rowday1") > -1){ colSum1 = colSum1 + currentValue; }
|
||||
if(currentClass.indexOf("rowday2") > -1){ colSum2 = colSum2 + currentValue; }
|
||||
if(currentClass.indexOf("rowday3") > -1){ colSum3 = colSum3 + currentValue; }
|
||||
if(currentClass.indexOf("rowday4") > -1){ colSum4 = colSum4 + currentValue; }
|
||||
if(currentClass.indexOf("rowday5") > -1){ colSum5 = colSum5 + currentValue; }
|
||||
if(currentClass.indexOf("rowday6") > -1){ colSum6 = colSum6 + currentValue; }
|
||||
if(currentClass.indexOf("rowday7") > -1){ colSum7 = colSum7 + currentValue; }
|
||||
});
|
||||
jQuery(this).find(".rowSum strong").text(rowSum);
|
||||
});
|
||||
|
||||
jQuery("#day1").text(colSum1.toFixed(2)); jQuery("#day2").text(colSum2.toFixed(2));
|
||||
jQuery("#day3").text(colSum3.toFixed(2)); jQuery("#day4").text(colSum4.toFixed(2));
|
||||
jQuery("#day5").text(colSum5.toFixed(2)); jQuery("#day6").text(colSum6.toFixed(2));
|
||||
jQuery("#day7").text(colSum7.toFixed(2));
|
||||
|
||||
var finalSum = colSum1 + colSum2 + colSum3 + colSum4 + colSum5 + colSum6 + colSum7;
|
||||
var roundedSum = Math.round((finalSum)*100)/100;
|
||||
jQuery("#finalSum").text(roundedSum);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
<!-- page header -->
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><span class="fa-regular fa-clock"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{!! __('headline.overview') !!}</h5>
|
||||
<h1>{!! __('headline.my_timesheets') !!}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- page header -->
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<form action="{{ BASE_URL }}/timesheets/showMy" method="post" id="timesheetList">
|
||||
<div class="btn-group viewDropDown pull-right">
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown">
|
||||
{!! __('links.week_view') !!} {!! __('links.view') !!}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{{ BASE_URL }}/timesheets/showMy" class="active">{!! __('links.week_view') !!}</a></li>
|
||||
<li><a href="{{ BASE_URL }}/timesheets/showMyList" >{!! __('links.list_view') !!}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pull-left" style="padding-left:5px; margin-top:-3px;">
|
||||
|
||||
<div class="padding-top-sm">
|
||||
<span>{!! __('label.week_from') !!}</span>
|
||||
<a href="javascript:void(0)" style="font-size:16px;" id="prevWeek"><i class="fa fa-chevron-left"></i></a>
|
||||
<input type="text" class="week-picker" name="startDate" autocomplete="off" id="startDate" placeholder="{{ __('language.dateformat') }}" value="{{ $dateFrom->formatDateForUser() }}" style="margin-top:5px;"/>
|
||||
{!! __('label.until') !!}
|
||||
<input type="text" class="week-picker" name="endDate" autocomplete="off" id="endDate" placeholder="{{ __('language.dateformat') }}" value="{{ $dateFrom->addDays(6)->formatDateForUser() }}" style="margin-top:6px;"/>
|
||||
<a href="javascript:void(0)" style="font-size:16px;" id="nextWeek"><i class="fa fa-chevron-right"></i></a>
|
||||
<input type="hidden" name="search" value="1" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<table cellpadding="0" width="100%" class="table table-bordered display timesheetTable" id="dyntableX">
|
||||
<colgroup>
|
||||
<col class="con0" >
|
||||
<col class="con1" >
|
||||
<col class="con0" >
|
||||
<col class="con1" >
|
||||
<col class="con0" >
|
||||
<col class="con1" >
|
||||
<col class="con0" >
|
||||
<col class="con1" >
|
||||
<col class="con0" >
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
</colgroup>
|
||||
<thead>
|
||||
@php
|
||||
$days = explode(',', __('language.dayNamesShort'));
|
||||
$days[] = array_shift($days);
|
||||
@endphp
|
||||
<tr>
|
||||
<th>{!! __('label.client_product') !!}</th>
|
||||
<th>{!! __('subtitles.todo') !!}</th>
|
||||
<th>{!! __('label.type') !!}</th>
|
||||
@php $i = 0; @endphp
|
||||
@foreach ($days as $day)
|
||||
<th class="@if ($dateFrom->addDays($i)->setToUserTimezone()->isToday()) active @endif">{{ $day }}<br />
|
||||
{{ $dateFrom->addDays($i)->formatDateForUser() }}
|
||||
@php $i++; @endphp
|
||||
</th>
|
||||
@endforeach
|
||||
<th>{!! __('label.total') !!}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@php
|
||||
$colSum = [
|
||||
'day1' => 0, 'day2' => 0, 'day3' => 0, 'day4' => 0,
|
||||
'day5' => 0, 'day6' => 0, 'day7' => 0,
|
||||
];
|
||||
@endphp
|
||||
@foreach ($allTimesheets as $timeRow)
|
||||
@php $timesheetId = 'new'; @endphp
|
||||
<tr class="gradeA timesheetRow">
|
||||
<td width="14%">{{ $timeRow['clientName'] }} // {{ $timeRow['name'] }}</td>
|
||||
<td width="14%">
|
||||
<a href="#/tickets/showTicket/{{ $timeRow['ticketId'] }}">{{ $timeRow['headline'] }}</a>
|
||||
</td>
|
||||
<td width="10%">
|
||||
{!! __($kind[$timeRow['kind'] ?? 'GENERAL_BILLABLE'] ?? $kind['GENERAL_BILLABLE']) !!}
|
||||
@if ($timeRow['hasTimesheetOffset'])
|
||||
<i class="fa-solid fa-clock-rotate-left pull-right label-blue"
|
||||
data-tippy-content="This entry was likely created using a different timezone. Only existing entries can be updated in this timezone">
|
||||
</i>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
@foreach (array_keys($timeRow) as $dayKey)
|
||||
@if (str_starts_with($dayKey, 'day'))
|
||||
@php $colSum[$dayKey] = ($colSum[$dayKey] ?? 0) + $timeRow[$dayKey]['hours']; @endphp
|
||||
|
||||
<td width="7%" class="row{{ $dayKey }}@if ($timeRow[$dayKey]['start']->setToUserTimezone()->isToday()) active @endif">
|
||||
|
||||
@php
|
||||
$inputNameKey = $timeRow['ticketId'] . '|' . $timeRow['kind'] . '|' . ($timeRow[$dayKey]['actualWorkDate'] ? $timeRow[$dayKey]['actualWorkDate']->formatDateForUser() : 'false') . '|' . ($timeRow[$dayKey]['actualWorkDate'] ? $timeRow[$dayKey]['actualWorkDate']->getTimestamp() : 'false');
|
||||
@endphp
|
||||
<input type="text"
|
||||
class="hourCell"
|
||||
@if (empty($timeRow[$dayKey]['actualWorkDate']))
|
||||
disabled="disabled"
|
||||
@endif
|
||||
name="{{ $inputNameKey }}"
|
||||
value="{{ $timeRow[$dayKey]['hours'] }}"
|
||||
@if (empty($timeRow[$dayKey]['actualWorkDate']))
|
||||
data-tippy-content="Cannot add time entry in previous timezone"
|
||||
@endif
|
||||
/>
|
||||
|
||||
@if (! empty($timeRow[$dayKey]['description']))
|
||||
<i class="fa fa-circle-info" data-tippy-content="{{ $tpl->escape($timeRow[$dayKey]['description']) }}"></i>
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
<td width="7%" class="rowSum"><strong>{{ $timeRow['rowSum'] }}</strong></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
<!-- Row to add new time registration -->
|
||||
<tr class="gradeA timesheetRow">
|
||||
<td width="14%">
|
||||
<div class="form-group" id="projectSelect">
|
||||
<select data-placeholder="{{ __('input.placeholders.choose_project') }}" style="" class="project-select" >
|
||||
<option value=""></option>
|
||||
@foreach ($allProjects as $projectRow)
|
||||
{!! sprintf(
|
||||
$tpl->dispatchTplFilter(
|
||||
'client_product_format',
|
||||
'<option value="%s">%s / %s</option>'
|
||||
),
|
||||
...$tpl->dispatchTplFilter(
|
||||
'client_product_values',
|
||||
[
|
||||
$projectRow['id'],
|
||||
$tpl->escape($projectRow['clientName']),
|
||||
$tpl->escape($projectRow['name']),
|
||||
]
|
||||
)
|
||||
) !!}
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td width="14%">
|
||||
<div class="form-group" id="ticketSelect">
|
||||
<select data-placeholder="{{ __('input.placeholders.choose_todo') }}" style="" class="ticket-select" name="ticketId">
|
||||
<option value=""></option>
|
||||
@foreach ($allTickets as $ticketRow)
|
||||
@if (in_array($ticketRow['id'], $existingTicketIds))
|
||||
@continue
|
||||
@endif
|
||||
{!! sprintf(
|
||||
$tpl->dispatchTplFilter(
|
||||
'todo_format',
|
||||
'<option value="%1$s" data-value="%2$s" class="project_%2$s">%1$s / %3$s</option>'
|
||||
),
|
||||
...$tpl->dispatchTplFilter(
|
||||
'todo_values',
|
||||
[
|
||||
$ticketRow['id'],
|
||||
$ticketRow['projectId'],
|
||||
$tpl->escape($ticketRow['headline']),
|
||||
]
|
||||
)
|
||||
) !!}
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td width="14%">
|
||||
<select class="kind-select" name="kindId">
|
||||
@foreach ($kind as $key => $kindRow)
|
||||
<option value="{{ $key }}">{!! __($kindRow) !!}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
|
||||
@php $i = 0; @endphp
|
||||
@foreach ($days as $day)
|
||||
<td width="7%" class="rowday{{ $i + 1 }}@if ($dateFrom->addDays($i)->setToUserTimezone()->isToday()) active @endif">
|
||||
<input type="text" class="hourCell" name="new|GENERAL_BILLABLE|{{ $dateFrom->addDays($i)->formatDateForUser() }}|{{ $dateFrom->addDays($i)->getTimestamp() }}" value="0" />
|
||||
</td>
|
||||
@php $i++; @endphp
|
||||
@endforeach
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tfoot>
|
||||
<tr style="font-weight:bold;">
|
||||
<td colspan="3">{!! __('label.total') !!}</td>
|
||||
@php $totalHours = 0; @endphp
|
||||
@foreach ($colSum as $key => $col)
|
||||
@php $totalHours += $col; @endphp
|
||||
<td id="{{ $key }}">{{ $col }}</td>
|
||||
@endforeach
|
||||
<td id="finalSum">{{ $totalHours }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div class="right">
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" labelText="Save" name="saveTimeSheet" class="saveTimesheetBtn" />
|
||||
</div>
|
||||
<div class="clearall"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
191
app/Domain/Timesheets/Templates/showMyList.blade.php
Normal file
191
app/Domain/Timesheets/Templates/showMyList.blade.php
Normal file
@@ -0,0 +1,191 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
use Leantime\Core\Support\FromFormat;
|
||||
@endphp
|
||||
|
||||
<!-- page header -->
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><span class="fa-regular fa-clock"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{!! __('headline.overview') !!}</h5>
|
||||
<h1>{!! __('headline.my_timesheets') !!}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- page header -->
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<form action="{{ BASE_URL }}/timesheets/showMyList" method="post" id="form" name="form">
|
||||
<div class="filterWrapper tw-relative">
|
||||
<a onclick="jQuery('.filterBar').toggle();" class="btn btn-default pull-left">{!! __('links.filter') !!} (1)</a>
|
||||
<div class="filterBar" style="display:none; top:30px;">
|
||||
|
||||
<div class="filterBoxLeft">
|
||||
<label for="dateFrom">{!! __('label.date_from') !!} {!! __('label.date_to') !!}</label>
|
||||
<input type="text"
|
||||
id="dateFrom"
|
||||
class="dateFrom"
|
||||
name="dateFrom"
|
||||
value="{{ $dateFrom->formatDateForUser() }}"
|
||||
style="margin-bottom:10px; width:90px; float:left; margin-right:10px"/>
|
||||
<input type="text"
|
||||
id="dateTo"
|
||||
class="dateTo"
|
||||
name="dateTo"
|
||||
value="{{ $dateTo->formatDateForUser() }}"
|
||||
style="margin-bottom:10px; width:90px" />
|
||||
</div>
|
||||
|
||||
<div class="filterBoxLeft">
|
||||
<label for="kind">{!! __('label.type') !!}</label>
|
||||
<select id="kind" name="kind" onchange="submit();">
|
||||
<option value="all">{!! __('label.all_types') !!}</option>
|
||||
@foreach($kind as $key => $row)
|
||||
<option value="{{ $key }}" @selected($key == $actKind)>{!! __($row) !!}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="filterBoxLeft">
|
||||
<label> </label>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.search')" class="reload" />
|
||||
</div>
|
||||
<div class="clearall"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div class="btn-group viewDropDown">
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown">{!! __('links.list_view') !!} {!! __('links.view') !!}</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{{ BASE_URL }}/timesheets/showMy">{!! __('links.week_view') !!}</a></li>
|
||||
<li><a href="{{ BASE_URL }}/timesheets/showMyList" class="active">{!! __('links.list_view') !!}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pull-right" style="margin-right:3px;">
|
||||
<div id="tableButtons" style="display:inline-block"></div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<table cellpadding="0" cellspacing="0" border="0" class="table table-bordered display" id="allTimesheetsTable">
|
||||
<colgroup>
|
||||
<col class="con0" width="100px"/>
|
||||
<col class="con1" />
|
||||
<col class="con0"/>
|
||||
<col class="con1" />
|
||||
<col class="con0"/>
|
||||
<col class="con1" />
|
||||
<col class="con0"/>
|
||||
<col class="con1" />
|
||||
<col class="con0"/>
|
||||
<col class="con1" />
|
||||
<col class="con0"/>
|
||||
<col class="con1"/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{!! __('label.id') !!}</th>
|
||||
<th>{!! __('label.date') !!}</th>
|
||||
<th>{!! __('label.hours') !!}</th>
|
||||
<th>{!! __('label.plan_hours') !!}</th>
|
||||
<th>{!! __('label.difference') !!}</th>
|
||||
<th>{!! __('label.ticket') !!}</th>
|
||||
<th>{!! __('label.project') !!}</th>
|
||||
<th>{!! __('label.employee') !!}</th>
|
||||
<th>{!! __('label.type') !!}</th>
|
||||
<th>{!! __('label.description') !!}</th>
|
||||
<th>{!! __('label.invoiced') !!}</th>
|
||||
<th>{!! __('label.invoiced_comp') !!}</th>
|
||||
<th>{!! __('label.paid') !!}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@php $sum = 0; @endphp
|
||||
@foreach($allTimesheets as $row)
|
||||
@php $sum = $sum + $row['hours']; @endphp
|
||||
<tr>
|
||||
<td data-order="{{ $row['id'] }}">
|
||||
<a href="{{ BASE_URL }}/timesheets/editTime/{{ $row['id'] }}" class="editTimeModal" id="editTimesheet-{{ $row['id'] }}">#{{ $row['id'] }} - {!! __('label.edit') !!} </a></td>
|
||||
<td data-order="{{ format($row['workDate'])->isoDateTime() }}">
|
||||
{{ format($row['workDate'])->date() }}
|
||||
{{ format($row['workDate'])->time() }}
|
||||
</td>
|
||||
<td data-order="{{ $row['hours'] }}">
|
||||
{{ $row['hours'] ?: 0 }}
|
||||
</td>
|
||||
<td data-order="{{ $row['planHours'] }}">
|
||||
{{ $row['planHours'] ?: 0 }}
|
||||
</td>
|
||||
@php $diff = ($row['planHours'] ?: 0) - ($row['hours'] ?: 0); @endphp
|
||||
<td data-order="{{ $diff }}">
|
||||
{{ $diff }}
|
||||
</td>
|
||||
<td data-order="{{ $row['headline'] }}">
|
||||
<a href="#/tickets/showTicket/{{ $row['ticketId'] }}">{{ $row['headline'] }}</a>
|
||||
</td>
|
||||
<td data-order="{{ $row['name'] }}">
|
||||
<a href="{{ BASE_URL }}/projects/showProject/{{ $row['projectId'] }}">{{ $row['name'] }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ sprintf(__('text.full_name'), e($row['firstname']), e($row['lastname'])) }}
|
||||
</td>
|
||||
<td>
|
||||
{!! __($kind[$row['kind']]) !!}
|
||||
</td>
|
||||
<td>
|
||||
{{ $row['description'] }}
|
||||
</td>
|
||||
<td data-order="@if($row['invoicedEmpl'] == '1'){{ format(value: $row['invoicedEmplDate'], fromFormat: FromFormat::DbDate)->date() }}@endif">
|
||||
@if($row['invoicedEmpl'] == '1')
|
||||
{{ format(value: $row['invoicedEmplDate'], fromFormat: FromFormat::DbDate)->date() }}
|
||||
@else
|
||||
{!! __('label.pending') !!}
|
||||
@endif
|
||||
</td>
|
||||
<td data-order="@if($row['invoicedComp'] == '1'){{ format(value: $row['invoicedCompDate'], fromFormat: FromFormat::DbDate)->date() }}@endif">
|
||||
@if($row['invoicedComp'] == '1')
|
||||
{{ format(value: $row['invoicedCompDate'], fromFormat: FromFormat::DbDate)->date() }}
|
||||
@else
|
||||
{!! __('label.pending') !!}
|
||||
@endif
|
||||
</td>
|
||||
<td data-order="@if($row['paid'] == '1'){{ format(value: $row['paidDate'], fromFormat: FromFormat::DbDate)->date() }}@endif">
|
||||
@if($row['paid'] == '1')
|
||||
{{ format(value: $row['paidDate'], fromFormat: FromFormat::DbDate)->date() }}
|
||||
@else
|
||||
{!! __('label.pending') !!}
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td colspan="1"><strong>{!! __('label.total_hours') !!}</strong></td>
|
||||
<td colspan="11"><strong>{{ $sum }}</strong></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function(){
|
||||
leantime.timesheetsController.initTimesheetsTable();
|
||||
leantime.timesheetsController.initEditTimeModal();
|
||||
leantime.dateController.initDateRangePicker(".dateFrom", ".dateTo", 1);
|
||||
});
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
145
app/Domain/Timesheets/Tools/GetProjectTimesheetsTool.php
Normal file
145
app/Domain/Timesheets/Tools/GetProjectTimesheetsTool.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Tools;
|
||||
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Auth\Models\Roles;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets;
|
||||
|
||||
/**
|
||||
* Get timesheet entries for an entire project.
|
||||
*/
|
||||
#[IsReadOnly]
|
||||
class GetProjectTimesheetsTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Timesheets $timesheetsService,
|
||||
private Projects $projectsService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->integer('projectId')->description('Project ID to get timesheets for.')
|
||||
->required()
|
||||
->string('dateFrom')->description('Start date in ISO8601 format.')
|
||||
->required()
|
||||
->string('dateTo')->description('End date in ISO8601 format.')
|
||||
->required()
|
||||
->integer('userId')->description('Filter by specific user ID (optional).');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'getProjectTimesheets';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Gets timesheet entries for an entire project.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$projectId = (int) ($arguments['projectId'] ?? 0);
|
||||
|
||||
$globalRole = session('userdata.role');
|
||||
if (! in_array($globalRole, [Roles::$manager, Roles::$admin, Roles::$owner], true)) {
|
||||
// getProjectRole() returns a stored role key (usually a numeric string like "30");
|
||||
// resolve it to a role name before comparing (same pattern as
|
||||
// Tickets::userIsAtLeastForProject).
|
||||
$projectRoleKey = $this->projectsService->getProjectRole(session('userdata.id'), $projectId);
|
||||
$projectRole = ctype_digit((string) $projectRoleKey)
|
||||
? Roles::getRoleString((int) $projectRoleKey)
|
||||
: $projectRoleKey;
|
||||
|
||||
if ($projectRole !== Roles::$manager) {
|
||||
return ToolResult::error("You don't have permission to view project-wide timesheet data. This requires a manager role or above, or a manager role in this project.");
|
||||
}
|
||||
}
|
||||
|
||||
$fromDate = dtHelper()->parseUserDateTime($arguments['dateFrom']);
|
||||
$toDate = dtHelper()->parseUserDateTime($arguments['dateTo']);
|
||||
$userId = ($arguments['userId'] ?? null);
|
||||
|
||||
$timesheets = $this->timesheetsService->getAll(
|
||||
dateFrom: $fromDate,
|
||||
dateTo: $toDate,
|
||||
projectId: $projectId,
|
||||
userId: $userId
|
||||
);
|
||||
|
||||
if (empty($timesheets)) {
|
||||
return ToolResult::text('No timesheet entries found for this project in the specified date range.');
|
||||
}
|
||||
|
||||
$project = $this->projectsService->getProject($projectId);
|
||||
|
||||
$response = '## Project Timesheet Summary: '.$project['name']."\n";
|
||||
$response .= 'From: '.$fromDate->formatDateForUser().' to '.$toDate->formatDateForUser()."\n\n";
|
||||
|
||||
$totalHours = 0;
|
||||
$userTotals = [];
|
||||
$ticketTotals = [];
|
||||
$kindTotals = [];
|
||||
|
||||
foreach ($timesheets as $entry) {
|
||||
$hours = (float) $entry['hours'];
|
||||
$userName = $entry['firstname'].' '.$entry['lastname'];
|
||||
$entryUserId = $entry['userId'];
|
||||
$ticketId = $entry['ticketId'];
|
||||
$ticketTitle = $entry['headline'] ?? 'No ticket';
|
||||
$kind = $entry['kind'];
|
||||
|
||||
$totalHours += $hours;
|
||||
|
||||
if (! isset($userTotals[$entryUserId])) {
|
||||
$userTotals[$entryUserId] = [
|
||||
'name' => $userName,
|
||||
'hours' => 0,
|
||||
];
|
||||
}
|
||||
$userTotals[$entryUserId]['hours'] += $hours;
|
||||
|
||||
if (! isset($ticketTotals[$ticketId])) {
|
||||
$ticketTotals[$ticketId] = [
|
||||
'title' => $ticketTitle,
|
||||
'hours' => 0,
|
||||
];
|
||||
}
|
||||
$ticketTotals[$ticketId]['hours'] += $hours;
|
||||
|
||||
if (! isset($kindTotals[$kind])) {
|
||||
$kindTotals[$kind] = 0;
|
||||
}
|
||||
$kindTotals[$kind] += $hours;
|
||||
}
|
||||
|
||||
$response .= '### Total Hours: '.number_format($totalHours, 2)."\n\n";
|
||||
|
||||
$response .= "### Hours by Team Member\n";
|
||||
foreach ($userTotals as $user) {
|
||||
$response .= '- **'.$user['name'].'**: '.number_format($user['hours'], 2)." hours\n";
|
||||
}
|
||||
|
||||
$response .= "\n### Hours by Task\n";
|
||||
foreach ($ticketTotals as $ticketId => $ticket) {
|
||||
$response .= '- **'.$ticket['title'].'**: '.number_format($ticket['hours'], 2)." hours\n";
|
||||
}
|
||||
|
||||
$response .= "\n### Hours by Type\n";
|
||||
foreach ($kindTotals as $kindKey => $hours) {
|
||||
$kindLabel = $this->timesheetsService->getLoggableHourTypes()[$kindKey] ?? $kindKey;
|
||||
$response .= '- **'.$kindLabel.'**: '.number_format($hours, 2)." hours\n";
|
||||
}
|
||||
|
||||
return ToolResult::text($response);
|
||||
}
|
||||
}
|
||||
149
app/Domain/Timesheets/Tools/GetTimesheetSummaryTool.php
Normal file
149
app/Domain/Timesheets/Tools/GetTimesheetSummaryTool.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Tools;
|
||||
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets;
|
||||
|
||||
/**
|
||||
* Get a summary of logged hours grouped by different criteria.
|
||||
*/
|
||||
#[IsReadOnly]
|
||||
class GetTimesheetSummaryTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Timesheets $timesheetsService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->string('dateFrom')->description('Start date in ISO8601 format.')
|
||||
->required()
|
||||
->string('dateTo')->description('End date in ISO8601 format.')
|
||||
->required()
|
||||
->string('groupBy')
|
||||
->description('How to group the data (project, user, day, week, ticket, kind).')
|
||||
->required()
|
||||
->integer('projectId')->description('Project ID to filter by (optional).')
|
||||
->integer('userId')->description('User ID to filter by (optional, managers only).');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'getTimesheetSummary';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Gets a summary of logged hours grouped by different criteria.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$currentUserId = (int) session('userdata.id');
|
||||
$userRole = session('userdata.role');
|
||||
|
||||
// User ID 0 (or omitted) means the current user per server instructions
|
||||
$userId = (int) ($arguments['userId'] ?? 0) ?: $currentUserId;
|
||||
|
||||
if ($userId !== $currentUserId && ! in_array($userRole, ['admin', 'manager', 'owner'])) {
|
||||
return ToolResult::error("You don't have permission to view other users' timesheet data.");
|
||||
}
|
||||
|
||||
$fromDate = dtHelper()->parseUserDateTime($arguments['dateFrom']);
|
||||
$toDate = dtHelper()->parseUserDateTime($arguments['dateTo']);
|
||||
$groupBy = $arguments['groupBy'];
|
||||
$projectId = ($arguments['projectId'] ?? null);
|
||||
$targetUserId = $userId;
|
||||
|
||||
$timesheets = $this->timesheetsService->getAll(
|
||||
dateFrom: $fromDate,
|
||||
dateTo: $toDate,
|
||||
projectId: $projectId ?? -1,
|
||||
kind: 'all',
|
||||
userId: $targetUserId
|
||||
);
|
||||
|
||||
if (empty($timesheets)) {
|
||||
return ToolResult::text('No timesheet entries found for the specified criteria.');
|
||||
}
|
||||
|
||||
$response = "## Timesheet Summary\n";
|
||||
$response .= 'From: '.$fromDate->formatDateForUser().' to '.$toDate->formatDateForUser()."\n\n";
|
||||
|
||||
$totalHours = 0;
|
||||
$groupedData = [];
|
||||
|
||||
foreach ($timesheets as $entry) {
|
||||
$hours = (float) $entry['hours'];
|
||||
$totalHours += $hours;
|
||||
|
||||
$groupKey = '';
|
||||
$groupLabel = '';
|
||||
|
||||
switch ($groupBy) {
|
||||
case 'project':
|
||||
$groupKey = $entry['projectId'];
|
||||
$groupLabel = $entry['name'];
|
||||
break;
|
||||
case 'user':
|
||||
$groupKey = $entry['userId'];
|
||||
$groupLabel = $entry['firstname'].' '.$entry['lastname'];
|
||||
break;
|
||||
case 'day':
|
||||
$date = dtHelper()->parseDbDateTime($entry['workDate']);
|
||||
$groupKey = $date->format('Y-m-d');
|
||||
$groupLabel = $date->formatDateForUser();
|
||||
break;
|
||||
case 'week':
|
||||
$date = dtHelper()->parseDbDateTime($entry['workDate']);
|
||||
$weekStart = $date->startOfWeek()->format('Y-m-d');
|
||||
$groupKey = $weekStart;
|
||||
$groupLabel = 'Week of '.$date->startOfWeek()->formatDateForUser();
|
||||
break;
|
||||
case 'ticket':
|
||||
$groupKey = $entry['ticketId'];
|
||||
$groupLabel = $entry['headline'] ?? 'No ticket';
|
||||
break;
|
||||
case 'kind':
|
||||
$groupKey = $entry['kind'];
|
||||
$kindLabel = $this->timesheetsService->getLoggableHourTypes()[$entry['kind']] ?? $entry['kind'];
|
||||
$groupLabel = $kindLabel;
|
||||
break;
|
||||
default:
|
||||
$groupKey = 'all';
|
||||
$groupLabel = 'All Entries';
|
||||
}
|
||||
|
||||
if (! isset($groupedData[$groupKey])) {
|
||||
$groupedData[$groupKey] = [
|
||||
'label' => $groupLabel,
|
||||
'hours' => 0,
|
||||
];
|
||||
}
|
||||
$groupedData[$groupKey]['hours'] += $hours;
|
||||
}
|
||||
|
||||
uasort($groupedData, function ($a, $b) {
|
||||
return $b['hours'] <=> $a['hours'];
|
||||
});
|
||||
|
||||
$response .= '### Total Hours: '.number_format($totalHours, 2)."\n\n";
|
||||
|
||||
$response .= '### Hours by '.ucfirst($groupBy)."\n";
|
||||
foreach ($groupedData as $group) {
|
||||
$percentage = ($group['hours'] / $totalHours) * 100;
|
||||
$response .= '- **'.$group['label'].'**: '.number_format($group['hours'], 2).
|
||||
' hours ('.number_format($percentage, 1)."%)\n";
|
||||
}
|
||||
|
||||
return ToolResult::text($response);
|
||||
}
|
||||
}
|
||||
111
app/Domain/Timesheets/Tools/GetUserTimesheetsTool.php
Normal file
111
app/Domain/Timesheets/Tools/GetUserTimesheetsTool.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Tools;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets;
|
||||
|
||||
/**
|
||||
* Get timesheet entries for the current user.
|
||||
*/
|
||||
#[IsReadOnly]
|
||||
class GetUserTimesheetsTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Timesheets $timesheetsService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->string('dateFrom')->description('Start date in ISO8601 format.')
|
||||
->required()
|
||||
->string('dateTo')->description('End date in ISO8601 format.')
|
||||
->required()
|
||||
->integer('projectId')->description('Project ID to filter by (optional).');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'getUserTimesheets';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Gets timesheet entries for the current user within a specified date range.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$fromDate = dtHelper()->parseUserDateTime($arguments['dateFrom']);
|
||||
$toDate = dtHelper()->parseUserDateTime($arguments['dateTo']);
|
||||
|
||||
$userId = session('userdata.id');
|
||||
$projectId = ($arguments['projectId'] ?? null);
|
||||
|
||||
$timesheets = $this->timesheetsService->getAll(
|
||||
dateFrom: $fromDate,
|
||||
dateTo: $toDate,
|
||||
projectId: $projectId ?? -1,
|
||||
userId: $userId
|
||||
);
|
||||
|
||||
if (empty($timesheets)) {
|
||||
return ToolResult::text('No timesheet entries found for the specified date range.');
|
||||
}
|
||||
|
||||
$response = "## Your Timesheet Entries\n";
|
||||
$response .= 'From: '.$fromDate->formatDateForUser().' to '.$toDate->formatDateForUser()."\n\n";
|
||||
|
||||
$totalHours = 0;
|
||||
$projectTotals = [];
|
||||
|
||||
foreach ($timesheets as $entry) {
|
||||
$entryProjectId = $entry['projectId'];
|
||||
$projectName = $entry['name'];
|
||||
$hours = (float) $entry['hours'];
|
||||
$date = dtHelper()->parseDbDateTime($entry['workDate'])->formatDateForUser();
|
||||
$ticketTitle = $entry['headline'] ?? 'No ticket';
|
||||
$description = Str::sanitizeForLLM($entry['description'] ?? '');
|
||||
$kind = $entry['kind'];
|
||||
|
||||
$result = [
|
||||
'date' => $date,
|
||||
'project' => $projectName,
|
||||
'ticket' => $ticketTitle,
|
||||
'hours' => $hours,
|
||||
'type' => $kind,
|
||||
'description' => $description,
|
||||
];
|
||||
|
||||
$response .= Str::toMarkdown($result)."\n";
|
||||
|
||||
$totalHours += $hours;
|
||||
|
||||
if (! isset($projectTotals[$entryProjectId])) {
|
||||
$projectTotals[$entryProjectId] = [
|
||||
'name' => $projectName,
|
||||
'hours' => 0,
|
||||
];
|
||||
}
|
||||
$projectTotals[$entryProjectId]['hours'] += $hours;
|
||||
}
|
||||
|
||||
$response .= "\n## Summary\n";
|
||||
$response .= 'Total Hours: '.number_format($totalHours, 2)."\n\n";
|
||||
|
||||
$response .= "### Hours by Project\n";
|
||||
foreach ($projectTotals as $project) {
|
||||
$response .= '- **'.$project['name'].'**: '.number_format($project['hours'], 2)." hours\n";
|
||||
}
|
||||
|
||||
return ToolResult::text($response);
|
||||
}
|
||||
}
|
||||
102
app/Domain/Timesheets/Tools/GetWeeklyTimesheetsTool.php
Normal file
102
app/Domain/Timesheets/Tools/GetWeeklyTimesheetsTool.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Tools;
|
||||
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets;
|
||||
|
||||
/**
|
||||
* Get a weekly view of timesheet entries.
|
||||
*/
|
||||
#[IsReadOnly]
|
||||
class GetWeeklyTimesheetsTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Timesheets $timesheetsService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->string('weekStart')->description('Start date of the week in ISO8601 format.')
|
||||
->required()
|
||||
->integer('projectId')->description('Project ID to filter by (optional).')
|
||||
->integer('userId')->description('User ID to filter by (optional, managers only).');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'getWeeklyTimesheets';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Gets a weekly view of timesheet entries.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$currentUserId = (int) session('userdata.id');
|
||||
$userRole = session('userdata.role');
|
||||
|
||||
// User ID 0 (or omitted) means the current user per server instructions
|
||||
$userId = (int) ($arguments['userId'] ?? 0) ?: $currentUserId;
|
||||
|
||||
if ($userId !== $currentUserId && ! in_array($userRole, ['admin', 'manager', 'owner'])) {
|
||||
return ToolResult::error("You don't have permission to view other users' timesheet data.");
|
||||
}
|
||||
|
||||
$fromDate = dtHelper()->parseUserDateTime($arguments['weekStart'])->startOfWeek();
|
||||
$projectId = ($arguments['projectId'] ?? null);
|
||||
$targetUserId = $userId;
|
||||
|
||||
$timesheetGroups = $this->timesheetsService->getWeeklyTimesheets(
|
||||
projectId: $projectId ?? -1,
|
||||
fromDate: $fromDate,
|
||||
userId: $targetUserId
|
||||
);
|
||||
|
||||
if (empty($timesheetGroups)) {
|
||||
return ToolResult::text('No timesheet entries found for the specified week.');
|
||||
}
|
||||
|
||||
$weekEnd = $fromDate->addDays(6);
|
||||
$response = "## Weekly Timesheet\n";
|
||||
$response .= 'Week of '.$fromDate->formatDateForUser().' to '.$weekEnd->formatDateForUser()."\n\n";
|
||||
|
||||
$response .= "| Task | Type | Mon | Tue | Wed | Thu | Fri | Sat | Sun | Total |\n";
|
||||
$response .= "|------|------|-----|-----|-----|-----|-----|-----|-----|-------|\n";
|
||||
|
||||
$dailyTotals = [0, 0, 0, 0, 0, 0, 0];
|
||||
$grandTotal = 0;
|
||||
|
||||
foreach ($timesheetGroups as $group) {
|
||||
$row = '| '.($group['headline'] ?? 'No task').' | '.$group['kind'].' |';
|
||||
|
||||
for ($i = 1; $i <= 7; $i++) {
|
||||
$hours = $group["day{$i}"]['hours'] ?? 0;
|
||||
$row .= ' '.($hours > 0 ? number_format($hours, 1) : '-').' |';
|
||||
$dailyTotals[$i - 1] += $hours;
|
||||
}
|
||||
|
||||
$row .= ' '.number_format($group['rowSum'], 1).' |';
|
||||
$grandTotal += $group['rowSum'];
|
||||
|
||||
$response .= $row."\n";
|
||||
}
|
||||
|
||||
$response .= '| **Daily Totals** | | ';
|
||||
foreach ($dailyTotals as $total) {
|
||||
$response .= '**'.number_format($total, 1).'** | ';
|
||||
}
|
||||
$response .= '**'.number_format($grandTotal, 1)."** |\n\n";
|
||||
|
||||
return ToolResult::text($response);
|
||||
}
|
||||
}
|
||||
73
app/Domain/Timesheets/Tools/LogTimeTool.php
Normal file
73
app/Domain/Timesheets/Tools/LogTimeTool.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Tools;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets;
|
||||
|
||||
/**
|
||||
* Log time for a specific ticket.
|
||||
*/
|
||||
class LogTimeTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Timesheets $timesheetsService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->integer('ticketId')->description('ID of the ticket to log time for.')
|
||||
->required()
|
||||
->number('hours')->description('Number of hours to log.')
|
||||
->required()
|
||||
->string('date')->description('Date for the time entry in ISO8601 format.')
|
||||
->required()
|
||||
->string('kind')->description('Type of work (e.g., GENERAL_BILLABLE, DEVELOPMENT).')
|
||||
->required()
|
||||
->string('description')->description('Description of the work performed.');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'logTime';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Logs time for a specific ticket.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
try {
|
||||
$ticketId = (int) ($arguments['ticketId'] ?? 0);
|
||||
$params = [
|
||||
'date' => $arguments['date'],
|
||||
'hours' => ($arguments['hours'] ?? null),
|
||||
'kind' => $arguments['kind'],
|
||||
'description' => ($arguments['description'] ?? ''),
|
||||
];
|
||||
|
||||
$result = $this->timesheetsService->logTime($ticketId, $params);
|
||||
|
||||
if ($result) {
|
||||
$hours = ($arguments['hours'] ?? null);
|
||||
|
||||
return ToolResult::text("Time entry successfully logged: {$hours} hours on ticket #{$ticketId}.");
|
||||
}
|
||||
|
||||
return ToolResult::error('Failed to log time entry. Please check the provided information.');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error logging time: '.$e->getMessage());
|
||||
|
||||
return ToolResult::error('Failed to log time entry. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
67
app/Domain/Timesheets/Tools/StartTimerTool.php
Normal file
67
app/Domain/Timesheets/Tools/StartTimerTool.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Tools;
|
||||
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets;
|
||||
|
||||
/**
|
||||
* Start a timer for a specific duration.
|
||||
*/
|
||||
class StartTimerTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Timesheets $timesheetsService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->string('duration')->description('Duration in format like "25m" or "1h30m".')
|
||||
->required()
|
||||
->integer('taskId')->description('Task ID to associate timer with.')
|
||||
->string('type')
|
||||
->description('Timer type (work/break).');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'startTimer';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Start a timer for a specific duration.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$duration = $arguments['duration'];
|
||||
$taskId = ($arguments['taskId'] ?? null);
|
||||
$type = ($arguments['type'] ?? 'work');
|
||||
|
||||
preg_match('/^(?:(\d+)h)?(?:(\d+)m)?$/', $duration, $matches);
|
||||
$minutes = 0;
|
||||
if (! empty($matches[1])) {
|
||||
$minutes += intval($matches[1]) * 60;
|
||||
}
|
||||
if (! empty($matches[2])) {
|
||||
$minutes += intval($matches[2]);
|
||||
}
|
||||
|
||||
if ($minutes <= 0) {
|
||||
return ToolResult::error('Invalid duration format. Use format like "25m" or "1h30m".');
|
||||
}
|
||||
|
||||
if ($taskId !== null && (int) $taskId > 0) {
|
||||
$this->timesheetsService->punchIn((int) $taskId);
|
||||
}
|
||||
|
||||
return ToolResult::text("[timer startTime='".(time() + 10)."' duration='".($minutes * 60)."' type='".$type."']");
|
||||
}
|
||||
}
|
||||
50
app/Domain/Timesheets/Tools/StopTimerTool.php
Normal file
50
app/Domain/Timesheets/Tools/StopTimerTool.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Timesheets\Tools;
|
||||
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets;
|
||||
|
||||
/**
|
||||
* Stop a running timer.
|
||||
*/
|
||||
class StopTimerTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Timesheets $timesheetsService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->integer('ticketId')->description('Ticket ID to stop the timer for. Omit to stop the active timer.');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'stopTimer';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Stop a running timer.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$ticketId = ($arguments['ticketId'] ?? null);
|
||||
|
||||
if ($ticketId !== null && (int) $ticketId > 0) {
|
||||
$this->timesheetsService->punchOut((int) $ticketId);
|
||||
} else {
|
||||
$this->timesheetsService->stopActiveTimer();
|
||||
}
|
||||
|
||||
return ToolResult::text('Timer stopped successfully.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user