OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
76
app/Domain/Clients/Controllers/DelClient.php
Normal file
76
app/Domain/Clients/Controllers/DelClient.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Clients\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Clients\Permissions\ClientsPermissions;
|
||||
use Leantime\Domain\Clients\Services\Clients as ClientService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* DelClient Controller - Deleting clients.
|
||||
*/
|
||||
class DelClient extends Controller
|
||||
{
|
||||
private ClientService $clientService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(ClientService $clientService): void
|
||||
{
|
||||
$this->clientService = $clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the delete client confirmation page.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::DELETE, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->display('errors.error403', responseCode: 403);
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
|
||||
if ($this->clientService->hasTickets($id)) {
|
||||
$this->tpl->setNotification($this->language->__('notification.client_has_todos'), 'error');
|
||||
}
|
||||
|
||||
$this->tpl->assign('client', $this->clientService->get($id));
|
||||
|
||||
return $this->tpl->display('clients.delClient');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles client deletion.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::DELETE, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->display('errors.error403', responseCode: 403);
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
|
||||
if ($this->clientService->hasTickets($id)) {
|
||||
$this->tpl->setNotification($this->language->__('notification.client_has_todos'), 'error');
|
||||
$this->tpl->assign('client', $this->clientService->get($id));
|
||||
|
||||
return $this->tpl->display('clients.delClient');
|
||||
}
|
||||
|
||||
$this->clientService->delete($id);
|
||||
$this->tpl->setNotification($this->language->__('notification.client_deleted'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/clients/showAll');
|
||||
}
|
||||
}
|
||||
88
app/Domain/Clients/Controllers/EditClient.php
Normal file
88
app/Domain/Clients/Controllers/EditClient.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Clients\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Clients\Permissions\ClientsPermissions;
|
||||
use Leantime\Domain\Clients\Services\Clients as ClientService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* EditClient Controller - Editing clients.
|
||||
*/
|
||||
class EditClient extends Controller
|
||||
{
|
||||
private ClientService $clientService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(ClientService $clientService): void
|
||||
{
|
||||
$this->clientService = $clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the edit client form.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::EDIT, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->display('errors.error403', responseCode: 403);
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
$row = $this->clientService->get($id);
|
||||
|
||||
if ($row === false) {
|
||||
return $this->tpl->display('errors.error404', responseCode: 404);
|
||||
}
|
||||
|
||||
$this->tpl->assign('values', $row);
|
||||
|
||||
return $this->tpl->display('clients.editClient');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles client edit form submission.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::EDIT, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->display('errors.error403', responseCode: 403);
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
|
||||
$values = [
|
||||
'id' => $id,
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'street' => $_POST['street'] ?? '',
|
||||
'zip' => $_POST['zip'] ?? '',
|
||||
'city' => $_POST['city'] ?? '',
|
||||
'state' => $_POST['state'] ?? '',
|
||||
'country' => $_POST['country'] ?? '',
|
||||
'phone' => $_POST['phone'] ?? '',
|
||||
'internet' => $_POST['internet'] ?? '',
|
||||
'email' => $_POST['email'] ?? '',
|
||||
];
|
||||
|
||||
if ($values['name'] !== '') {
|
||||
$this->clientService->editClient($values);
|
||||
$this->tpl->setNotification('EDIT_CLIENT_SUCCESS', 'success', 'client_updated');
|
||||
} else {
|
||||
$this->tpl->setNotification('NO_NAME', 'error');
|
||||
}
|
||||
|
||||
$this->tpl->assign('values', $values);
|
||||
|
||||
return $this->tpl->display('clients.editClient');
|
||||
}
|
||||
}
|
||||
89
app/Domain/Clients/Controllers/NewClient.php
Normal file
89
app/Domain/Clients/Controllers/NewClient.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Clients\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Exceptions\EntityExistsException;
|
||||
use Leantime\Core\Exceptions\MissingParameterException;
|
||||
use Leantime\Domain\Clients\Permissions\ClientsPermissions;
|
||||
use Leantime\Domain\Clients\Services\Clients as ClientService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* NewClient Controller - Add a new client.
|
||||
*/
|
||||
class NewClient extends Controller
|
||||
{
|
||||
private ClientService $clientService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(ClientService $clientService): void
|
||||
{
|
||||
$this->clientService = $clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the new client form.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::CREATE, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$values = [
|
||||
'name' => '',
|
||||
'street' => '',
|
||||
'zip' => '',
|
||||
'city' => '',
|
||||
'state' => '',
|
||||
'country' => '',
|
||||
'phone' => '',
|
||||
'internet' => '',
|
||||
'email' => '',
|
||||
];
|
||||
|
||||
$this->tpl->assign('values', $values);
|
||||
|
||||
return $this->tpl->display('clients.newClient');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles new client form submission.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::CREATE, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$values = [
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'street' => $_POST['street'] ?? '',
|
||||
'zip' => $_POST['zip'] ?? '',
|
||||
'city' => $_POST['city'] ?? '',
|
||||
'state' => $_POST['state'] ?? '',
|
||||
'country' => $_POST['country'] ?? '',
|
||||
'phone' => $_POST['phone'] ?? '',
|
||||
'internet' => $_POST['internet'] ?? '',
|
||||
'email' => $_POST['email'] ?? '',
|
||||
];
|
||||
|
||||
try {
|
||||
$id = $this->clientService->createClient($values);
|
||||
$this->tpl->setNotification($this->language->__('notification.client_added_successfully'), 'success', 'new_client');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/clients/showClient/'.$id);
|
||||
} catch (EntityExistsException) {
|
||||
$this->tpl->setNotification($this->language->__('notification.client_exists_already'), 'error');
|
||||
} catch (MissingParameterException) {
|
||||
$this->tpl->setNotification($this->language->__('notification.client_name_not_specified'), 'error');
|
||||
}
|
||||
|
||||
$this->tpl->assign('values', $values);
|
||||
|
||||
return $this->tpl->display('clients.newClient');
|
||||
}
|
||||
}
|
||||
69
app/Domain/Clients/Controllers/RemoveUser.php
Normal file
69
app/Domain/Clients/Controllers/RemoveUser.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Clients\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Clients\Permissions\ClientsPermissions;
|
||||
use Leantime\Domain\Clients\Services\Clients as ClientService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* RemoveUser Controller - Remove user from client.
|
||||
*/
|
||||
class RemoveUser extends Controller
|
||||
{
|
||||
private ClientService $clientService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(ClientService $clientService): void
|
||||
{
|
||||
$this->clientService = $clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the remove user confirmation (no state change on GET).
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::EDIT, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$clientId = (int) ($params['id'] ?? $_GET['id'] ?? 0);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/clients/showClient/'.$clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user removal from client via POST (CSRF-protected).
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::EDIT, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$clientId = (int) ($params['id'] ?? $_POST['id'] ?? 0);
|
||||
$userId = (int) ($params['userId'] ?? $_POST['userId'] ?? 0);
|
||||
|
||||
if ($clientId === 0 || $userId === 0) {
|
||||
return $this->tpl->display('errors.error403', responseCode: 403);
|
||||
}
|
||||
|
||||
if ($this->clientService->removeUser($clientId, $userId)) {
|
||||
$this->tpl->setNotification(
|
||||
$this->language->__('notification.user_removed_from_client'),
|
||||
'success'
|
||||
);
|
||||
} else {
|
||||
$this->tpl->setNotification(
|
||||
$this->language->__('notification.error_removing_user'),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/clients/showClient/'.$clientId);
|
||||
}
|
||||
}
|
||||
42
app/Domain/Clients/Controllers/ShowAll.php
Normal file
42
app/Domain/Clients/Controllers/ShowAll.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Clients\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Clients\Permissions\ClientsPermissions;
|
||||
use Leantime\Domain\Clients\Services\Clients as ClientService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* ShowAll Controller - Show all clients.
|
||||
*/
|
||||
class ShowAll extends Controller
|
||||
{
|
||||
private ClientService $clientService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(ClientService $clientService): void
|
||||
{
|
||||
$this->clientService = $clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the list of all clients.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::VIEW, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
if (session('userdata.role') == 'admin') {
|
||||
$this->tpl->assign('admin', true);
|
||||
}
|
||||
|
||||
$this->tpl->assign('allClients', $this->clientService->getAll());
|
||||
|
||||
return $this->tpl->display('clients.showAll');
|
||||
}
|
||||
}
|
||||
161
app/Domain/Clients/Controllers/ShowClient.php
Normal file
161
app/Domain/Clients/Controllers/ShowClient.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Clients\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Exceptions\MissingParameterException;
|
||||
use Leantime\Domain\Clients\Permissions\ClientsPermissions;
|
||||
use Leantime\Domain\Clients\Services\Clients as ClientService;
|
||||
use Leantime\Domain\Comments\Services\Comments as CommentService;
|
||||
use Leantime\Domain\Files\Services\Files as FileService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* ShowClient Controller - Show one client.
|
||||
*/
|
||||
class ShowClient extends Controller
|
||||
{
|
||||
private ClientService $clientService;
|
||||
|
||||
private CommentService $commentService;
|
||||
|
||||
private FileService $fileService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(
|
||||
ClientService $clientService,
|
||||
CommentService $commentService,
|
||||
FileService $fileService
|
||||
): void {
|
||||
$this->clientService = $clientService;
|
||||
$this->commentService = $commentService;
|
||||
$this->fileService = $fileService;
|
||||
|
||||
if (! session()->exists('lastPage')) {
|
||||
session(['lastPage' => BASE_URL.'/clients/showAll']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the client detail page.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::VIEW, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->display('errors.error404', responseCode: 404);
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
$client = $this->clientService->get($id);
|
||||
|
||||
if ($client === false) {
|
||||
return $this->tpl->display('errors.error404', responseCode: 404);
|
||||
}
|
||||
|
||||
// Handle file deletion via GET param
|
||||
if (isset($_GET['delFile'])) {
|
||||
$result = $this->fileService->deleteFile($_GET['delFile']);
|
||||
|
||||
if ($result === true) {
|
||||
$this->tpl->setNotification($this->language->__('notifications.file_deleted'), 'success', 'clientfile_deleted');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/clients/showClient/'.$id.'#files');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notifications.file_deleted_error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
if (session('userdata.role') == 'admin') {
|
||||
$this->tpl->assign('admin', true);
|
||||
}
|
||||
|
||||
$this->tpl->assign('client', $client);
|
||||
|
||||
$pageData = $this->clientService->getClientPageData($id);
|
||||
array_map([$this->tpl, 'assign'], array_keys($pageData), array_values($pageData));
|
||||
|
||||
return $this->tpl->display('clients.showClient');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles client detail form submissions (save, upload, comment).
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::EDIT, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->display('errors.error404', responseCode: 404);
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
$client = $this->clientService->get($id);
|
||||
|
||||
if ($client === false) {
|
||||
return $this->tpl->display('errors.error404', responseCode: 404);
|
||||
}
|
||||
|
||||
// Handle file upload
|
||||
if (isset($_POST['upload'])) {
|
||||
if (isset($_FILES['file']) && $_FILES['file']['tmp_name'] != '') {
|
||||
$this->fileService->upload($_FILES, 'client', $id);
|
||||
$this->tpl->setNotification($this->language->__('notifications.file_upload_success'), 'success', 'clientfile_uploaded');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notifications.file_upload_error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle comment
|
||||
if (isset($_POST['comment'])) {
|
||||
if ($this->commentService->addComment($_POST, 'client', $id, $client)) {
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_create_success'), 'success');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_create_error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle client save
|
||||
if (isset($_POST['save'])) {
|
||||
$values = [
|
||||
'id' => $id,
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'street' => $_POST['street'] ?? '',
|
||||
'zip' => $_POST['zip'] ?? '',
|
||||
'city' => $_POST['city'] ?? '',
|
||||
'state' => $_POST['state'] ?? '',
|
||||
'country' => $_POST['country'] ?? '',
|
||||
'phone' => $_POST['phone'] ?? '',
|
||||
'internet' => $_POST['internet'] ?? '',
|
||||
'email' => $_POST['email'] ?? '',
|
||||
];
|
||||
|
||||
try {
|
||||
$this->clientService->updateClient($values);
|
||||
$this->tpl->setNotification($this->language->__('notification.client_saved_successfully'), 'success');
|
||||
} catch (MissingParameterException) {
|
||||
$this->tpl->setNotification($this->language->__('notification.client_name_not_specified'), 'error');
|
||||
}
|
||||
|
||||
$client = $values;
|
||||
}
|
||||
|
||||
if (session('userdata.role') == 'admin') {
|
||||
$this->tpl->assign('admin', true);
|
||||
}
|
||||
|
||||
$this->tpl->assign('client', $client);
|
||||
|
||||
$pageData = $this->clientService->getClientPageData($id);
|
||||
array_map([$this->tpl, 'assign'], array_keys($pageData), array_values($pageData));
|
||||
|
||||
return $this->tpl->display('clients.showClient');
|
||||
}
|
||||
}
|
||||
77
app/Domain/Clients/Js/clientsController.js
Normal file
77
app/Domain/Clients/Js/clientsController.js
Normal file
@@ -0,0 +1,77 @@
|
||||
leantime.clientsController = (function () {
|
||||
|
||||
|
||||
//Functions
|
||||
|
||||
var initDates = function () {
|
||||
|
||||
jQuery(".projectDateFrom, .projectDateTo").datepicker(
|
||||
{
|
||||
dateFormat: leantime.i18n.__("language.dateformat"),
|
||||
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"),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var initClientTabs = function () {
|
||||
jQuery('.clientTabs').tabs();
|
||||
};
|
||||
|
||||
var initClientTable = function () {
|
||||
|
||||
jQuery(document).ready(function () {
|
||||
|
||||
var size = 100;
|
||||
|
||||
var allProjects = jQuery("#allClientsTable").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"),
|
||||
}
|
||||
|
||||
},
|
||||
"dom": '<"top">rt<"bottom"ilp><"clear">',
|
||||
"searching": false,
|
||||
"displayLength":100
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Make public what you want to have public, everything else is private
|
||||
return {
|
||||
initDates:initDates,
|
||||
initClientTabs:initClientTabs,
|
||||
initClientTable:initClientTable
|
||||
};
|
||||
})();
|
||||
50
app/Domain/Clients/Permissions/ClientsPermissions.php
Normal file
50
app/Domain/Clients/Permissions/ClientsPermissions.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Clients\Permissions;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\Permission;
|
||||
use Leantime\Core\Auth\Permissions\ProvidesPermissions;
|
||||
|
||||
/**
|
||||
* The Clients (company account management) permission vocabulary — the verbs only.
|
||||
*
|
||||
* Declares *what* can be done with clients; it says nothing about *which roles* may do it
|
||||
* (role assignment is centrally owned — see {@see \Leantime\Core\Auth\Permissions\DefaultRolePermissions}).
|
||||
*
|
||||
* Every client capability is COMPANY-WIDE, not project-scoped: clients are a company resource,
|
||||
* so authority is the user's GLOBAL role (see {@see \Leantime\Core\Auth\RoleResolver}). Each
|
||||
* Permission below is constructed with `projectScoped = false`, and call sites gate with
|
||||
* `#[RequiresPermission(..., global: true)]`.
|
||||
*
|
||||
* admin + owner hold all `clients.*` via the company-wide wildcard in DefaultRolePermissions;
|
||||
* lower roles hold none — matching today's behavior, where all client management is admin+.
|
||||
*/
|
||||
final class ClientsPermissions implements ProvidesPermissions
|
||||
{
|
||||
/** View the client roster / read a client. */
|
||||
public const VIEW = 'clients.view';
|
||||
|
||||
/** Create clients. */
|
||||
public const CREATE = 'clients.create';
|
||||
|
||||
/** Edit a client (and manage its user assignments). */
|
||||
public const EDIT = 'clients.edit';
|
||||
|
||||
/** Delete clients (cascades to their projects). */
|
||||
public const DELETE = 'clients.delete';
|
||||
|
||||
public function domain(): string
|
||||
{
|
||||
return 'clients';
|
||||
}
|
||||
|
||||
public function permissions(): array
|
||||
{
|
||||
return [
|
||||
new Permission(self::VIEW, 'View clients', false),
|
||||
new Permission(self::CREATE, 'Create clients', false),
|
||||
new Permission(self::EDIT, 'Edit clients', false),
|
||||
new Permission(self::DELETE, 'Delete clients', false),
|
||||
];
|
||||
}
|
||||
}
|
||||
212
app/Domain/Clients/Repositories/Clients.php
Normal file
212
app/Domain/Clients/Repositories/Clients.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Client class - All data access for clients
|
||||
*/
|
||||
|
||||
namespace Leantime\Domain\Clients\Repositories;
|
||||
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Leantime\Core\Db\DatabaseHelper;
|
||||
use Leantime\Core\Db\Db as DbCore;
|
||||
use Leantime\Core\Db\Repository;
|
||||
|
||||
class Clients extends Repository
|
||||
{
|
||||
public string $name;
|
||||
|
||||
protected string $entity = 'clients';
|
||||
|
||||
public int $id;
|
||||
|
||||
private ConnectionInterface $db;
|
||||
|
||||
private DatabaseHelper $dbHelper;
|
||||
|
||||
/**
|
||||
* __construct - get database connection
|
||||
*/
|
||||
public function __construct(DbCore $db, DatabaseHelper $dbHelper)
|
||||
{
|
||||
$this->db = $db->getConnection();
|
||||
$this->dbHelper = $dbHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* getClient - get one client from db
|
||||
*/
|
||||
public function getClient(int|string $id): array|false
|
||||
{
|
||||
$result = $this->db->table('zp_clients')
|
||||
->select(
|
||||
'zp_clients.id',
|
||||
'zp_clients.name',
|
||||
'zp_clients.street',
|
||||
'zp_clients.zip',
|
||||
'zp_clients.city',
|
||||
'zp_clients.state',
|
||||
'zp_clients.country',
|
||||
'zp_clients.phone',
|
||||
'zp_clients.internet',
|
||||
'zp_clients.email'
|
||||
)
|
||||
->selectRaw('COUNT('.$this->dbHelper->wrapColumn('zp_projects.clientId').') AS '.$this->dbHelper->wrapColumn('numberOfProjects'))
|
||||
->leftJoin('zp_projects', 'zp_clients.id', '=', 'zp_projects.clientId')
|
||||
->where('zp_clients.id', $id)
|
||||
->groupBy(
|
||||
'zp_clients.id',
|
||||
'zp_clients.name',
|
||||
'zp_clients.street',
|
||||
'zp_clients.zip',
|
||||
'zp_clients.city',
|
||||
'zp_clients.state',
|
||||
'zp_clients.country',
|
||||
'zp_clients.phone',
|
||||
'zp_clients.internet',
|
||||
'zp_clients.email'
|
||||
)
|
||||
->orderBy('zp_clients.name')
|
||||
->limit(1)
|
||||
->first();
|
||||
|
||||
if ($result !== null) {
|
||||
$row = (array) $result;
|
||||
$this->name = $row['name'];
|
||||
$this->id = $row['id'];
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* getAll - get all clients
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
$results = $this->db->table('zp_clients')
|
||||
->select(
|
||||
'zp_clients.id',
|
||||
'zp_clients.name',
|
||||
'zp_clients.internet'
|
||||
)
|
||||
->selectRaw('COUNT('.$this->dbHelper->wrapColumn('zp_projects.clientId').') AS '.$this->dbHelper->wrapColumn('numberOfProjects'))
|
||||
->leftJoin('zp_projects', 'zp_clients.id', '=', 'zp_projects.clientId')
|
||||
->groupBy(
|
||||
'zp_clients.id',
|
||||
'zp_clients.name',
|
||||
'zp_clients.internet'
|
||||
)
|
||||
->orderBy('zp_clients.name')
|
||||
->get();
|
||||
|
||||
return array_map(fn ($item) => (array) $item, $results->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|mixed
|
||||
*/
|
||||
public function getNumberOfClients(): mixed
|
||||
{
|
||||
return $this->db->table('zp_clients')->count();
|
||||
}
|
||||
|
||||
public function isClient(array $values): bool
|
||||
{
|
||||
return $this->db->table('zp_clients')
|
||||
->where('name', $values['name'])
|
||||
->where('street', $values['street'])
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function getClientsUsers(int|string $clientId): false|array
|
||||
{
|
||||
$results = $this->db->table('zp_user')
|
||||
->select(
|
||||
'id',
|
||||
'firstname',
|
||||
'lastname',
|
||||
'username',
|
||||
'notifications',
|
||||
'profileId',
|
||||
'phone',
|
||||
'status'
|
||||
)
|
||||
->where('clientId', $clientId)
|
||||
->where(function ($query) {
|
||||
$query->whereNull('source')
|
||||
->orWhere('source', '!=', 'api');
|
||||
})
|
||||
->get();
|
||||
|
||||
return array_map(fn ($item) => (array) $item, $results->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* addClient - add a client and postback test
|
||||
*/
|
||||
public function addClient(array $values): false|string
|
||||
{
|
||||
$id = $this->db->table('zp_clients')->insertGetId([
|
||||
'name' => $values['name'],
|
||||
'street' => $values['street'] ?? '',
|
||||
'zip' => $values['zip'] ?? '',
|
||||
'city' => $values['city'] ?? '',
|
||||
'state' => $values['state'] ?? '',
|
||||
'country' => $values['country'] ?? '',
|
||||
'phone' => $values['phone'] ?? '',
|
||||
'internet' => $values['internet'] ?? '',
|
||||
'email' => $values['email'] ?? '',
|
||||
]);
|
||||
|
||||
return (string) $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* editClient - edit a client
|
||||
*/
|
||||
public function editClient(array $values, int|string $id): bool
|
||||
{
|
||||
return $this->db->table('zp_clients')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
'name' => $values['name'],
|
||||
'street' => $values['street'],
|
||||
'zip' => $values['zip'],
|
||||
'city' => $values['city'],
|
||||
'state' => $values['state'],
|
||||
'country' => $values['country'],
|
||||
'phone' => $values['phone'],
|
||||
'internet' => $values['internet'],
|
||||
'email' => $values['email'],
|
||||
]) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* deleteClient - delete a client and associated projects
|
||||
*/
|
||||
public function deleteClient(int|string $id): bool
|
||||
{
|
||||
// Delete projects associated with the client first
|
||||
$this->db->table('zp_projects')
|
||||
->where('clientId', $id)
|
||||
->delete();
|
||||
|
||||
// Then delete the client
|
||||
return $this->db->table('zp_clients')
|
||||
->where('id', $id)
|
||||
->delete() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* hasTickets - check if a project has Tickets
|
||||
*/
|
||||
public function hasTickets(int|string $id): bool
|
||||
{
|
||||
return $this->db->table('zp_projects')
|
||||
->join('zp_tickets', 'zp_projects.id', '=', 'zp_tickets.projectId')
|
||||
->where('zp_projects.clientId', $id)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
304
app/Domain/Clients/Services/Clients.php
Normal file
304
app/Domain/Clients/Services/Clients.php
Normal file
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Clients\Services;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Exceptions\EntityExistsException;
|
||||
use Leantime\Core\Exceptions\MissingParameterException;
|
||||
use Leantime\Domain\Clients\Permissions\ClientsPermissions;
|
||||
use Leantime\Domain\Clients\Repositories\Clients as ClientRepository;
|
||||
use Leantime\Domain\Comments\Services\Comments as CommentService;
|
||||
use Leantime\Domain\Files\Services\Files as FileService;
|
||||
use Leantime\Domain\Projects\Repositories\Projects as ProjectRepository;
|
||||
use Leantime\Domain\Users\Repositories\Users as UserRepository;
|
||||
|
||||
/**
|
||||
* Client service - Business logic for client management.
|
||||
*/
|
||||
class Clients
|
||||
{
|
||||
private ProjectRepository $projectRepository;
|
||||
|
||||
private ClientRepository $clientRepository;
|
||||
|
||||
private CommentService $commentService;
|
||||
|
||||
private FileService $fileService;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct(
|
||||
ProjectRepository $projectRepository,
|
||||
ClientRepository $clientRepository,
|
||||
CommentService $commentService,
|
||||
FileService $fileService,
|
||||
UserRepository $userRepository,
|
||||
) {
|
||||
$this->projectRepository = $projectRepository;
|
||||
$this->clientRepository = $clientRepository;
|
||||
$this->commentService = $commentService;
|
||||
$this->fileService = $fileService;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets clients accessible by a specific user based on their project assignments.
|
||||
*
|
||||
* @param int $userId The user ID
|
||||
* @return array List of clients the user has access to
|
||||
*
|
||||
* @internal Internal-only; deliberately excluded from the JSON-RPC surface. The $userId is
|
||||
* caller-supplied, so exposing it would let any user enumerate another user's
|
||||
* client list (IDOR). Only ever called internally with the session user's id
|
||||
* (e.g. the roadmap/milestone client-filter dropdown).
|
||||
*/
|
||||
public function getUserClients(int $userId): array
|
||||
{
|
||||
$userProjects = $this->projectRepository->getUserProjects($userId);
|
||||
$clients = [];
|
||||
|
||||
if (is_array($userProjects)) {
|
||||
foreach ($userProjects as $project) {
|
||||
if (! array_key_exists($project['clientId'], $clients)) {
|
||||
$clients[$project['clientId']] = ['id' => $project['clientId'], 'name' => $project['clientName']];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $clients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all clients.
|
||||
*
|
||||
* @param array|null $searchparams Optional search parameters
|
||||
* @return array List of all clients
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::VIEW, global: true)]
|
||||
public function getAll(?array $searchparams = null): array
|
||||
{
|
||||
return $this->clientRepository->getAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches the client by key.
|
||||
*
|
||||
* @param int $id Id of the object to be patched
|
||||
* @param array $params Key=>value array where key represents the object field name and value the value
|
||||
* @return bool Returns true on success, false on failure
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::EDIT, global: true)]
|
||||
public function patch(int $id, array $params): bool
|
||||
{
|
||||
return $this->clientRepository->patch($id, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing client.
|
||||
*
|
||||
* @param array $values Client data including 'id' key
|
||||
* @return bool Returns true on success, false on failure
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::EDIT, global: true)]
|
||||
public function editClient(array $values): bool
|
||||
{
|
||||
return $this->clientRepository->editClient($values, $values['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new client.
|
||||
*
|
||||
* @param array $values Client data to create
|
||||
* @return int|false Returns id of new element or false
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::CREATE, global: true)]
|
||||
public function create(array $values): int|false
|
||||
{
|
||||
return $this->clientRepository->addClient($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a client and its associated projects.
|
||||
*
|
||||
* @param int $id Id of the client to be deleted
|
||||
* @return bool Returns true on success, false on failure
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::DELETE, global: true)]
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
return $this->clientRepository->deleteClient($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets 1 specific client by id.
|
||||
*
|
||||
* @param int $id Id of the client to be retrieved
|
||||
* @return array|false Returns client data or false if not found
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::VIEW, global: true)]
|
||||
public function get(int $id): array|false
|
||||
{
|
||||
return $this->clientRepository->getClient($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a client with the same name and street already exists.
|
||||
*
|
||||
* @param array $values Client data with 'name' and 'street' keys
|
||||
* @return bool Returns true if client exists
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::VIEW, global: true)]
|
||||
public function isClient(array $values): bool
|
||||
{
|
||||
return $this->clientRepository->isClient($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a client has any tickets via its projects.
|
||||
*
|
||||
* @param int $id Client id
|
||||
* @return bool Returns true if client has tickets
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::VIEW, global: true)]
|
||||
public function hasTickets(int $id): bool
|
||||
{
|
||||
return $this->clientRepository->hasTickets($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all users assigned to a client.
|
||||
*
|
||||
* @param int $clientId Client id
|
||||
* @return array|false Returns list of users or false
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::VIEW, global: true)]
|
||||
public function getClientsUsers(int $clientId): array|false
|
||||
{
|
||||
return $this->clientRepository->getClientsUsers($clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets projects belonging to a client.
|
||||
*
|
||||
* @param int $clientId Client id
|
||||
* @return array List of projects for this client
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::VIEW, global: true)]
|
||||
public function getClientProjects(int $clientId): array
|
||||
{
|
||||
return $this->projectRepository->getClientProjects($clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new client after validating it and checking for duplicates.
|
||||
*
|
||||
* Encapsulates the name-required validation and the duplicate-name check
|
||||
* that previously lived in the controller.
|
||||
*
|
||||
* @param array $values Client data to create (requires a non-empty 'name')
|
||||
* @return int Id of the newly created client
|
||||
*
|
||||
* @throws MissingParameterException When the client name is empty
|
||||
* @throws EntityExistsException When a client with the same name/street already exists
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::CREATE, global: true)]
|
||||
public function createClient(array $values): int
|
||||
{
|
||||
if (($values['name'] ?? '') === '') {
|
||||
throw new MissingParameterException('Client name not specified');
|
||||
}
|
||||
|
||||
if ($this->isClient($values) === true) {
|
||||
throw new EntityExistsException('Client exists already');
|
||||
}
|
||||
|
||||
return (int) $this->clientRepository->addClient($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing client after validating the name is present.
|
||||
*
|
||||
* @param array $values Client data including 'id' key (requires a non-empty 'name')
|
||||
* @return bool Returns true on success, false on failure
|
||||
*
|
||||
* @throws MissingParameterException When the client name is empty
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::EDIT, global: true)]
|
||||
public function updateClient(array $values): bool
|
||||
{
|
||||
if (($values['name'] ?? '') === '') {
|
||||
throw new MissingParameterException('Client name not specified');
|
||||
}
|
||||
|
||||
return $this->editClient($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a user from a client by clearing the user's client assignment.
|
||||
*
|
||||
* Keeps the Clients controllers within the Clients service surface while the
|
||||
* underlying mutation lives on the Users repository.
|
||||
*
|
||||
* @param int $clientId Client the user should be removed from
|
||||
* @param int $userId User to remove from the client
|
||||
* @return bool Returns true on success, false on failure
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::EDIT, global: true)]
|
||||
public function removeUser(int $clientId, int $userId): bool
|
||||
{
|
||||
if ($clientId === 0 || $userId === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->userRepository->removeFromClient($userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles the template data needed to render the client detail page.
|
||||
*
|
||||
* Centralizes the shared assignment block previously duplicated across the
|
||||
* GET and POST handlers of the ShowClient controller.
|
||||
*
|
||||
* @param int $id Client id
|
||||
* @return array{userClients: array|false, comments: array|false, imgExtensions: array<int, string>, clientProjects: array, files: array|false}
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ClientsPermissions::VIEW, global: true)]
|
||||
public function getClientPageData(int $id): array
|
||||
{
|
||||
return [
|
||||
'userClients' => $this->getClientsUsers($id),
|
||||
'comments' => $this->commentService->getComments('client', $id),
|
||||
'imgExtensions' => ['jpg', 'jpeg', 'png', 'gif', 'psd', 'bmp', 'tif', 'thm', 'yuv'],
|
||||
'clientProjects' => $this->getClientProjects($id),
|
||||
'files' => $this->fileService->getFilesByModule('client', $id),
|
||||
];
|
||||
}
|
||||
}
|
||||
40
app/Domain/Clients/Templates/delClient.blade.php
Normal file
40
app/Domain/Clients/Templates/delClient.blade.php
Normal file
@@ -0,0 +1,40 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
@dispatchEvent('beforePageHeaderOpen')
|
||||
<div class="pageheader">
|
||||
@dispatchEvent('afterPageHeaderOpen')
|
||||
<div class="pageicon"><span class="fa {{ $tpl->getModulePicture() }}"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{!! __('label.administration') !!}</h5>
|
||||
<h1>{!! sprintf(__('headline.delete_client'), $client['name']) !!}</h1>
|
||||
</div>
|
||||
@dispatchEvent('beforePageHeaderClose')
|
||||
</div><!--pageheader-->
|
||||
@dispatchEvent('afterPageHeaderClose')
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<h4 class="widget widgettitle">{!! __('subtitles.delete') !!}</h4>
|
||||
<div class="widgetcontent">
|
||||
|
||||
<form method="post">
|
||||
|
||||
@dispatchEvent('afterFormOpen')
|
||||
|
||||
<p>{!! __('text.confirm_client_deletion') !!}<br /></p>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.yes_delete')" name="del" />
|
||||
<x-global::forms.button tag="a" link="/clients/showClient/{{ $client['id'] }}" contentRole="tertiary">{!! __('buttons.back') !!}</x-global::forms.button>
|
||||
|
||||
@dispatchEvent('beforeFormClose')
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
69
app/Domain/Clients/Templates/editClient.blade.php
Normal file
69
app/Domain/Clients/Templates/editClient.blade.php
Normal file
@@ -0,0 +1,69 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
@dispatchEvent('beforePageHeaderOpen')
|
||||
<div class="pageheader">
|
||||
@dispatchEvent('afterPageHeaderOpen')
|
||||
<div class="pageicon"><span class="fa {{ $tpl->getModulePicture() }}"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>Administration</h5>
|
||||
<h1>{!! __('EDIT_CLIENT') !!}</h1>
|
||||
</div>
|
||||
@dispatchEvent('beforePageHeaderClose')
|
||||
</div><!--pageheader-->
|
||||
@dispatchEvent('afterPageHeaderClose')
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<form action="" method="post" class="stdform">
|
||||
|
||||
<div class="widget">
|
||||
<h4 class="widgettitle">{!! __('OVERVIEW') !!}</h4>
|
||||
<div class="widgetcontent">
|
||||
|
||||
<label for="name">{!! __('NAME') !!}</label>
|
||||
<x-global::forms.text-input name="name" id="name" value="{{ $values['name'] }}" /><br />
|
||||
|
||||
<label for="email">{!! __('EMAIL') !!}</label>
|
||||
<x-global::forms.text-input name="email" id="email" value="{{ $values['email'] }}" /><br />
|
||||
|
||||
<label for="internet">{!! __('URL') !!}</label> <x-global::forms.text-input
|
||||
name="internet" id="internet"
|
||||
value="{{ $values['internet'] }}" /><br />
|
||||
|
||||
<label for="street">{!! __('STREET') !!}</label> <x-global::forms.text-input
|
||||
name="street" id="street"
|
||||
value="{{ $values['street'] }}" /><br />
|
||||
|
||||
<label for="zip">{!! __('ZIP') !!}</label> <x-global::forms.text-input
|
||||
name="zip" id="zip" value="{{ $values['zip'] }}" /><br />
|
||||
|
||||
<label for="city">{!! __('CITY') !!}</label> <x-global::forms.text-input
|
||||
name="city" id="city" value="{{ $values['city'] }}" /><br />
|
||||
|
||||
<label for="state">{!! __('STATE') !!}</label> <x-global::forms.text-input
|
||||
name="state" id="state"
|
||||
value="{{ $values['state'] }}" /><br />
|
||||
|
||||
<label for="country">{!! __('COUNTRY') !!}</label> <x-global::forms.text-input
|
||||
name="country" id="country"
|
||||
value="{{ $values['country'] }}" /><br />
|
||||
|
||||
<label for="phone">{!! __('PHONE') !!}</label> <x-global::forms.text-input
|
||||
name="phone" id="phone"
|
||||
value="{{ $values['phone'] }}" /><br />
|
||||
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('SAVE')" name="save" id="save" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
128
app/Domain/Clients/Templates/newClient.blade.php
Normal file
128
app/Domain/Clients/Templates/newClient.blade.php
Normal file
@@ -0,0 +1,128 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
@dispatchEvent('beforePageHeaderOpen')
|
||||
<div class="pageheader">
|
||||
@dispatchEvent('afterPageHeaderOpen')
|
||||
<div class="pageicon"><span class="fa fa-address-book"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{!! __('label.administration') !!}</h5>
|
||||
<h1>{!! __('headline.new_client') !!}</h1>
|
||||
</div>
|
||||
@dispatchEvent('beforePageHeaderClose')
|
||||
</div><!--pageheader-->
|
||||
@dispatchEvent('afterPageHeaderClose')
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="widget">
|
||||
<h4 class="widgettitle">{!! __('subtitle.details') !!}</h4>
|
||||
<div class="widgetcontent">
|
||||
|
||||
<form action="" method="post" class="stdform">
|
||||
|
||||
@dispatchEvent('afterFormOpen')
|
||||
|
||||
<div class="row row-fluid">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="span4 control-label">{!! __('label.name') !!}</label>
|
||||
<div class="span6">
|
||||
<x-global::forms.text-input name="name" id="name" value="{{ $values['name'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="span4 control-label">{!! __('label.email') !!}</label>
|
||||
<div class="span6">
|
||||
<x-global::forms.text-input name="email" id="email" value="{{ $values['email'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="span4 control-label">{!! __('label.url') !!}</label>
|
||||
<div class="span6">
|
||||
<x-global::forms.text-input
|
||||
name="internet" id="internet"
|
||||
value="{{ $values['internet'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="span4 control-label">{!! __('label.street') !!}</label>
|
||||
<div class="span6">
|
||||
<x-global::forms.text-input
|
||||
name="street" id="street"
|
||||
value="{{ $values['street'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="span4 control-label">{!! __('label.zip') !!}</label>
|
||||
<div class="span6">
|
||||
<x-global::forms.text-input
|
||||
name="zip" id="zip" value="{{ $values['zip'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="span4 control-label">{!! __('label.city') !!}</label>
|
||||
<div class="span6">
|
||||
<x-global::forms.text-input
|
||||
name="city" id="city" value="{{ $values['city'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="span4 control-label">{!! __('label.state') !!}</label>
|
||||
<div class="span6">
|
||||
<x-global::forms.text-input
|
||||
name="state" id="state"
|
||||
value="{{ $values['state'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="span4 control-label">{!! __('label.country') !!}</label>
|
||||
<div class="span6">
|
||||
<x-global::forms.text-input
|
||||
name="country" id="country"
|
||||
value="{{ $values['country'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="span4 control-label">{!! __('label.phone') !!}</label>
|
||||
<div class="span6">
|
||||
<x-global::forms.text-input
|
||||
name="phone" id="phone"
|
||||
value="{{ $values['phone'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@dispatchEvent('beforeSubmitButton')
|
||||
|
||||
<div class="form-group">
|
||||
<div class="span4 control-label">
|
||||
<x-global::forms.button tag="input" inputType="submit" name="save" id="save"
|
||||
:labelText="__('buttons.save')" contentRole="primary" />
|
||||
</div>
|
||||
<div class="span6">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@dispatchEvent('beforeFormClose')
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
77
app/Domain/Clients/Templates/showAll.blade.php
Normal file
77
app/Domain/Clients/Templates/showAll.blade.php
Normal file
@@ -0,0 +1,77 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
@dispatchEvent('beforePageHeaderOpen')
|
||||
<div class="pageheader">
|
||||
@dispatchEvent('afterPageHeaderOpen')
|
||||
<div class="pageicon"><span class="fa fa-address-book"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{!! __('label.administration') !!}</h5>
|
||||
<h1>{!! __('headline.all_clients') !!}</h1>
|
||||
</div>
|
||||
@dispatchEvent('beforePageHeaderClose')
|
||||
</div><!--pageheader-->
|
||||
@dispatchEvent('afterPageHeaderClose')
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
@can('clients.create')
|
||||
<x-global::forms.button tag="a" link="{{ BASE_URL }}/clients/newClient" contentRole="primary"><i class='fa fa-plus'></i> {!! __('link.new_client') !!}</x-global::forms.button>
|
||||
@endcan
|
||||
|
||||
<table class="table table-bordered" cellpadding="0" cellspacing="0" border="0" id="allClientsTable">
|
||||
<colgroup>
|
||||
<col class='con0' />
|
||||
<col class='con1' />
|
||||
<col class='con0' />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class='head0'>{!! __('label.client_id') !!}</th>
|
||||
<th class='head1'>{!! __('label.client_name') !!}</th>
|
||||
<th class='head0'>{!! __('label.url') !!}</th>
|
||||
<th class='head1'>{!! __('label.number_of_projects') !!}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@foreach ($allClients as $row)
|
||||
<tr>
|
||||
<td>{{ $row['id'] }}</td>
|
||||
<td>
|
||||
<a class="" href="{{ BASE_URL }}/clients/showClient/{{ $row['id'] }}"><i class='fa fa-plus'></i> {{ $row['name'] }}</a>
|
||||
</td>
|
||||
<td><a href="{{ $row['internet'] }}" target="_blank">{{ $row['internet'] }}</a></td>
|
||||
<td>{{ $row['numberOfProjects'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
@dispatchEvent('scripts.afterOpen')
|
||||
|
||||
jQuery(document).ready(function() {
|
||||
|
||||
leantime.clientsController.initClientTable();
|
||||
|
||||
|
||||
});
|
||||
|
||||
@dispatchEvent('scripts.beforeClose')
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
286
app/Domain/Clients/Templates/showClient.blade.php
Normal file
286
app/Domain/Clients/Templates/showClient.blade.php
Normal file
@@ -0,0 +1,286 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$values = $client;
|
||||
@endphp
|
||||
|
||||
@dispatchEvent('beforePageHeaderOpen')
|
||||
<div class="pageheader">
|
||||
@dispatchEvent('afterPageHeaderOpen')
|
||||
<div class="pageicon"><span class="fa fa-address-book"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{!! __('label.administration') !!}</h5>
|
||||
<h1>{{ $values['name'] }}</h1>
|
||||
</div>
|
||||
@dispatchEvent('beforePageHeaderClose')
|
||||
</div><!--pageheader-->
|
||||
@dispatchEvent('afterPageHeaderClose')
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="tabbedwidget tab-primary clientTabs">
|
||||
|
||||
<ul>
|
||||
<li><a href="#clientDetails">{!! __('label.client_details') !!}</a></li>
|
||||
<li><a href="#comment">{!! sprintf(__('tabs.discussion_with_count'), count($comments)) !!}</a></li>
|
||||
<li><a href="#files">{!! sprintf(__('tabs.files_with_count'), count($files)) !!}</a></li>
|
||||
</ul>
|
||||
|
||||
<div id='clientDetails'>
|
||||
<form action="" method="post">
|
||||
|
||||
<div class="row row-fluid">
|
||||
<div class="col-md-6">
|
||||
<h4 class="widgettitle title-light"><span class="fa fa-leaf"></span> {!! __('subtitle.details') !!}</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label class=" control-label">{!! __('label.client_id') !!}</label>
|
||||
<div class="">
|
||||
<x-global::forms.text-input name="id" id="id" value="{{ $values['id'] }}" readonly />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class=" control-label">{!! __('label.name') !!}</label>
|
||||
<div class="">
|
||||
<x-global::forms.text-input name="name" id="name" value="{{ $values['name'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class=" control-label">{!! __('label.email') !!}</label>
|
||||
<div class="">
|
||||
<x-global::forms.text-input name="email" id="email" value="{{ $values['email'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class=" control-label">{!! __('label.url') !!}</label>
|
||||
<div class="">
|
||||
<x-global::forms.text-input
|
||||
name="internet" id="internet"
|
||||
value="{{ $values['internet'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class=" control-label">{!! __('label.street') !!}</label>
|
||||
<div class="">
|
||||
<x-global::forms.text-input
|
||||
name="street" id="street"
|
||||
value="{{ $values['street'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class=" control-label">{!! __('label.zip') !!}</label>
|
||||
<div class="">
|
||||
<x-global::forms.text-input
|
||||
name="zip" id="zip" value="{{ $values['zip'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class=" control-label">{!! __('label.city') !!}</label>
|
||||
<div class="">
|
||||
<x-global::forms.text-input
|
||||
name="city" id="city" value="{{ $values['city'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class=" control-label">{!! __('label.state') !!}</label>
|
||||
<div class="">
|
||||
<x-global::forms.text-input
|
||||
name="state" id="state"
|
||||
value="{{ $values['state'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class=" control-label">{!! __('label.country') !!}</label>
|
||||
<div class="">
|
||||
<x-global::forms.text-input
|
||||
name="country" id="country"
|
||||
value="{{ $values['country'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class=" control-label">{!! __('label.phone') !!}</label>
|
||||
<div class="">
|
||||
<x-global::forms.text-input
|
||||
name="phone" id="phone"
|
||||
value="{{ $values['phone'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h4 class="widgettitle title-light"><span class="fa fa-users"></span> {!! __('subtitles.users_assigned_to_this_client') !!}</h4>
|
||||
<x-global::forms.button tag="a" link="#/users/newUser?preSelectedClient={{ $values['id'] }}" contentRole="primary"><i class='fa fa-plus'></i> {!! __('buttons.add_user') !!} </x-global::forms.button>
|
||||
<table class='table table-bordered'>
|
||||
<colgroup>
|
||||
<col class="con1" />
|
||||
<col class="con0"/>
|
||||
<col class="con1" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{!! __('label.name') !!}</th>
|
||||
<th>{!! __('label.email') !!}</th>
|
||||
<th>{!! __('label.phone') !!}</th>
|
||||
<th>{!! __('label.actions') !!}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($userClients as $user)
|
||||
<tr>
|
||||
<td>
|
||||
{!! sprintf(__('text.full_name'), e($user['firstname']), e($user['lastname'])) !!}
|
||||
</td>
|
||||
<td><a href='mailto:{{ $user['username'] }}'>{{ $user['username'] }}</a></td>
|
||||
<td>{{ $user['phone'] }}</td>
|
||||
<td>
|
||||
<a href="{{ BASE_URL }}/users/editUser/{{ $user['id'] }}" title="{{ __('buttons.edit') }}">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
<form method="post" action="{{ BASE_URL }}/clients/removeUser" style="display:inline;"
|
||||
onsubmit="return confirm('{{ __('text.confirm_remove_user_from_client') }}')">
|
||||
@csrf
|
||||
<input type="hidden" name="id" value="{{ $values['id'] }}" />
|
||||
<input type="hidden" name="userId" value="{{ $user['id'] }}" />
|
||||
<x-global::forms.button inputType="submit" class="delete" contentRole="link" title="{{ __('buttons.remove') }}" style="padding:0; border:none; background:none;">
|
||||
<i class="fa fa-trash"></i>
|
||||
</x-global::forms.button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
@if (count($userClients) == 0)
|
||||
<tr><td colspan='4'>{!! __('text.no_users_assigned_to_this_client') !!}</td></tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<x-global::forms.button tag="input" inputType="submit" name="save" id="save"
|
||||
:labelText="__('buttons.save')" contentRole="primary" />
|
||||
</div>
|
||||
<div class="col-md-6 align-right">
|
||||
<x-global::forms.button tag="a" link="{{ BASE_URL }}/clients/delClient/{{ $_GET['id'] }}" class="delete" state="danger" variant="outline"><i class="fa fa-trash"></i> {!! __('links.delete') !!}</x-global::forms.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id='comment'>
|
||||
|
||||
<form method="post" action="{{ BASE_URL }}/clients/showClient/{{ $_GET['id'] }}#comment">
|
||||
<input type="hidden" name="comment" value="1" />
|
||||
@include('comments::submodules.generalComment', ['formUrl' => BASE_URL . '/clients/showClient/' . e(request()->query('id', ''))])
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div id='files'>
|
||||
|
||||
<div class="mediamgr_category">
|
||||
<form action='#files' method='POST' enctype="multipart/form-data">
|
||||
|
||||
<div class="par f-left" style="margin-right: 15px;">
|
||||
|
||||
<div class='fileupload fileupload-new' data-provides='fileupload'>
|
||||
<input type="hidden" />
|
||||
<div class="input-append">
|
||||
<div class="uneditable-input span3">
|
||||
<i class="fa-file fileupload-exists"></i><span class="fileupload-preview"></span>
|
||||
</div>
|
||||
<span class="btn btn-file">
|
||||
<span class="fileupload-new">{!! __('label.select_file') !!}</span>
|
||||
<span class='fileupload-exists'>{!! __('label.change') !!}</span>
|
||||
<input type='file' name='file' />
|
||||
</span>
|
||||
<a href='#' class='btn fileupload-exists' data-dismiss='fileupload'>{!! __('buttons.remove') !!}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.upload')" name="upload" />
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mediamgr_content">
|
||||
|
||||
<ul id='medialist' class='listfile'>
|
||||
@foreach ($files as $file)
|
||||
<li class="{{ $file['moduleId'] }}">
|
||||
<div class="inlineDropDownContainer" style="float:right;">
|
||||
|
||||
<a href="javascript:void(0);" class="dropdown-toggle ticketDropDown" data-toggle="dropdown">
|
||||
<i class="fa fa-ellipsis-v" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="nav-header">{!! __('subtitles.file') !!}</li>
|
||||
<li><a href="{{ BASE_URL }}/files/get?module={{ $file['module'] }}&encName={{ $file['encName'] }}&ext={{ $file['extension'] }}&realName={{ $file['realName'] }}">{!! __('links.download') !!}</a></li>
|
||||
|
||||
@can('clients.edit')
|
||||
<li><a href="{{ BASE_URL }}/clients/showClient/{{ $_GET['id'] }}?delFile={{ $file['id'] }}" class="delete"><i class="fa fa-trash"></i> {!! __('links.delete') !!}</a></li>
|
||||
@endcan
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<a class="cboxElement" href="{{ BASE_URL }}/files/get?module={{ $file['module'] }}&encName={{ $file['encName'] }}&ext={{ $file['extension'] }}&realName={{ $file['realName'] }}">
|
||||
@if (in_array(strtolower($file['extension']), $imgExtensions))
|
||||
<img style='max-height: 50px; max-width: 70px;' src="{{ BASE_URL }}/files/get?module={{ $file['module'] }}&encName={{ $file['encName'] }}&ext={{ $file['extension'] }}&realName={{ $file['realName'] }}" alt="" />
|
||||
@else
|
||||
<img style='max-height: 50px; max-width: 70px;' src='{{ BASE_URL }}/dist/images/thumbs/doc.png' />
|
||||
@endif
|
||||
<span class="filename">{{ $file['realName'] }}</span>
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
<br class="clearall" />
|
||||
</ul>
|
||||
|
||||
</div><!--mediamgr_content-->
|
||||
<div style='clear:both'> </div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
@dispatchEvent('scripts.afterOpen')
|
||||
|
||||
jQuery(document).ready(function($)
|
||||
{
|
||||
leantime.clientsController.initClientTabs();
|
||||
}
|
||||
);
|
||||
|
||||
@dispatchEvent('scripts.beforeClose')
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
Reference in New Issue
Block a user