Merge branch 'develop' into trunk
This commit is contained in:
commit
347bf77d43
88 changed files with 3373 additions and 325 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -1,3 +1,22 @@
|
|||
### 31.05.2022 -> 1.2.0
|
||||
- Ajout de la fonctionnalitée d'estimations
|
||||
|
||||
### 26.05.2022 -> 1.1.0
|
||||
- Correction de regression sur macro planning
|
||||
- Correction de regression sur droits planning
|
||||
- Ajout de fonctionnalité Label
|
||||
- Ajout de fonctionalité assigner un ticket
|
||||
- Rafactorisation du code ticket
|
||||
- Refonte de tickets.index
|
||||
- Ajout CSP report URI
|
||||
- Remove external JS and CSS dependencies
|
||||
- Use Vuejs runtime
|
||||
- Add xss sanitizer
|
||||
- Normalize Laravel mix + add cleaner
|
||||
- Update npm deps
|
||||
- Upgrade bootswatch theme
|
||||
- ADD Secure HEADERS
|
||||
|
||||
### 23.05.2022 -> 1.0.9
|
||||
- Correction d'erreurs multiples
|
||||
- Fonctionnalité macro planning
|
||||
|
|
|
|||
56
app/Casts/HtmlColor.php
Normal file
56
app/Casts/HtmlColor.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
|
||||
/**
|
||||
* Custom cast Html Color
|
||||
* https://laravel.com/docs/8.x/eloquent-mutators#custom-casts
|
||||
*/
|
||||
class HtmlColor implements CastsAttributes
|
||||
{
|
||||
/**
|
||||
* Cast the given value.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param array $attributes
|
||||
* @return string
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
|
||||
*/
|
||||
public function get($model, $key, $value, $attributes)
|
||||
{
|
||||
return "#{$this->zeropad(dechex($value), 6)}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the given value for storage.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param array $attributes
|
||||
* @return integer
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
|
||||
*/
|
||||
public function set($model, $key, $value, $attributes)
|
||||
{
|
||||
return \intval(hexdec(ltrim($value, '#')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pad with zeros a numeric string
|
||||
*
|
||||
* @param string $num
|
||||
* @param integer $lim
|
||||
* @return string
|
||||
*/
|
||||
private function zeropad(string $num, int $lim): string
|
||||
{
|
||||
return (strlen($num) >= $lim) ? $num : $this->zeropad("0" . $num, $lim);
|
||||
}
|
||||
}
|
||||
38
app/Enums/Credit/AccountType.php
Normal file
38
app/Enums/Credit/AccountType.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums\Credit;
|
||||
|
||||
use Kwaadpepper\Enum\BaseEnumRoutable;
|
||||
|
||||
/**
|
||||
* @method static self development()
|
||||
* @method static self seo()
|
||||
*/
|
||||
class AccountType extends BaseEnumRoutable
|
||||
{
|
||||
/**
|
||||
* Enum values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function values(): array
|
||||
{
|
||||
return [
|
||||
'development' => 0,
|
||||
'seo' => 1
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum labels
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
'development' => trans('Jours de développement'),
|
||||
'seo' => trans('Jours de seo')
|
||||
];
|
||||
}
|
||||
}
|
||||
38
app/Enums/Credit/BalanceOperation.php
Normal file
38
app/Enums/Credit/BalanceOperation.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums\Credit;
|
||||
|
||||
use Kwaadpepper\Enum\BaseEnumRoutable;
|
||||
|
||||
/**
|
||||
* @method static self credit()
|
||||
* @method static self debit()
|
||||
*/
|
||||
class BalanceOperation extends BaseEnumRoutable
|
||||
{
|
||||
/**
|
||||
* Enum values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function values(): array
|
||||
{
|
||||
return [
|
||||
'credit' => 0,
|
||||
'debit' => 1
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum labels
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
'credit' => trans('Créditer un compte'),
|
||||
'debit' => trans('Débiter un compte')
|
||||
];
|
||||
}
|
||||
}
|
||||
41
app/Enums/Credit/EstimationStatus.php
Normal file
41
app/Enums/Credit/EstimationStatus.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums\Credit;
|
||||
|
||||
use Kwaadpepper\Enum\BaseEnumRoutable;
|
||||
|
||||
/**
|
||||
* @method static self pending()
|
||||
* @method static self accepted()
|
||||
* @method static self refused()
|
||||
*/
|
||||
class EstimationStatus extends BaseEnumRoutable
|
||||
{
|
||||
/**
|
||||
* Enum values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function values(): array
|
||||
{
|
||||
return [
|
||||
'pending' => 0,
|
||||
'accepted' => 1,
|
||||
'refused' => 2
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum labels
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
'pending' => trans('En attente'),
|
||||
'accepted' => trans('Acceptée'),
|
||||
'refused' => trans('Refusée'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -9,9 +9,13 @@ use Kwaadpepper\Enum\BaseEnumRoutable;
|
|||
* @method static self admin()
|
||||
* @method static self client()
|
||||
*/
|
||||
|
||||
class Role extends BaseEnumRoutable
|
||||
{
|
||||
/**
|
||||
* Enum values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function values(): array
|
||||
{
|
||||
return [
|
||||
|
|
@ -21,6 +25,11 @@ class Role extends BaseEnumRoutable
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum labels
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ use Kwaadpepper\Enum\BaseEnumRoutable;
|
|||
*/
|
||||
class TicketStatus extends BaseEnumRoutable
|
||||
{
|
||||
/**
|
||||
* Enum values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function values(): array
|
||||
{
|
||||
return [
|
||||
|
|
@ -40,6 +45,11 @@ class TicketStatus extends BaseEnumRoutable
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum labels
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
|
|
@ -58,4 +68,40 @@ class TicketStatus extends BaseEnumRoutable
|
|||
'archived' => trans('Archivé'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum classes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function classes(): array
|
||||
{
|
||||
return [
|
||||
'new' => 'text-danger',
|
||||
'estimateTodo' => 'text-info',
|
||||
'estimateValidated' => 'text-info',
|
||||
'estimateDone' => 'text-info',
|
||||
'toPlan' => 'text-info',
|
||||
'planned' => 'text-info',
|
||||
'inprogress' => 'text-dark',
|
||||
'waiting' => 'text-white bg-danger',
|
||||
'waitingValidation' => 'text-warning bg-danger',
|
||||
'processed' => trans('Traité'),
|
||||
'recette' => 'text-primary',
|
||||
'production' => 'text-success',
|
||||
'archived' => 'text-secondary',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @return integer|string
|
||||
*/
|
||||
public function __get(string $attribute)
|
||||
{
|
||||
if ($attribute === 'class') {
|
||||
return $this->classes()[$this->getDefinition()];
|
||||
}
|
||||
return parent::__get($attribute);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ use Kwaadpepper\Enum\BaseEnumRoutable;
|
|||
*/
|
||||
class TicketType extends BaseEnumRoutable
|
||||
{
|
||||
/**
|
||||
* Enum values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function values(): array
|
||||
{
|
||||
return [
|
||||
|
|
@ -22,6 +27,11 @@ class TicketType extends BaseEnumRoutable
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum labels
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
|
|
@ -31,4 +41,31 @@ class TicketType extends BaseEnumRoutable
|
|||
'newsletter' => trans('Demande newsletter')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum classes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function classes(): array
|
||||
{
|
||||
return [
|
||||
'bug' => 'bg-danger',
|
||||
'evolution' => 'bg-success',
|
||||
'br' => 'bg-primary',
|
||||
'newsletter' => 'bg-warning'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @return integer|string
|
||||
*/
|
||||
public function __get(string $attribute)
|
||||
{
|
||||
if ($attribute === 'class') {
|
||||
return $this->classes()[$this->getDefinition()];
|
||||
}
|
||||
return parent::__get($attribute);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
51
app/Http/Controllers/BrandController.php
Normal file
51
app/Http/Controllers/BrandController.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Brand;
|
||||
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
|
||||
use Kwaadpepper\CrudPolicies\Traits\CrudController;
|
||||
|
||||
class BrandController extends Controller
|
||||
{
|
||||
use CrudController {
|
||||
__construct as __CrudConstructor;
|
||||
index as crudIndex;
|
||||
}
|
||||
|
||||
/** @var Account */
|
||||
public static $modelClass = Brand::class;
|
||||
|
||||
/**
|
||||
* Crud Layout
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $layout = 'layout.app';
|
||||
|
||||
/**
|
||||
* ProjectController
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->__CrudConstructor();
|
||||
$this->authorizeResource(Brand::class, 'brand');
|
||||
}
|
||||
|
||||
/**
|
||||
* Crud index view
|
||||
*
|
||||
* @param \Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest $request
|
||||
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
|
||||
*/
|
||||
public function index(CrudIndexRequest $request)
|
||||
{
|
||||
/** @var \Illuminate\View\View */
|
||||
$view = $this->crudIndex($request);
|
||||
if ($request->ajax()) {
|
||||
return $view;
|
||||
}
|
||||
return view('brands.index', $view->getData());
|
||||
}
|
||||
}
|
||||
118
app/Http/Controllers/Credit/AccountController.php
Normal file
118
app/Http/Controllers/Credit/AccountController.php
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Credit;
|
||||
|
||||
use App\Enums\Credit\BalanceOperation;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Credit\AddCreditToAccountRequest;
|
||||
use App\Models\Brand;
|
||||
use App\Models\Credit\Account;
|
||||
use App\Models\Credit\CreditLog;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
|
||||
use Kwaadpepper\CrudPolicies\Traits\CrudController;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
use CrudController {
|
||||
__construct as __CrudConstructor;
|
||||
index as crudIndex;
|
||||
}
|
||||
|
||||
/** @var Account */
|
||||
public static $modelClass = Account::class;
|
||||
|
||||
/**
|
||||
* Crud Layout
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $layout = 'layout.app';
|
||||
|
||||
/**
|
||||
* Index query middleware
|
||||
*
|
||||
* @param Builder $query
|
||||
* @return Builder
|
||||
*/
|
||||
public function indexQuery(Builder $query): Builder
|
||||
{
|
||||
/** @var \App\Models\User */
|
||||
$user = auth()->user();
|
||||
if (!$user->isAdmin()) {
|
||||
// Show only users brand(s) accounts.
|
||||
$brands = $user->sites()->with('brand')->get()->pluck('brand')
|
||||
->filter(function ($v) {
|
||||
return !\is_null($v);
|
||||
});
|
||||
return $query->whereHas('brand', function ($q) use ($brands) {
|
||||
$q->whereIn('id', $brands->pluck('id'));
|
||||
});
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crud index view
|
||||
*
|
||||
* @param \Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest $request
|
||||
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
|
||||
*/
|
||||
public function index(CrudIndexRequest $request)
|
||||
{
|
||||
/** @var \Illuminate\View\View */
|
||||
$view = $this->crudIndex($request);
|
||||
if ($request->ajax()) {
|
||||
return $view;
|
||||
}
|
||||
return view('accounts.index', $view->getData());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Before store model middleware
|
||||
*
|
||||
* @param Account $model
|
||||
* @return void
|
||||
*/
|
||||
public function storeModel(Account &$model)
|
||||
{
|
||||
$model->brand()->associate(\request()->brand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit
|
||||
*
|
||||
* @param \App\Models\Brand $brand
|
||||
* @param \App\Models\Account $account
|
||||
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
*/
|
||||
public function credit(Brand $brand, Account $account)
|
||||
{
|
||||
return view('accounts.credit', compact('brand', 'account'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit Account
|
||||
*
|
||||
* @param \App\Http\Requests\Credit\AddCreditToAccountRequest $request
|
||||
* @param \App\Models\Brand $brand
|
||||
* @param \App\Models\Account $account
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
|
||||
*/
|
||||
public function creditSubmit(AddCreditToAccountRequest $request, Brand $brand, Account $account)
|
||||
{
|
||||
$log = new CreditLog();
|
||||
$log->account()->associate($account);
|
||||
$log->type = BalanceOperation::credit();
|
||||
$log->amount = \strval($request->amount);
|
||||
if ($log->saveOrFail()) {
|
||||
return \response()->redirectToRoute('brands.credit_accounts.index', $brand)
|
||||
->with('info', trans('Le compte à bien été crédité'));
|
||||
}
|
||||
return back();
|
||||
}
|
||||
}
|
||||
55
app/Http/Controllers/Credit/CreditLogController.php
Normal file
55
app/Http/Controllers/Credit/CreditLogController.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Credit;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Credit\CreditLog;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
|
||||
use Kwaadpepper\CrudPolicies\Traits\CrudController;
|
||||
|
||||
class CreditLogController extends Controller
|
||||
{
|
||||
use CrudController {
|
||||
__construct as __CrudConstructor;
|
||||
index as crudIndex;
|
||||
}
|
||||
|
||||
/** @var CreditLog */
|
||||
public static $modelClass = CreditLog::class;
|
||||
|
||||
/**
|
||||
* Crud Layout
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $layout = 'layout.app';
|
||||
|
||||
/**
|
||||
* Index query middleware
|
||||
*
|
||||
* @param Builder $query
|
||||
* @return Builder
|
||||
*/
|
||||
public function indexQuery(Builder $query): Builder
|
||||
{
|
||||
$query->orderBy('created_at', 'DESC');
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crud index view
|
||||
*
|
||||
* @param \Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest $request
|
||||
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
|
||||
*/
|
||||
public function index(CrudIndexRequest $request)
|
||||
{
|
||||
/** @var \Illuminate\View\View */
|
||||
$view = $this->crudIndex($request);
|
||||
if ($request->ajax()) {
|
||||
return $view;
|
||||
}
|
||||
return view('credit_logs.index', $view->getData());
|
||||
}
|
||||
}
|
||||
102
app/Http/Controllers/Credit/EstimationController.php
Normal file
102
app/Http/Controllers/Credit/EstimationController.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Credit;
|
||||
|
||||
use App\Enums\Credit\EstimationStatus;
|
||||
use App\Enums\TicketStatus;
|
||||
use App\Events\NotifyAdmin;
|
||||
use App\Events\NotifySubscriptions;
|
||||
use App\Events\TicketStatusChanged;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Credit\AnswerEstimateRequest;
|
||||
use App\Http\Requests\Credit\CreateEstimateRequest;
|
||||
use App\Models\Credit\Estimation;
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EstimationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show estimate Form
|
||||
*
|
||||
* @param \App\Models\Ticket $ticket
|
||||
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
|
||||
*/
|
||||
public function estimate(Ticket $ticket)
|
||||
{
|
||||
$this->authorize('estimate', $ticket);
|
||||
return view('estimations.create', \compact('ticket'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Ticket estimation
|
||||
*
|
||||
* @param \App\Http\Requests\Credit\CreateEstimateRequest $request
|
||||
* @param \App\Models\Ticket $ticket
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function estimateSubmit(CreateEstimateRequest $request, Ticket $ticket)
|
||||
{
|
||||
return DB::transaction(function () use ($request, $ticket) {
|
||||
/** @var \App\Models\User */
|
||||
$user = auth()->user();
|
||||
$estimate = new Estimation($request->validated());
|
||||
$estimate->ticket()->associate($ticket);
|
||||
$estimate->estimator()->associate($user);
|
||||
$ticket->status = TicketStatus::waitingValidation();
|
||||
$ticket->saveQuietly();
|
||||
if ($estimate->saveOrFail()) {
|
||||
$message = sprintf(
|
||||
'<b>%s</b> a modifié le statut en <b>%s</b>, Une estimation de %s jours à été ajoutée',
|
||||
$user->name,
|
||||
$ticket->status->label,
|
||||
\number_format(\floatval($request->amount), 1, ',', '')
|
||||
);
|
||||
event(new TicketStatusChanged($ticket, $ticket->status, $user));
|
||||
event(new NotifySubscriptions($ticket, $message));
|
||||
event(new NotifyAdmin($ticket, $message));
|
||||
return response()->redirectToRoute('ticket.show', $ticket)
|
||||
->with('info', trans('L\'estimation à bien été ajouté'));
|
||||
}
|
||||
return \back();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Answer ticket estimation
|
||||
*
|
||||
* @param \App\Http\Requests\Credit\AnswerEstimateRequest $request
|
||||
* @param \App\Models\Ticket $ticket
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function estimatePendingSubmit(AnswerEstimateRequest $request, Ticket $ticket)
|
||||
{
|
||||
/** @var \App\Models\User */
|
||||
$user = auth()->user();
|
||||
/** @var \App\Models\Credit\Estimation */
|
||||
$estimation = $ticket->estimation;
|
||||
if ($request->answer) {
|
||||
$ticket->status = TicketStatus::toPlan();
|
||||
$estimation->status = EstimationStatus::accepted();
|
||||
} else {
|
||||
$ticket->status = TicketStatus::archived();
|
||||
$estimation->status = EstimationStatus::refused();
|
||||
}
|
||||
$estimation->validator()->associate($user);
|
||||
if ($estimation->saveOrFail() and $ticket->saveOrFail()) {
|
||||
$message = sprintf(
|
||||
'Une réponse à été fourni par %s pour l\'estimation du ticket n°%d : %s',
|
||||
$user->name,
|
||||
$ticket->id,
|
||||
$estimation->status->label
|
||||
);
|
||||
event(new TicketStatusChanged($ticket, $ticket->status, $user));
|
||||
event(new NotifySubscriptions($ticket, $message));
|
||||
event(new NotifyAdmin($ticket, $message));
|
||||
return response()->redirectToRoute('ticket.show', $ticket)
|
||||
->with('info', trans('Votre réponse à été enregistrée'));
|
||||
}
|
||||
return \back();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,11 @@ use Parsedown;
|
|||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show Home page
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
|
||||
*/
|
||||
public function home()
|
||||
{
|
||||
$Parsedown = new Parsedown();
|
||||
|
|
|
|||
21
app/Http/Controllers/LabelController.php
Normal file
21
app/Http/Controllers/LabelController.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Label;
|
||||
use Kwaadpepper\CrudPolicies\Traits\CrudController;
|
||||
|
||||
class LabelController extends Controller
|
||||
{
|
||||
use CrudController;
|
||||
|
||||
/** @var Label */
|
||||
public static $modelClass = Label::class;
|
||||
|
||||
/**
|
||||
* Crud Layout
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $layout = 'layout.app';
|
||||
}
|
||||
|
|
@ -17,6 +17,15 @@ use Carbon\CarbonImmutable;
|
|||
*/
|
||||
class ScheduleController extends Controller
|
||||
{
|
||||
/**
|
||||
* ScheduleController
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware('is-admin');
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$date = CarbonImmutable::now()->startOfWeek();
|
||||
|
|
|
|||
|
|
@ -73,17 +73,23 @@ class TicketController extends Controller
|
|||
|
||||
/** @var \App\Models\User */
|
||||
$user = auth()->user();
|
||||
$tickets = Ticket::with('site')->has('site');
|
||||
$tickets = Ticket::with(['site', 'labels']);
|
||||
if (!$user->isAdmin()) {
|
||||
$tickets = $tickets->whereHas('site', function ($q) use ($user) {
|
||||
$tickets->whereHas('site', function ($q) use ($user) {
|
||||
$q->whereHas('users', function ($q) use ($user) {
|
||||
$q->whereId($user->id);
|
||||
});
|
||||
});
|
||||
$tickets = $tickets->where('is_internal', false);
|
||||
$tickets->where('is_internal', false);
|
||||
} else {
|
||||
$tickets->has('site');
|
||||
}
|
||||
$tickets = $tickets->where('status', ($request->st == TicketStatus::archived()) ?
|
||||
'=' : '<>', TicketStatus::archived());
|
||||
|
||||
$tickets->where(
|
||||
'status',
|
||||
($request->st == TicketStatus::archived()) ? '=' : '<>',
|
||||
TicketStatus::archived()
|
||||
);
|
||||
|
||||
$tickets = $this->searchTicket($tickets, $request);
|
||||
$tickets = $this->filterTickets($tickets, $request);
|
||||
|
|
@ -184,6 +190,9 @@ class TicketController extends Controller
|
|||
$ticket->file2 = ToolboxHelper::storeImage($ticket->getTable(), $fileTicket2);
|
||||
}
|
||||
|
||||
if ($request->labels) {
|
||||
$ticket->labels()->sync($request->labels);
|
||||
}
|
||||
$ticket->user()->associate(auth()->user());
|
||||
$ticket->site()->associate($site);
|
||||
$ticket->saveOrFail();
|
||||
|
|
@ -254,6 +263,10 @@ class TicketController extends Controller
|
|||
$ticket->file2 = ToolboxHelper::storeImage($ticket->getTable(), $fileTicket2);
|
||||
}
|
||||
|
||||
if ($request->labels) {
|
||||
$ticket->labels()->sync($request->labels);
|
||||
}
|
||||
|
||||
$ticket->site()->associate($site);
|
||||
$ticket->saveOrFail();
|
||||
|
||||
|
|
|
|||
|
|
@ -38,12 +38,7 @@ class Kernel extends HttpKernel
|
|||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\SecureHeaders::class,
|
||||
'xss'
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'throttle:api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class SecureHeaders
|
|||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;",
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
"script-src 'self' $jsDev;"
|
||||
"script-src 'self' $jsDev https://cdnjs.cloudflare.com;"
|
||||
];
|
||||
$response->headers->set(
|
||||
'Content-Security-Policy',
|
||||
|
|
|
|||
41
app/Http/Requests/Credit/AddCreditToAccountRequest.php
Normal file
41
app/Http/Requests/Credit/AddCreditToAccountRequest.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Credit;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AddCreditToAccountRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
if (!auth()->check()) {
|
||||
return false;
|
||||
}
|
||||
/** @var \App\Models\User */
|
||||
$user = auth()->user();
|
||||
return $user->isAdmin() and $user->can('credit', $this->account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'amount' => [
|
||||
'required', 'numeric', 'gt:0', function ($attribute, $value, $fail) {
|
||||
if ((\floatval($value) * 10) % 5 !== 0) {
|
||||
$fail("$attribute doit être un multiple de 5 (comptez par demi journées, soit 0.5)");
|
||||
}
|
||||
}
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
48
app/Http/Requests/Credit/AnswerEstimateRequest.php
Normal file
48
app/Http/Requests/Credit/AnswerEstimateRequest.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Credit;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AnswerEstimateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
if (!auth()->check()) {
|
||||
return false;
|
||||
}
|
||||
/** @var \App\Models\User */
|
||||
$user = auth()->user();
|
||||
return !$user->isAdmin() and $user->can('validateEstimate', $this->ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function prepareForValidation()
|
||||
{
|
||||
$this->merge([
|
||||
'answer' => $this->status === 'accept'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'status' => 'required|in:accept,refuse',
|
||||
'answer' => 'required|boolean'
|
||||
];
|
||||
}
|
||||
}
|
||||
67
app/Http/Requests/Credit/CreateEstimateRequest.php
Normal file
67
app/Http/Requests/Credit/CreateEstimateRequest.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Credit;
|
||||
|
||||
use App\Models\Credit\Account;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CreateEstimateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
if (!auth()->check()) {
|
||||
return false;
|
||||
}
|
||||
/** @var \App\Models\User */
|
||||
$user = auth()->user();
|
||||
return $user->isAdmin() and $user->can('estimate', $this->ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
/** @var \App\Models\Ticket */
|
||||
$ticket = $this->ticket;
|
||||
return [
|
||||
'amount' => [
|
||||
'required', 'numeric', 'gt:0', function ($attribute, $value, $fail) {
|
||||
if ((\floatval($value) * 10) % 5 !== 0) {
|
||||
$fail("$attribute doit être un multiple de 5 (comptez par demi journées, soit 0.5)");
|
||||
}
|
||||
}
|
||||
],
|
||||
'credit_account_id' => [
|
||||
'required', 'exists:credit_accounts,id',
|
||||
function ($attribute, $value, $fail) use ($ticket) {
|
||||
/** @var \App\Models\Credit\Account|null */
|
||||
$account = Account::whereId($value)->first();
|
||||
if ($account and !$ticket->accountIsValidForThisTicket($account)) {
|
||||
$fail("$attribute does not belong to the ticket brand");
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'amount' => 'Montant',
|
||||
'credit_account_id' => 'Compte'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,12 @@ class PostActivityRequest extends FormRequest
|
|||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return auth()->check() and $this->ajax();
|
||||
if (!auth()->check() or !$this->ajax()) {
|
||||
return false;
|
||||
}
|
||||
/** @var \App\Models\User */
|
||||
$user = auth()->user();
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace App\Http\Requests;
|
|||
|
||||
use App\Enums\TicketStatus;
|
||||
use App\Enums\TicketType;
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreTicket extends FormRequest
|
||||
|
|
@ -30,7 +31,7 @@ class StoreTicket extends FormRequest
|
|||
if ($user) {
|
||||
if (!$user->isAdmin()) {
|
||||
$this->merge([
|
||||
'status' => $this->site->status->value,
|
||||
'status' => TicketStatus::new()->value,
|
||||
'is_internal' => false,
|
||||
]);
|
||||
} else {
|
||||
|
|
@ -50,6 +51,8 @@ class StoreTicket extends FormRequest
|
|||
*/
|
||||
public function rules()
|
||||
{
|
||||
/** @var \App\Models\User */
|
||||
$user = auth()->user();
|
||||
return [
|
||||
'site_id' => [
|
||||
'required',
|
||||
|
|
@ -93,7 +96,16 @@ class StoreTicket extends FormRequest
|
|||
'text' => 'required|string|min:40',
|
||||
'file_ticket' => 'mimes:jpeg,jpg,png,pdf|max:2048',
|
||||
'file_ticket2' => 'mimes:jpeg,jpg,png,pdf|max:2048',
|
||||
'is_internal' => 'required|boolean'
|
||||
'is_internal' => 'required|boolean',
|
||||
'labels' => [
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
|
||||
'sometimes', 'array', function ($attribute, $value, $fail) use ($user) {
|
||||
if (!$user->can('assignLabels', Ticket::class)) {
|
||||
$fail(trans('Vous n\'êtes pas autorisé à modifier cette valeur'));
|
||||
}
|
||||
}
|
||||
],
|
||||
'labels.*' => 'required|exists:labels,id'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@
|
|||
namespace App\Lib;
|
||||
|
||||
use App\Enums\LoadLevel;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
|
|
@ -252,4 +255,35 @@ class ToolboxHelper
|
|||
}
|
||||
return $fullPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert Is Unique
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param array<string> $columns
|
||||
* @return void
|
||||
* @throws \Illuminate\Validation\ValidationException If a model like this already exists.
|
||||
*/
|
||||
public static function assertIsUnique(Model $model, array $columns): void
|
||||
{
|
||||
$columns = collect($columns);
|
||||
/** @var string|null */
|
||||
$firstColumn = $columns->pull($columns->keys()->first());
|
||||
if (!$firstColumn) {
|
||||
return;
|
||||
}
|
||||
$modelClass = \get_class($model);
|
||||
$uniqueRule = Rule::unique((new $modelClass())->getTable());
|
||||
if ($model->id) {
|
||||
$uniqueRule->ignore($model->id);
|
||||
}
|
||||
$uniqueRule->where(function ($q) use ($model, $columns) {
|
||||
$columns->map(function (string $column) use ($model, $q) {
|
||||
$q->where($column, $model->{$column});
|
||||
});
|
||||
});
|
||||
Validator::make([$firstColumn => $model->{$firstColumn}], [
|
||||
$firstColumn => ['required', $uniqueRule]
|
||||
])->validate();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
117
app/Models/Brand.php
Normal file
117
app/Models/Brand.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Credit\Account;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
|
||||
use Kwaadpepper\CrudPolicies\Enums\CrudType;
|
||||
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
|
||||
|
||||
class Brand extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use IsCrudModel;
|
||||
|
||||
/**
|
||||
* Crud Label
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $crudLabelColumn = 'name';
|
||||
|
||||
/**
|
||||
* Crud Value
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $crudValueColumn = 'id';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name'
|
||||
];
|
||||
|
||||
/**
|
||||
* User
|
||||
*
|
||||
* @param array $attributes
|
||||
*/
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
self::$editableProperties = [
|
||||
'name' => [
|
||||
'label' => trans('Nom'),
|
||||
'type' => CrudType::string(),
|
||||
'validate' => [
|
||||
'string',
|
||||
'max:255'
|
||||
],
|
||||
'required' => true,
|
||||
'nullable' => false,
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::view(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update(),
|
||||
]
|
||||
],
|
||||
'sites' => [
|
||||
'label' => trans('models.classes.sites'),
|
||||
'type' => CrudType::hasMany(),
|
||||
'hasMany' => Site::class,
|
||||
'hasManyLabel' => 'name',
|
||||
'nullable' => true,
|
||||
'readonly' => false,
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::view(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update()
|
||||
]
|
||||
],
|
||||
'accounts' => [
|
||||
'label' => trans('models.classes.accounts'),
|
||||
'type' => CrudType::hasMany(),
|
||||
'hasMany' => Account::class,
|
||||
'hasManyLabel' => 'label',
|
||||
'nullable' => true,
|
||||
'readonly' => false,
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::view(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update()
|
||||
]
|
||||
],
|
||||
];
|
||||
parent::__construct($attributes);
|
||||
}
|
||||
|
||||
// * RELATIONSHIPS
|
||||
|
||||
/**
|
||||
* HasMany Site relation
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function sites()
|
||||
{
|
||||
return $this->hasMany(Site::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* HasMany Account relation
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function accounts()
|
||||
{
|
||||
return $this->hasMany(Account::class);
|
||||
}
|
||||
}
|
||||
144
app/Models/Credit/Account.php
Normal file
144
app/Models/Credit/Account.php
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models\Credit;
|
||||
|
||||
use App\Enums\Credit\AccountType;
|
||||
use App\Models\Brand;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
|
||||
use Kwaadpepper\CrudPolicies\Enums\CrudType;
|
||||
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
|
||||
use Kwaadpepper\Enum\Traits\CastsEnums;
|
||||
|
||||
class Account extends Model
|
||||
{
|
||||
use CastsEnums;
|
||||
use IsCrudModel;
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'credit_accounts';
|
||||
|
||||
/**
|
||||
* Crud Label
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $crudLabelColumn = 'label';
|
||||
|
||||
/**
|
||||
* Crud Value
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $crudValueColumn = 'id';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'label', 'type',
|
||||
'balance', 'brand_id'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to Enums.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $enumCasts = [
|
||||
'type' => AccountType::class
|
||||
];
|
||||
|
||||
/**
|
||||
* Account
|
||||
*
|
||||
* @param array $attributes
|
||||
*/
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
self::$editableProperties = [
|
||||
'label' => [
|
||||
'label' => trans('Label'),
|
||||
'placeholder' => trans('Nom du compte'),
|
||||
'type' => CrudType::string(),
|
||||
'validate' => [
|
||||
'string',
|
||||
'max:255'
|
||||
],
|
||||
'required' => true,
|
||||
'nullable' => false,
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::view(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update(),
|
||||
]
|
||||
],
|
||||
'type' => [
|
||||
'label' => trans('Type'),
|
||||
'type' => CrudType::enum(),
|
||||
'enum' => AccountType::class,
|
||||
'required' => true,
|
||||
'nullable' => false,
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::view(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update(),
|
||||
]
|
||||
],
|
||||
'balance' => [
|
||||
'label' => trans('Solde'),
|
||||
'type' => CrudType::float(),
|
||||
'required' => false,
|
||||
'nullable' => false,
|
||||
'readonly' => true,
|
||||
'step' => 0.5,
|
||||
'validate' => [
|
||||
'numeric',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (($value * 10) % 5 !== 0) {
|
||||
$fail("$attribute doit être divisible par 0.5 jour/homme");
|
||||
}
|
||||
},
|
||||
],
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::view(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update(),
|
||||
]
|
||||
],
|
||||
'brand' => [
|
||||
'label' => Str::singular(trans('models.classes.brands')),
|
||||
'type' => CrudType::belongsTo(),
|
||||
'belongsTo' => Brand::class,
|
||||
'required' => false,
|
||||
'nullable' => false,
|
||||
'actions' => [
|
||||
CrudAction::viewAny()
|
||||
]
|
||||
]
|
||||
];
|
||||
parent::__construct($attributes);
|
||||
}
|
||||
|
||||
// * RELATIONSHIPS
|
||||
|
||||
/**
|
||||
* BelongsTo Brand relation
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function brand()
|
||||
{
|
||||
return $this->belongsTo(Brand::class);
|
||||
}
|
||||
}
|
||||
122
app/Models/Credit/CreditLog.php
Normal file
122
app/Models/Credit/CreditLog.php
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models\Credit;
|
||||
|
||||
use App\Enums\Credit\BalanceOperation;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
|
||||
use Kwaadpepper\CrudPolicies\Enums\CrudType;
|
||||
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
|
||||
use Kwaadpepper\Enum\Traits\CastsEnums;
|
||||
|
||||
class CreditLog extends Model
|
||||
{
|
||||
use CastsEnums;
|
||||
use IsCrudModel;
|
||||
|
||||
/**
|
||||
* Crud Label
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $crudLabelColumn = 'type';
|
||||
|
||||
/**
|
||||
* Crud Value
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $crudValueColumn = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to Enums.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $enumCasts = [
|
||||
'type' => BalanceOperation::class
|
||||
];
|
||||
|
||||
/**
|
||||
* Account
|
||||
*
|
||||
* @param array $attributes
|
||||
*/
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
self::$editableProperties = [
|
||||
'amount' => [
|
||||
'label' => trans('Montant'),
|
||||
'type' => CrudType::float(),
|
||||
'actions' => [
|
||||
CrudAction::viewAny()
|
||||
]
|
||||
],
|
||||
'type' => [
|
||||
'label' => trans('Opération'),
|
||||
'type' => CrudType::enum(),
|
||||
'enum' => BalanceOperation::class,
|
||||
'actions' => [
|
||||
CrudAction::viewAny()
|
||||
]
|
||||
],
|
||||
'created_at' => [
|
||||
'label' => trans('Date'),
|
||||
'type' => CrudType::datetime(),
|
||||
'actions' => [
|
||||
CrudAction::viewAny()
|
||||
]
|
||||
]
|
||||
];
|
||||
parent::__construct($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any actions required after the model boots.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function (self $model) {
|
||||
$model->created_at = now();
|
||||
if ($model->type->equals(BalanceOperation::debit())) {
|
||||
$model->account->balance = $model->account->balance - $model->amount;
|
||||
}
|
||||
if ($model->type->equals(BalanceOperation::credit())) {
|
||||
$model->account->balance = $model->account->balance + $model->amount;
|
||||
}
|
||||
$model->account->saveOrFail();
|
||||
});
|
||||
}
|
||||
|
||||
// * RELATIONSHIPS
|
||||
|
||||
/**
|
||||
* BelongsToMany estimation relation (1 nullable)
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function estimation()
|
||||
{
|
||||
return $this->belongsToMany(Estimation::class, EstimationLog::class, 'credit_log_id', 'credit_estimation_id')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* BelongsTo account relation
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function account()
|
||||
{
|
||||
return $this->belongsTo(Account::class, 'credit_account_id');
|
||||
}
|
||||
}
|
||||
108
app/Models/Credit/Estimation.php
Normal file
108
app/Models/Credit/Estimation.php
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models\Credit;
|
||||
|
||||
use App\Enums\Credit\BalanceOperation;
|
||||
use App\Enums\Credit\EstimationStatus;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Kwaadpepper\Enum\Traits\CastsEnums;
|
||||
|
||||
class Estimation extends Model
|
||||
{
|
||||
use CastsEnums;
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to Enums.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $enumCasts = [
|
||||
'status' => EstimationStatus::class
|
||||
];
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'credit_estimations';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'amount', 'status', 'credit_account_id'
|
||||
];
|
||||
|
||||
/**
|
||||
* Perform any actions required after the model boots.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function booted()
|
||||
{
|
||||
static::updating(function (self $model) {
|
||||
if (!$model->original['validator_id'] !== $model->validator_id) {
|
||||
$log = new CreditLog();
|
||||
$log->account()->associate($model->account);
|
||||
$log->type = BalanceOperation::debit();
|
||||
$log->amount = $model->amount;
|
||||
$log->saveOrFail();
|
||||
$log->estimation()->sync([$model->id]);
|
||||
}
|
||||
if ($model->status->equals(EstimationStatus::pending())) {
|
||||
throw ValidationException::withMessages([trans('Impossible de modifier une estimation')]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// * RELATIONSHIPS
|
||||
|
||||
/**
|
||||
* BelongsTo Ticket relation
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function ticket()
|
||||
{
|
||||
return $this->belongsTo(Ticket::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* BelongsTo Credit\Account relation
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function account()
|
||||
{
|
||||
return $this->belongsTo(Account::class, 'credit_account_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* BelongsTo User relation
|
||||
* The use who created the estimate
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function estimator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'estimator_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* BelongsTo User relation (nullable)
|
||||
* The use who validated the estimate
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function validator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'validator_id');
|
||||
}
|
||||
}
|
||||
15
app/Models/Credit/EstimationLog.php
Normal file
15
app/Models/Credit/EstimationLog.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models\Credit;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
|
||||
class EstimationLog extends Pivot
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'credit_estimation_log';
|
||||
}
|
||||
136
app/Models/Label.php
Normal file
136
app/Models/Label.php
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Casts\HtmlColor;
|
||||
use App\Lib\ToolboxHelper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
|
||||
use Kwaadpepper\CrudPolicies\Enums\CrudType;
|
||||
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
|
||||
use LVR\Colour\Hex;
|
||||
|
||||
class Label extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use IsCrudModel;
|
||||
|
||||
/**
|
||||
* Crud Label
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $crudLabelColumn = 'name';
|
||||
|
||||
/**
|
||||
* Crud Value
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $crudValueColumn = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'color' => HtmlColor::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name', 'color'
|
||||
];
|
||||
|
||||
/**
|
||||
* Site
|
||||
*
|
||||
* @param array $attributes
|
||||
*/
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
self::$editableProperties = [
|
||||
'name' => [
|
||||
'name' => trans('Libellé'),
|
||||
'type' => CrudType::string(),
|
||||
'validate' => [
|
||||
'required',
|
||||
'min:2',
|
||||
'max:255',
|
||||
(new Unique('labels', 'name'))->ignore(request()->label)
|
||||
],
|
||||
'required' => true,
|
||||
'nullable' => false,
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update(),
|
||||
]
|
||||
],
|
||||
'color' => [
|
||||
'label' => trans('Couleur'),
|
||||
'type' => CrudType::color(),
|
||||
'validate' => ['required', new Hex()],
|
||||
'required' => true,
|
||||
'nullable' => false,
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update(),
|
||||
]
|
||||
]
|
||||
];
|
||||
parent::__construct($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any actions required after the model boots.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function (self $model) {
|
||||
ToolboxHelper::assertIsUnique($model, ['name']);
|
||||
});
|
||||
static::updating(function (self $model) {
|
||||
ToolboxHelper::assertIsUnique($model, ['name']);
|
||||
});
|
||||
}
|
||||
|
||||
// * RELATIONSHIPS
|
||||
|
||||
/**
|
||||
* BelongsToMany Ticket relation
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function tickets()
|
||||
{
|
||||
return $this->belongsToMany(Ticket::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* BelongsTo Site relation
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function site()
|
||||
{
|
||||
return $this->belongsTo(Site::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -146,7 +146,7 @@ class Site extends Model
|
|||
]
|
||||
],
|
||||
'projects' => [
|
||||
'label' => trans('Projets'),
|
||||
'label' => trans('models.classes.projects'),
|
||||
'type' => CrudType::belongsToMany(),
|
||||
'belongsToMany' => Project::class,
|
||||
'belongsToManyLabel' => 'name',
|
||||
|
|
@ -174,31 +174,12 @@ class Site extends Model
|
|||
*/
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function (Site $site) {
|
||||
$path = Site::ticketAssetsFullPath($site->slug_name);
|
||||
if (!File::exists($path)) {
|
||||
File::makeDirectory($path, 0755, true);
|
||||
}
|
||||
static::created(function (self $site) {
|
||||
Label::createDefaultLabelsFor($site);
|
||||
});
|
||||
static::updating(function (Site $site) {
|
||||
$oldName = Site::where('id', $site->id)->first()->name;
|
||||
$oldPath = Site::ticketAssetsFullPath($oldName);
|
||||
$newPath = Site::ticketAssetsFullPath($site->slug_name);
|
||||
if (File::exists($oldPath)) {
|
||||
File::move($oldPath, $newPath);
|
||||
} elseif (!File::exists($newPath)) {
|
||||
File::makeDirectory($newPath, 0755, true);
|
||||
}
|
||||
});
|
||||
static::updated(function (Site $site) {
|
||||
static::updated(function (self $site) {
|
||||
event(new SiteUpdated($site, \auth()->user()));
|
||||
});
|
||||
static::deleted(function (Site $site) {
|
||||
$path = Site::ticketAssetsFullPath($site->slug_name);
|
||||
if (File::exists($path)) {
|
||||
File::deleteDirectory($path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// * RELATIONSHIPS
|
||||
|
|
@ -224,6 +205,16 @@ class Site extends Model
|
|||
return $this->hasMany(Ticket::class, 'site_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* BelongsTo Brand relation
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function brand()
|
||||
{
|
||||
return $this->belongsTo(Brand::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* BelongsToMany Project relation
|
||||
*
|
||||
|
|
@ -265,14 +256,4 @@ class Site extends Model
|
|||
{
|
||||
return Str::slug($this->name);
|
||||
}
|
||||
|
||||
public static function ticketAssetsFullPath(string $str)
|
||||
{
|
||||
return public_path(self::ticketAssetsPath($str));
|
||||
}
|
||||
|
||||
public static function ticketAssetsPath(string $str)
|
||||
{
|
||||
return sprintf('fichiers/%s', $str);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ use App\Events\NotifySubscriptions;
|
|||
use App\Events\TicketStatusChanged;
|
||||
use App\Jobs\SendTicketMentionMail;
|
||||
use App\Mail\TicketMention;
|
||||
use App\Models\Credit\Account;
|
||||
use App\Models\Credit\Estimation;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
|
|
@ -89,6 +91,16 @@ class Ticket extends Model
|
|||
|
||||
// * RELATIONSHIPS
|
||||
|
||||
/**
|
||||
* HasOne Credit\Estimation relation
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function estimation()
|
||||
{
|
||||
return $this->hasOne(Estimation::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* BelongsTo Site relation
|
||||
*
|
||||
|
|
@ -142,6 +154,16 @@ class Ticket extends Model
|
|||
return $this->belongsToMany(User::class, 'subscriptions');
|
||||
}
|
||||
|
||||
/**
|
||||
* BelongsToMany Label relation
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function labels()
|
||||
{
|
||||
return $this->belongsToMany(Label::class, 'label_ticket');
|
||||
}
|
||||
|
||||
// * METHODS
|
||||
|
||||
/**
|
||||
|
|
@ -193,4 +215,37 @@ class Ticket extends Model
|
|||
}//end foreach
|
||||
return $usersNotified;
|
||||
}
|
||||
|
||||
/**
|
||||
* full_name getter
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFullNameAttribute(): string
|
||||
{
|
||||
return trans('Ticket n° :ticketId : :ticketSiteName - :ticketTypeLabel', [
|
||||
'ticketId' => $this->id,
|
||||
'ticketSiteName' => $this->site->name,
|
||||
'ticketTypeLabel' => $this->type->label
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an account has the same brand as this ticket.
|
||||
*
|
||||
* @param Account $account
|
||||
* @return boolean
|
||||
*/
|
||||
public function accountIsValidForThisTicket(Account $account): bool
|
||||
{
|
||||
/** @var \App\Models\Brand|null */
|
||||
if (!($siteBrand = optional($this->site)->brand)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var \App\Models\Brand */
|
||||
$accountBrand = $account->brand;
|
||||
|
||||
return !\is_null($accountBrand->id) and $accountBrand->id === $siteBrand->id;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
39
app/Policies/AccountPolicy.php
Normal file
39
app/Policies/AccountPolicy.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Policies\Rules\UserRoleRules as URR;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
|
||||
|
||||
class AccountPolicy extends RootPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Init policies on call
|
||||
*
|
||||
* @param User $user
|
||||
* @param Model $model
|
||||
* @return void
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
*/
|
||||
public function setPolicies(User $user, Model $model = null)
|
||||
{
|
||||
/**
|
||||
* This first rule to return true in a policy action authorises the action
|
||||
*/
|
||||
$this->policies = [
|
||||
'account' => [
|
||||
'viewAny' => URR::make('allowEveryOne'),
|
||||
'view' => URR::make('denyEveryOne'),
|
||||
'update' => URR::make('denyEveryOne'),
|
||||
'create' => URR::make('userIsAdmin', $user),
|
||||
'delete' => URR::make('userIsAdmin', $user),
|
||||
'credit' => URR::make('userIsAdmin', $user)
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
38
app/Policies/BrandPolicy.php
Normal file
38
app/Policies/BrandPolicy.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Policies\Rules\UserRoleRules as URR;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
|
||||
|
||||
class BrandPolicy extends RootPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Init policies on call
|
||||
*
|
||||
* @param User $user
|
||||
* @param Model $model
|
||||
* @return void
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
*/
|
||||
public function setPolicies(User $user, Model $model = null)
|
||||
{
|
||||
/**
|
||||
* This first rule to return true in a policy action authorises the action
|
||||
*/
|
||||
$this->policies = [
|
||||
'brand' => [
|
||||
'viewAny' => URR::make('userIsAdmin', $user),
|
||||
'view' => URR::make('userIsAdmin', $user),
|
||||
'update' => URR::make('userIsAdmin', $user),
|
||||
'create' => URR::make('userIsAdmin', $user),
|
||||
'delete' => URR::make('userIsAdmin', $user)
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Policies/CreditLogPolicy.php
Normal file
34
app/Policies/CreditLogPolicy.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Policies\Rules\UserRoleRules as URR;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
|
||||
|
||||
class CreditLogPolicy extends RootPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Init policies on call
|
||||
*
|
||||
* @param User $user
|
||||
* @param Model $model
|
||||
* @return void
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
*/
|
||||
public function setPolicies(User $user, Model $model = null)
|
||||
{
|
||||
/**
|
||||
* This first rule to return true in a policy action authorises the action
|
||||
*/
|
||||
$this->policies = [
|
||||
'creditlog' => [
|
||||
'viewAny' => URR::make('allowEveryOne')
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Policies/EstimationPolicy.php
Normal file
34
app/Policies/EstimationPolicy.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Policies\Rules\EstimationStaticRules;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
|
||||
|
||||
class EstimationPolicy extends RootPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Init policies on call
|
||||
*
|
||||
* @param User $user
|
||||
* @param Model $model
|
||||
* @return void
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
|
||||
*/
|
||||
public function setPolicies(User $user, Model $model = null)
|
||||
{
|
||||
/**
|
||||
* This first rule to return true in a policy action authorises the action
|
||||
*/
|
||||
$this->policies = [
|
||||
'estimation' => [
|
||||
'answer' => EstimationStaticRules::make('estimateIsPending', $model)
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
37
app/Policies/LabelPolicy.php
Normal file
37
app/Policies/LabelPolicy.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Policies\Rules\UserRoleRules as URR;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
|
||||
|
||||
class LabelPolicy extends RootPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Init policies on call
|
||||
*
|
||||
* @param User $user
|
||||
* @param Model $model
|
||||
* @return void
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
*/
|
||||
public function setPolicies(User $user, Model $model = null)
|
||||
{
|
||||
/**
|
||||
* This first rule to return true in a policy action authorises the action
|
||||
*/
|
||||
$this->policies = [
|
||||
'label' => [
|
||||
'viewAny' => URR::make('userIsAdmin', $user),
|
||||
'create' => URR::make('userIsAdmin', $user),
|
||||
'update' => URR::make('userIsAdmin', $user),
|
||||
'delete' => URR::make('userIsAdmin', $user)
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -17,11 +17,11 @@ class LogPolicy extends RootPolicy
|
|||
/**
|
||||
* Init policies on call
|
||||
*
|
||||
* @param User $user
|
||||
* @param User $user
|
||||
* @param Model $model
|
||||
* @return void
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
*/
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
public function setPolicies(User $user, Model $model = null)
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
25
app/Policies/Rules/EstimationStaticRules.php
Normal file
25
app/Policies/Rules/EstimationStaticRules.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies\Rules;
|
||||
|
||||
use App\Enums\Credit\EstimationStatus;
|
||||
use App\Models\Credit\Estimation;
|
||||
use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules;
|
||||
|
||||
/**
|
||||
* Estimation Policiy Static Rules
|
||||
* ! Rules has to return boolean value
|
||||
*/
|
||||
abstract class EstimationStaticRules extends StaticRules
|
||||
{
|
||||
/**
|
||||
* Estimation is pending
|
||||
*
|
||||
* @param Estimation $model
|
||||
* @return boolean
|
||||
*/
|
||||
protected static function estimateIsPending(Estimation $model): bool
|
||||
{
|
||||
return $model->status->equals(EstimationStatus::pending());
|
||||
}
|
||||
}
|
||||
|
|
@ -7,12 +7,19 @@ use App\Models\User;
|
|||
use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules;
|
||||
|
||||
/**
|
||||
* Userlevel Policiy Static Rules
|
||||
* Log Policiy Static Rules
|
||||
* ! Rules has to return boolean value
|
||||
*/
|
||||
abstract class LogStaticRules extends StaticRules
|
||||
{
|
||||
protected static function logIsOwnedByUser(User $user, Log $log)
|
||||
/**
|
||||
* Log Is owned by user.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Log $log
|
||||
* @return boolean
|
||||
*/
|
||||
protected static function logIsOwnedByUser(User $user, Log $log): bool
|
||||
{
|
||||
return $log->user->id === $user->id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use App\Models\Ticket;
|
|||
use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules;
|
||||
|
||||
/**
|
||||
* Userlevel Policiy Static Rules
|
||||
* Ticket Policiy Static Rules
|
||||
* ! Rules has to return boolean value
|
||||
*/
|
||||
abstract class TicketStaticRules extends StaticRules
|
||||
|
|
@ -33,4 +33,52 @@ abstract class TicketStaticRules extends StaticRules
|
|||
{
|
||||
return !$ticket->status->equals(TicketStatus::archived());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticket has brand which has at least one account
|
||||
*
|
||||
* @param Ticket $ticket
|
||||
* @return boolean
|
||||
*/
|
||||
protected static function ticketsBrandHasAnAccount(Ticket $ticket): bool
|
||||
{
|
||||
/** @var \App\Models\Brand|null */
|
||||
if (!($brand = $ticket->site->brand)) {
|
||||
return false;
|
||||
}
|
||||
return (bool)$brand->accounts()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticket does already have an estimation
|
||||
*
|
||||
* @param Ticket $ticket
|
||||
* @return boolean
|
||||
*/
|
||||
protected static function ticketDoesHaveEstimation(Ticket $ticket): bool
|
||||
{
|
||||
return $ticket->estimation()->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticket does not already have an estimation
|
||||
*
|
||||
* @param Ticket $ticket
|
||||
* @return boolean
|
||||
*/
|
||||
protected static function ticketDoesntHaveEstimation(Ticket $ticket): bool
|
||||
{
|
||||
return !$ticket->estimation()->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticket is not internal
|
||||
*
|
||||
* @param Ticket $ticket
|
||||
* @return boolean
|
||||
*/
|
||||
protected static function ticketIsNotInternal(Ticket $ticket): bool
|
||||
{
|
||||
return !(bool)$ticket->is_internal;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,32 +7,60 @@ use App\Models\User;
|
|||
use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules;
|
||||
|
||||
/**
|
||||
* Userlevel Policiy Static Rules
|
||||
* User Policiy Static Rules
|
||||
* ! Rules has to return boolean value
|
||||
*/
|
||||
abstract class UserRoleRules extends StaticRules
|
||||
{
|
||||
protected static function userIsAdmin(User $user)
|
||||
/**
|
||||
* User is admin
|
||||
*
|
||||
* @param User $user
|
||||
* @return boolean
|
||||
*/
|
||||
protected static function userIsAdmin(User $user): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
protected static function userIsDev(User $user)
|
||||
/**
|
||||
* User Is Dev
|
||||
*
|
||||
* @param User $user
|
||||
* @return boolean
|
||||
*/
|
||||
protected static function userIsDev(User $user): bool
|
||||
{
|
||||
return $user->role->equals(Role::developper());
|
||||
}
|
||||
|
||||
protected static function userIsClient(User $user)
|
||||
/**
|
||||
* User Is Client
|
||||
*
|
||||
* @param User $user
|
||||
* @return boolean
|
||||
*/
|
||||
protected static function userIsClient(User $user): bool
|
||||
{
|
||||
return $user->role->equals(Role::client());
|
||||
}
|
||||
|
||||
protected static function allowEveryOne()
|
||||
/**
|
||||
* Allow every one
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected static function allowEveryOne(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function denyEveryOne()
|
||||
/**
|
||||
* Deny everyone
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected static function denyEveryOne(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ class SitePolicy extends RootPolicy
|
|||
/**
|
||||
* Init policies on call
|
||||
*
|
||||
* @param User $user
|
||||
* @param User $user
|
||||
* @param Model $model
|
||||
* @return void
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
*/
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
public function setPolicies(User $user, Model $model = null)
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -55,6 +55,22 @@ class TicketPolicy extends RootPolicy
|
|||
URR::make('userIsAdmin', $user),
|
||||
TicketStaticRules::make('ticketIsNotInProduction', $model),
|
||||
TicketStaticRules::make('ticketIsNotArchived', $model)
|
||||
]),
|
||||
'assignTickets' => URR::make('userIsAdmin', $user),
|
||||
'assignLabels' => URR::make('userIsAdmin', $user),
|
||||
'estimate' => StaticRules::and([
|
||||
URR::make('userIsAdmin', $user),
|
||||
TicketStaticRules::make('ticketsBrandHasAnAccount', $model),
|
||||
TicketStaticRules::make('ticketDoesntHaveEstimation', $model),
|
||||
TicketStaticRules::make('ticketIsNotInternal', $model),
|
||||
TicketStaticRules::make('ticketIsNotArchived', $model)
|
||||
]),
|
||||
'validateEstimate' => StaticRules::and([
|
||||
URR::make('userIsClient', $user),
|
||||
TicketStaticRules::make('ticketsBrandHasAnAccount', $model),
|
||||
TicketStaticRules::make('ticketDoesHaveEstimation', $model),
|
||||
TicketStaticRules::make('ticketIsNotInternal', $model),
|
||||
TicketStaticRules::make('ticketIsNotArchived', $model)
|
||||
])
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Brand;
|
||||
use App\Models\Credit\Account;
|
||||
use App\Models\File;
|
||||
use App\Models\Label;
|
||||
use App\Models\Log;
|
||||
use App\Models\MacroSchedule\Marker;
|
||||
use App\Models\Project;
|
||||
|
|
@ -38,15 +41,13 @@ class RouteServiceProvider extends ServiceProvider
|
|||
$this->cspReportRoute();
|
||||
|
||||
$this->routes(function () {
|
||||
Route::prefix('api')
|
||||
->middleware('api')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/api.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/web.php'));
|
||||
});
|
||||
Route::model('account', Account::class);
|
||||
Route::model('brand', Brand::class);
|
||||
Route::model('label', Label::class);
|
||||
Route::model('marker', Marker::class);
|
||||
Route::model('file', File::class);
|
||||
Route::model('log', Log::class);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ldcweb/supportweb",
|
||||
"version": "1.0.9",
|
||||
"version": "1.2.0",
|
||||
"type": "project",
|
||||
"description": "The Laravel Framework.",
|
||||
"keywords": [
|
||||
|
|
@ -15,10 +15,11 @@
|
|||
"fideloper/proxy": "^4.4",
|
||||
"fruitcake/laravel-cors": "^2.0",
|
||||
"guzzlehttp/guzzle": "^7.0.1",
|
||||
"kwaadpepper/crud-policies": "^0.3.6",
|
||||
"kwaadpepper/crud-policies": "^0.4.0",
|
||||
"kwaadpepper/enum": "^2.1",
|
||||
"kwaadpepper/exception-handler": "^0.0.5",
|
||||
"laravel-lang/lang": "~8.0",
|
||||
"laravel-validation-rules/colour": "^1.6",
|
||||
"laravel/framework": "^8.12",
|
||||
"laravel/tinker": "^2.5",
|
||||
"laravel/ui": "^3.3",
|
||||
|
|
|
|||
67
composer.lock
generated
67
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "2a618153177d1ef96c1f71551d669f8c",
|
||||
"content-hash": "22dfaf5696ddcb98ef8764d0f03a6ba0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "asm89/stack-cors",
|
||||
|
|
@ -1505,16 +1505,16 @@
|
|||
},
|
||||
{
|
||||
"name": "kwaadpepper/crud-policies",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Kwaadpepper/crud-policies.git",
|
||||
"reference": "205b8a16c99c362fc78422eeb31c0ba618a3d351"
|
||||
"reference": "91616b1788fb581a15473f435fad2c0dbdd07f4c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Kwaadpepper/crud-policies/zipball/205b8a16c99c362fc78422eeb31c0ba618a3d351",
|
||||
"reference": "205b8a16c99c362fc78422eeb31c0ba618a3d351",
|
||||
"url": "https://api.github.com/repos/Kwaadpepper/crud-policies/zipball/91616b1788fb581a15473f435fad2c0dbdd07f4c",
|
||||
"reference": "91616b1788fb581a15473f435fad2c0dbdd07f4c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -1565,9 +1565,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Kwaadpepper/crud-policies/issues",
|
||||
"source": "https://github.com/Kwaadpepper/crud-policies/tree/0.3.6"
|
||||
"source": "https://github.com/Kwaadpepper/crud-policies/tree/0.4.0"
|
||||
},
|
||||
"time": "2022-05-04T13:19:25+00:00"
|
||||
"time": "2022-05-31T15:35:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "kwaadpepper/enum",
|
||||
|
|
@ -1743,6 +1743,59 @@
|
|||
},
|
||||
"time": "2021-04-14T17:41:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel-validation-rules/colour",
|
||||
"version": "1.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel-validation-rules/colour.git",
|
||||
"reference": "1e7ab2420879738f45adcf0320f8c571789e9fb4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel-validation-rules/colour/zipball/1e7ab2420879738f45adcf0320f8c571789e9fb4",
|
||||
"reference": "1e7ab2420879738f45adcf0320f8c571789e9fb4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/contracts": "^5.5|^6.0|^7.0|^8.0|^9.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"orchestra/testbench": "^5.0|^7.0",
|
||||
"phpunit/phpunit": "^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"LVR\\Colour\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ashley Clarke",
|
||||
"email": "me@ashleyclarke.me"
|
||||
},
|
||||
{
|
||||
"name": "Scott Robinson",
|
||||
"email": "scott@dor.ky"
|
||||
}
|
||||
],
|
||||
"description": "Validate colours",
|
||||
"keywords": [
|
||||
"colour",
|
||||
"laravel",
|
||||
"validation"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel-validation-rules/colour/issues",
|
||||
"source": "https://github.com/laravel-validation-rules/colour/tree/1.6.0"
|
||||
},
|
||||
"time": "2022-02-08T08:13:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v8.83.13",
|
||||
|
|
|
|||
|
|
@ -39,13 +39,7 @@ return [
|
|||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'driver' => 'token',
|
||||
'provider' => 'users',
|
||||
'hash' => false,
|
||||
],
|
||||
]
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
50
config/crud-policies.php
Normal file
50
config/crud-policies.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
* The url prefix of Omen file manager
|
||||
** default is 'crud-policies'
|
||||
*! The url prefix must not match any file in public folder
|
||||
*/
|
||||
'urlPrefix' => 'crud-policies',
|
||||
|
||||
/**
|
||||
* You shall protect routes
|
||||
* These are applied to crud routes directly as middlewares
|
||||
*/
|
||||
'middlewares' => [
|
||||
'web',
|
||||
'auth'
|
||||
],
|
||||
|
||||
/**
|
||||
* The public path where are stored
|
||||
* crud-policies asset files
|
||||
*
|
||||
* Change this if you want to modify
|
||||
* published assets folder under /public
|
||||
** default is 'vendor/crud-policies'
|
||||
*/
|
||||
'assetPath' => 'vendor/crud-policies',
|
||||
|
||||
/**
|
||||
* This is the package layout that will be used
|
||||
* to display crud elements
|
||||
*
|
||||
* You can publish views to know what sections it
|
||||
* has to display
|
||||
*/
|
||||
'viewLayout' => 'crud-policies::crud.layout',
|
||||
|
||||
/**
|
||||
* Controls whether to display flash messages
|
||||
* on actions such as create, update or delete.
|
||||
*/
|
||||
'displayAlerts' => true,
|
||||
|
||||
/**
|
||||
* Controls whether to show delete button on index routes
|
||||
*/
|
||||
'deleteOnIndex' => true
|
||||
];
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Label;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateLabelsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('labels', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique()->comment('Un ou deux mot désignant le label');
|
||||
$table->unsignedInteger('color')->comment('Cast HTML color');
|
||||
});
|
||||
|
||||
$defaultLabels = [
|
||||
['name' => 'urgent', 'color' => '#ebb734'],
|
||||
['name' => 'bloquant', 'color' => '#851400']
|
||||
];
|
||||
collect($defaultLabels)->map(function (array $defaultLabel) {
|
||||
$label = new Label();
|
||||
$label->name = $defaultLabel['name'];
|
||||
$label->color = $defaultLabel['color'];
|
||||
$label->saveOrFail();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('labels');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateLabelTicketTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('label_ticket', function (Blueprint $table) {
|
||||
$table->foreignIdFor(Ticket::class)
|
||||
->constrained()->cascadeOnDelete();
|
||||
$table->foreignIdFor(Label::class)
|
||||
->constrained()->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('label_ticket');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateBrandsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('brands', function (Blueprint $table) {
|
||||
$table->id()->comment('Doit correspondre aux ids marques de la CRM');
|
||||
|
||||
$table->string('name')->comment('Nom de la marque');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('brands');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Brand;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddBrandIdToSites extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('sites', function (Blueprint $table) {
|
||||
$table->foreignIdFor(Brand::class)
|
||||
->nullable()
|
||||
->comment('Relation is nullable, since not all site would be affiliate with a brand.')
|
||||
->after('todo')
|
||||
->constrained();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('sites', function (Blueprint $table) {
|
||||
$table->dropForeign(['brand_id']);
|
||||
$table->dropColumn('brand_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Brand;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateCreditAccountsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('credit_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->unsignedTinyInteger('type')->comment('Enum Credit\\AccountType');
|
||||
|
||||
$table->string('label')->comment('Custom account label');
|
||||
$table->float('balance')->comment('Account balance');
|
||||
|
||||
$table->foreignIdFor(Brand::class)
|
||||
->constrained();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('credit_accounts');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateCreditLogsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('credit_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->unsignedTinyInteger('type')->comment('Enum Credit\\BalanceOperation');
|
||||
$table->unsignedFloat('amount')->comment('Montant de l\'opération');
|
||||
|
||||
$table->foreignId('credit_account_id')
|
||||
->constrained('credit_accounts')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->timestamp('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('credit_logs');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\Credit\EstimationStatus;
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateCreditEstimationsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('credit_estimations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->unsignedTinyInteger('status')->default(EstimationStatus::pending())
|
||||
->comment('Enum Credit\\EstimationStatus');
|
||||
|
||||
$table->unsignedFloat('amount')->comment('Estimation amount');
|
||||
|
||||
$table->foreignIdFor(Ticket::class)
|
||||
->constrained();
|
||||
|
||||
$table->foreignId('credit_account_id')
|
||||
->comment('The account to debit.')
|
||||
->constrained('credit_accounts');
|
||||
$table->foreignId('estimator_id')
|
||||
->comment('The person who has done the estimate.')
|
||||
->constrained('users');
|
||||
$table->foreignId('validator_id')
|
||||
->nullable()
|
||||
->comment('The person who has validate the estimate.')
|
||||
->constrained('users');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('credit_estimations');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateCreditEstimationLogTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('credit_estimation_log', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('credit_log_id')
|
||||
->constrained('credit_logs')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->foreignId('credit_estimation_id')
|
||||
->constrained('credit_estimations')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('credit_estimation_log');
|
||||
}
|
||||
}
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -6,6 +6,7 @@
|
|||
"": {
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-async-to-generator": "^7.16.8",
|
||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||
"autoprefixer": "10.4.5",
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^5.1.3",
|
||||
|
|
@ -1656,6 +1657,16 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.1.1.tgz",
|
||||
"integrity": "sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
||||
|
|
@ -11719,6 +11730,12 @@
|
|||
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
|
||||
"dev": true
|
||||
},
|
||||
"@fortawesome/fontawesome-free": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.1.1.tgz",
|
||||
"integrity": "sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/gen-mapping": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-async-to-generator": "^7.16.8",
|
||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||
"autoprefixer": "10.4.5",
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^5.1.3",
|
||||
|
|
|
|||
1
resources/js/app.js
vendored
1
resources/js/app.js
vendored
|
|
@ -2,3 +2,4 @@ require('./bootstrap')
|
|||
require('./session_watch')
|
||||
require('./vue')
|
||||
require('./files_add')
|
||||
require('./label_colorize')
|
||||
|
|
|
|||
8
resources/js/bootstrap.js
vendored
8
resources/js/bootstrap.js
vendored
|
|
@ -20,8 +20,12 @@ window._ = require('lodash')
|
|||
|
||||
window.axios = require('axios')
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
||||
let csrfToken = document.querySelector('meta[name="csrf-token"]')
|
||||
if (csrfToken.getAttribute('content')) {
|
||||
window.axios.defaults.headers.common['Authorization'] = `Bearer ${csrfToken.getAttribute('content')}`
|
||||
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken.getAttribute('content')
|
||||
}
|
||||
|
||||
|
||||
require('fslightbox')
|
||||
|
|
|
|||
33
resources/js/components/LabelSelectComponent.vue
Normal file
33
resources/js/components/LabelSelectComponent.vue
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-select multiple v-model="selectedLabels" :options="labels" @search="addOption">
|
||||
<template v-slot:option="option">
|
||||
<span class="py-1 px-2" :style="`background-color: ${option.color};color: ${invertColor(option.color, true)};`">{{ option.label }}</span>
|
||||
</template>
|
||||
</v-select>
|
||||
<input v-for="label of selectedLabels" type="hidden" :key="label.code" :value="label.code" :name="name + '[]'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
mounted() {
|
||||
let data = JSON.parse(this.$parent.$options.json)
|
||||
this.name = data.name
|
||||
for(let label of (data.value ?? [])) {
|
||||
this.selectedLabels.push({label: label.name, code: label.id, color: label.color})
|
||||
}
|
||||
for(let label of data.labels) {
|
||||
this.labels.push({label: label.name, code: label.id, color: label.color})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
selectedLabels: [],
|
||||
inputValue: '',
|
||||
labels: []
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
19
resources/js/label_colorize.js
vendored
Normal file
19
resources/js/label_colorize.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import colors from './modules/colors'
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Ticket index colorize labels
|
||||
if (document.querySelector('.content.ticketsIndex') || document.querySelector('.content.ticketsShow')) {
|
||||
let labels = document.querySelectorAll('.label.colorize')
|
||||
|
||||
labels.forEach(label => {
|
||||
if (label.dataset.color) {
|
||||
if (colors.methods.isrgb(label.dataset.color)) {
|
||||
label.dataset.color = colors.methods.rgb2hex(label.dataset.color)
|
||||
}
|
||||
label.style.color = colors.methods.invertColor(label.dataset.color, true)
|
||||
label.style.backgroundColor = label.dataset.color
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
40
resources/js/modules/colors.js
vendored
Normal file
40
resources/js/modules/colors.js
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
export default {
|
||||
methods: {
|
||||
isrgb(rgb) {
|
||||
return (String(rgb).match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) ?? []).length !== 0
|
||||
},
|
||||
rgb2hex(rgb) {
|
||||
return `#${rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/).slice(1).map(
|
||||
n => parseInt(n, 10).toString(16).padStart(2, '0')
|
||||
).join('')}`
|
||||
},
|
||||
invertColor(hex, bw) {
|
||||
if (hex.indexOf('#') === 0) {
|
||||
hex = hex.slice(1);
|
||||
}
|
||||
// convert 3-digit hex to 6-digits.
|
||||
if (hex.length === 3) {
|
||||
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
||||
}
|
||||
if (hex.length !== 6) {
|
||||
throw new Error('Invalid HEX color.');
|
||||
}
|
||||
var r = parseInt(hex.slice(0, 2), 16),
|
||||
g = parseInt(hex.slice(2, 4), 16),
|
||||
b = parseInt(hex.slice(4, 6), 16);
|
||||
if (bw) {
|
||||
// https://stackoverflow.com/a/3943023/112731
|
||||
return (r * 0.299 + g * 0.587 + b * 0.114) > 186
|
||||
? '#000000'
|
||||
: '#FFFFFF';
|
||||
}
|
||||
// invert color components
|
||||
r = (255 - r).toString(16);
|
||||
g = (255 - g).toString(16);
|
||||
b = (255 - b).toString(16);
|
||||
// pad each with zeros and return
|
||||
return "#" + r.padStart(1, '0') + g.padStart(1, '0') + b.padStart(1, '0')
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -2,10 +2,15 @@
|
|||
|
||||
return [
|
||||
'classes' => [
|
||||
'users' => 'Users',
|
||||
'accounts' => 'Accounts',
|
||||
'projects' => 'Projects',
|
||||
'markers' => 'Markers',
|
||||
'tickets' => 'Tickets',
|
||||
'logs' => 'Logs',
|
||||
'creditlogs' => 'Logs',
|
||||
'files' => 'Files',
|
||||
'sites' => 'Sites',
|
||||
'brands' => 'Brands',
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@
|
|||
|
||||
return [
|
||||
'classes' => [
|
||||
'users' => 'Utilisateurs',
|
||||
'accounts' => 'Comptes',
|
||||
'projects' => 'Projets',
|
||||
'markers' => 'Marqueurs',
|
||||
'tickets' => 'Tickets',
|
||||
'logs' => 'Journaux',
|
||||
'creditlogs' => 'Historique',
|
||||
'files' => 'Fichiers',
|
||||
'sites' => 'Sites',
|
||||
'brands' => 'Marques',
|
||||
]
|
||||
];
|
||||
|
|
|
|||
17
resources/sass/bootstrap.scss
vendored
17
resources/sass/bootstrap.scss
vendored
|
|
@ -1,3 +1,20 @@
|
|||
@import 'variables';
|
||||
@import '~bootstrap/scss/bootstrap';
|
||||
@import 'bootswatch';
|
||||
|
||||
// OVERLOAD BOOTSTRAP
|
||||
.btn-xs, .btn-group-xs > .btn {
|
||||
padding: 0.1rem 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
|
||||
// Tickets Labels
|
||||
.ticketIndexLabels {
|
||||
display: -webkit-box;
|
||||
max-width: 15rem;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
|||
33
resources/views/accounts/credit.blade.php
Normal file
33
resources/views/accounts/credit.blade.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('bodyClass', 'col-md-8')
|
||||
|
||||
@section('content')
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="col h4">
|
||||
<a href="{{ route('brands.credit_accounts.index', $brand) }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ _('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
|
||||
{{ $brand->name }} :
|
||||
{{ $account->label }}
|
||||
<span class="text-decoration-underline">{{ __('Créditer un compte') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('brands.credit_accounts.creditSubmit', ['brand' => $brand, 'account' => $account]) }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div class="form-group mb-5">
|
||||
<label for="amount" class="col-form-label text-md-right">{{ __('Montant crédité') }}</label>
|
||||
<input class="form-control" name="amount" id="amount" type="number" min="0.5" step="0.5" value="{{ old('number', 0.5) }}" aria-describedby="amountHelper"/>
|
||||
@include('modules.input-error', [
|
||||
'inputName' => 'amount', 'helperId' => 'amountHelper' , 'helper' => __('Le montant en homme jour à créditer')
|
||||
])
|
||||
</div>
|
||||
|
||||
<div class="form-group text-center">
|
||||
<input type="submit" class="btn btn-warning" value="Créditer">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
121
resources/views/accounts/index.blade.php
Normal file
121
resources/views/accounts/index.blade.php
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
@extends($viewLayout)
|
||||
|
||||
@section('title', sprintf('%s - %s', transFb(sprintf('models.classes.%s', strtolower(Str::plural(class_basename($modelClass)))), Str::plural(class_basename($modelClass))), __('crud-policies::crud.viewAny')))
|
||||
@section('description', sprintf('%s %s', __('crud-policies::crud.viewAny'), trans(Str::plural(class_basename($modelClass)))))
|
||||
@section('metaIndex', 'noindex,nofollow')
|
||||
|
||||
@section('content')
|
||||
<div class="card">
|
||||
@include('crud-policies::crud.modules.flashMessage')
|
||||
<div class="card-header">
|
||||
@include('crud-policies::crud.modules.breadcrumb', ['action' => CrudAction::viewAny()])
|
||||
@include('crud-policies::crud.modules.actionbar.bar', ['action' => CrudAction::viewAny()])
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col">
|
||||
{{ $models->links() }}
|
||||
</div>
|
||||
<div class="col">
|
||||
<form action="{{ url()->current() }}">
|
||||
<div class="input-group mb-3">
|
||||
<input type="search" name="search" class="form-control" value="{{ request()->search }}"/>
|
||||
<input type="text" name="sort_col" class="d-none" value="{{ request()->sort_col }}"/>
|
||||
<input type="text" name="sort_way" class="d-none" value="{{ request()->sort_way }}"/>
|
||||
<a class="btn btn-danger" href="{{ url()->current() }}">{{ __('crud-policies::crud.reset') }}</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{{ __('crud-policies::crud.search') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table text-center">
|
||||
<thead class="thead-dark">
|
||||
@include('crud-policies::crud.modules.sorter')
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(count($models))
|
||||
|
||||
{{-- INIT QUERIES for OPTIMIZING --}}
|
||||
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
|
||||
@if($prop['type']->equals(CrudType::order()))
|
||||
@php ${"$modelTable$fieldName"} = $modelClass::max($fieldName); @endphp
|
||||
@endif
|
||||
@if($prop['type']->equals(CrudType::belongsTo()))
|
||||
@php try { $models->load(Str::singular((new $prop['belongsTo']())->getTable())); } catch(\Exception $e) {} @endphp
|
||||
@endif
|
||||
@if($prop['type']->equals(CrudType::belongsToMany()))
|
||||
@php $models->load("$fieldName"); @endphp
|
||||
@endif
|
||||
@if($prop['type']->equals(CrudType::hasMany()))
|
||||
@php $models->load("$fieldName"); @endphp
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- DISPLAY MODELS --}}
|
||||
@foreach($models as $model)
|
||||
<tr>
|
||||
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
|
||||
@if(in_array(CrudAction::viewAny(), $prop['actions']))
|
||||
@switch($prop['type'])
|
||||
@case(CrudType::password())
|
||||
{{-- Ignore --}}
|
||||
@break
|
||||
@default
|
||||
<td>@include('crud-policies::crud.printfield', [
|
||||
'action' => CrudAction::viewAny(),
|
||||
'field' => $model->{$fieldName},
|
||||
'props' => $prop,
|
||||
"$modelTable$fieldName" => ${"$modelTable$fieldName"} ?? null
|
||||
])</td>
|
||||
@endswitch
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- MODEL ACTIONS --}}
|
||||
<td>
|
||||
<div class="btn-group" role="group" aria-label="{{ Str::plural(__('crud-policies::crud.action')) }}">
|
||||
@can('viewAny', \App\Models\Credit\CreditLog::class)
|
||||
@if(is_object(request()->brand) and get_class(request()->brand) === App\Models\Brand::class)
|
||||
<a href="{{ CrudController::getRoutePrefixed('credit_logs.index', ['brand' => request()->brand, 'account' => $model]) }}" class="btn btn-sm btn-dark">{{ __('Historique') }}</a>
|
||||
@else
|
||||
<a href="{{ CrudController::getRoutePrefixed('credit_accounts.credit_logs.index', $model) }}" class="btn btn-sm btn-dark">{{ __('Historique') }}</a>
|
||||
@endif
|
||||
@endcan
|
||||
@can('credit', $model)
|
||||
@if(is_object(request()->brand) and get_class(request()->brand) === App\Models\Brand::class)
|
||||
<a href="{{ route('brands.credit_accounts.credit', ['brand' => request()->brand, 'account' => $model]) }}" class="btn btn-sm btn-warning">{{ __('Créditer') }}</a>
|
||||
@endif
|
||||
@endcan
|
||||
@can('view', $model)
|
||||
<a href="{{ CrudController::getRoutePrefixed("$modelTable.show", $model) }}" class="btn btn-sm btn-primary">{{ __('crud-policies::crud.view') }}</a>
|
||||
@endcan
|
||||
@can('update', $model)
|
||||
<a href="{{ CrudController::getRoutePrefixed("$modelTable.edit", $model) }}" class="btn btn-sm btn-info">{{ __('crud-policies::crud.edit') }}</a>
|
||||
@endcan
|
||||
@can('delete', $model)
|
||||
<form action="{{ CrudController::getRoutePrefixed("$modelTable.destroy", $model) }}" method="POST" onsubmit="__CRUD.confirmDelete(event)">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger">{{ __('crud-policies::crud.delete') }}</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td colspan="{{ count($modelClass::getEditableProperties()) }}">
|
||||
{{ __('crud-policies::crud.nothingtoshow') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
{{ $models->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@include('crud-policies::crud.modules.scriptConstants')
|
||||
@endsection
|
||||
112
resources/views/brands/index.blade.php
Normal file
112
resources/views/brands/index.blade.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
@extends($viewLayout)
|
||||
|
||||
@section('title', sprintf('%s - %s', transFb(sprintf('models.classes.%s', strtolower(Str::plural(class_basename($modelClass)))), Str::plural(class_basename($modelClass))), __('crud-policies::crud.viewAny')))
|
||||
@section('description', sprintf('%s %s', __('crud-policies::crud.viewAny'), trans(Str::plural(class_basename($modelClass)))))
|
||||
@section('metaIndex', 'noindex,nofollow')
|
||||
|
||||
@section('content')
|
||||
<div class="card">
|
||||
@include('crud-policies::crud.modules.flashMessage')
|
||||
<div class="card-header">
|
||||
@include('crud-policies::crud.modules.breadcrumb', ['action' => CrudAction::viewAny()])
|
||||
@include('crud-policies::crud.modules.actionbar.bar', ['action' => CrudAction::viewAny()])
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col">
|
||||
{{ $models->links() }}
|
||||
</div>
|
||||
<div class="col">
|
||||
<form action="{{ url()->current() }}">
|
||||
<div class="input-group mb-3">
|
||||
<input type="search" name="search" class="form-control" value="{{ request()->search }}"/>
|
||||
<input type="text" name="sort_col" class="d-none" value="{{ request()->sort_col }}"/>
|
||||
<input type="text" name="sort_way" class="d-none" value="{{ request()->sort_way }}"/>
|
||||
<a class="btn btn-danger" href="{{ url()->current() }}">{{ __('crud-policies::crud.reset') }}</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{{ __('crud-policies::crud.search') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table text-center">
|
||||
<thead class="thead-dark">
|
||||
@include('crud-policies::crud.modules.sorter')
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(count($models))
|
||||
|
||||
{{-- INIT QUERIES for OPTIMIZING --}}
|
||||
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
|
||||
@if($prop['type']->equals(CrudType::order()))
|
||||
@php ${"$modelTable$fieldName"} = $modelClass::max($fieldName); @endphp
|
||||
@endif
|
||||
@if($prop['type']->equals(CrudType::belongsTo()))
|
||||
@php try { $models->load(Str::singular((new $prop['belongsTo']())->getTable())); } catch(\Exception $e) {} @endphp
|
||||
@endif
|
||||
@if($prop['type']->equals(CrudType::belongsToMany()))
|
||||
@php $models->load("$fieldName"); @endphp
|
||||
@endif
|
||||
@if($prop['type']->equals(CrudType::hasMany()))
|
||||
@php $models->load("$fieldName"); @endphp
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- DISPLAY MODELS --}}
|
||||
@foreach($models as $model)
|
||||
<tr>
|
||||
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
|
||||
@if(in_array(CrudAction::viewAny(), $prop['actions']))
|
||||
@switch($prop['type'])
|
||||
@case(CrudType::password())
|
||||
{{-- Ignore --}}
|
||||
@break
|
||||
@default
|
||||
<td>@include('crud-policies::crud.printfield', [
|
||||
'action' => CrudAction::viewAny(),
|
||||
'field' => $model->{$fieldName},
|
||||
'props' => $prop,
|
||||
"$modelTable$fieldName" => ${"$modelTable$fieldName"} ?? null
|
||||
])</td>
|
||||
@endswitch
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- MODEL ACTIONS --}}
|
||||
<td>
|
||||
<div class="btn-group" role="group" aria-label="{{ Str::plural(__('crud-policies::crud.action')) }}">
|
||||
@can('viewAny', \App\Models\Credit\Account::class)
|
||||
<a href="{{ CrudController::getRoutePrefixed("brands.credit_accounts.index", $model) }}" class="btn btn-sm btn-dark">{{ __('models.classes.accounts') }}</a>
|
||||
@endcan
|
||||
@can('view', $model)
|
||||
<a href="{{ CrudController::getRoutePrefixed("$modelTable.show", $model) }}" class="btn btn-sm btn-primary">{{ __('crud-policies::crud.view') }}</a>
|
||||
@endcan
|
||||
@can('update', $model)
|
||||
<a href="{{ CrudController::getRoutePrefixed("$modelTable.edit", $model) }}" class="btn btn-sm btn-info">{{ __('crud-policies::crud.edit') }}</a>
|
||||
@endcan
|
||||
@can('delete', $model)
|
||||
<form action="{{ CrudController::getRoutePrefixed("$modelTable.destroy", $model) }}" method="POST" onsubmit="__CRUD.confirmDelete(event)">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger">{{ __('crud-policies::crud.delete') }}</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td colspan="{{ count($modelClass::getEditableProperties()) }}">
|
||||
{{ __('crud-policies::crud.nothingtoshow') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
{{ $models->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@include('crud-policies::crud.modules.scriptConstants')
|
||||
@endsection
|
||||
117
resources/views/credit_logs/index.blade.php
Normal file
117
resources/views/credit_logs/index.blade.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
@extends($viewLayout)
|
||||
|
||||
@section('title', sprintf('%s - %s', transFb(sprintf('models.classes.%s', strtolower(Str::plural(class_basename($modelClass)))), Str::plural(class_basename($modelClass))), __('crud-policies::crud.viewAny')))
|
||||
@section('description', sprintf('%s %s', __('crud-policies::crud.viewAny'), trans(Str::plural(class_basename($modelClass)))))
|
||||
@section('metaIndex', 'noindex,nofollow')
|
||||
|
||||
@section('content')
|
||||
<div class="card">
|
||||
@include('crud-policies::crud.modules.flashMessage')
|
||||
<div class="card-header">
|
||||
@include('crud-policies::crud.modules.breadcrumb', ['action' => CrudAction::viewAny()])
|
||||
@include('crud-policies::crud.modules.actionbar.bar', ['action' => CrudAction::viewAny()])
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col">
|
||||
{{ $models->links() }}
|
||||
</div>
|
||||
<div class="col">
|
||||
<form action="{{ url()->current() }}">
|
||||
<div class="input-group mb-3">
|
||||
<input type="search" name="search" class="form-control" value="{{ request()->search }}"/>
|
||||
<input type="text" name="sort_col" class="d-none" value="{{ request()->sort_col }}"/>
|
||||
<input type="text" name="sort_way" class="d-none" value="{{ request()->sort_way }}"/>
|
||||
<a class="btn btn-danger" href="{{ url()->current() }}">{{ __('crud-policies::crud.reset') }}</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{{ __('crud-policies::crud.search') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table text-center">
|
||||
<thead class="thead-dark">
|
||||
@include('crud-policies::crud.modules.sorter')
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(count($models))
|
||||
|
||||
{{-- INIT QUERIES for OPTIMIZING --}}
|
||||
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
|
||||
@if($prop['type']->equals(CrudType::order()))
|
||||
@php ${"$modelTable$fieldName"} = $modelClass::max($fieldName); @endphp
|
||||
@endif
|
||||
@if($prop['type']->equals(CrudType::belongsTo()))
|
||||
@php try { $models->load(Str::singular((new $prop['belongsTo']())->getTable())); } catch(\Exception $e) {} @endphp
|
||||
@endif
|
||||
@if($prop['type']->equals(CrudType::belongsToMany()))
|
||||
@php $models->load("$fieldName"); @endphp
|
||||
@endif
|
||||
@if($prop['type']->equals(CrudType::hasMany()))
|
||||
@php $models->load("$fieldName"); @endphp
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- DISPLAY MODELS --}}
|
||||
@foreach($models as $model)
|
||||
<tr>
|
||||
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
|
||||
@if(in_array(CrudAction::viewAny(), $prop['actions']))
|
||||
@if($fieldName === 'amount')
|
||||
<td><span class="fw-bold">{{ __(':amount jour(s)/homme', ['amount' => number_format($model->{$fieldName}, 1, ',', '')]) }}</span></td>
|
||||
@continue
|
||||
@endif
|
||||
@if($fieldName === 'type')
|
||||
<td><span class="fw-bold @if($model->{$fieldName}->equals(App\Enums\Credit\BalanceOperation::credit())) text-success @else text-danger @endif">{{ $model->{$fieldName}->label }}</span></td>
|
||||
@continue
|
||||
@endif
|
||||
@switch($prop['type'])
|
||||
@case(CrudType::password())
|
||||
{{-- Ignore --}}
|
||||
@break
|
||||
@default
|
||||
<td>@include('crud-policies::crud.printfield', [
|
||||
'action' => CrudAction::viewAny(),
|
||||
'field' => $model->{$fieldName},
|
||||
'props' => $prop,
|
||||
"$modelTable$fieldName" => ${"$modelTable$fieldName"} ?? null
|
||||
])</td>
|
||||
@endswitch
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- MODEL ACTIONS --}}
|
||||
<td>
|
||||
<div class="btn-group" role="group" aria-label="{{ Str::plural(__('crud-policies::crud.action')) }}">
|
||||
@can('view', $model)
|
||||
<a href="{{ CrudController::getRoutePrefixed("$modelTable.show", $model) }}" class="btn btn-sm btn-primary">{{ __('crud-policies::crud.view') }}</a>
|
||||
@endcan
|
||||
@can('update', $model)
|
||||
<a href="{{ CrudController::getRoutePrefixed("$modelTable.edit", $model) }}" class="btn btn-sm btn-info">{{ __('crud-policies::crud.edit') }}</a>
|
||||
@endcan
|
||||
@can('delete', $model)
|
||||
<form action="{{ CrudController::getRoutePrefixed("$modelTable.destroy", $model) }}" method="POST" onsubmit="__CRUD.confirmDelete(event)">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger">{{ __('crud-policies::crud.delete') }}</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td colspan="{{ count($modelClass::getEditableProperties()) }}">
|
||||
{{ __('crud-policies::crud.nothingtoshow') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
{{ $models->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@include('crud-policies::crud.modules.scriptConstants')
|
||||
@endsection
|
||||
25
resources/views/estimations/create.blade.php
Normal file
25
resources/views/estimations/create.blade.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('bodyClass', 'col-md-8')
|
||||
|
||||
@section('content')
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="col h4">
|
||||
<a href="{{ route('ticket.show', $ticket) }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ _('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
|
||||
{{ $ticket->full_name }}
|
||||
@if ($ticket->is_internal)<i class="fa-solid fa-lock"></i>@endif
|
||||
<span class="text-decoration-underline">{{ __('Créer une estimation') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('ticket.estimation.estimateSubmit', $ticket) }}">
|
||||
@csrf
|
||||
@include('estimations.form-inputs')
|
||||
<div class="form-group text-center">
|
||||
<input type="submit" class="btn btn-warning" value="Valider">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
19
resources/views/estimations/form-inputs.blade.php
Normal file
19
resources/views/estimations/form-inputs.blade.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<div class="form-group mb-5">
|
||||
<label for="amount" class="col-form-label text-md-right">{{ __('Montant estimé') }}</label>
|
||||
<input class="form-control" name="amount" id="amount" type="number" min="0.5" step="0.5" value="{{ old('number', 0.5) }}" aria-describedby="amountHelper"/>
|
||||
@include('modules.input-error', [
|
||||
'inputName' => 'amount', 'helperId' => 'amountHelper' , 'helper' => __('Le montant en homme jour estimé.')
|
||||
])
|
||||
</div>
|
||||
<div class="form-group mb-5">
|
||||
<label for="creditAccount"><b>{{ __('Compte à débiter') }}</b></label>
|
||||
<select class="form-select" name="credit_account_id" id="creditAccount" aria-describedby="creditAccountHelp">
|
||||
<option value="" @if(!old('credit_account_id')) selected @endif disabled>{{ __('Veuillez sélectionner un compte à débiter') }}</option>
|
||||
@foreach($ticket->site->brand->accounts as $account)
|
||||
<option value="{{ $account->id }}" @if(old('credit_account_id') == $account->id) selected @endif>{{ $account->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@include('modules.input-error', [
|
||||
'inputName' => 'brand_id', 'helperId' => 'creditAccountHelp' , 'helper' => __('Le compte de la marque à débiter')
|
||||
])
|
||||
</div>
|
||||
45
resources/views/estimations/pendingCard.blade.php
Normal file
45
resources/views/estimations/pendingCard.blade.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="h5">{{ __('Estimation faite par :user', ['user' => $estimation->estimator->name]) }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<span class="fw-bold h4">{{ __('Temps estimé :time jours/homme', ['time' => $estimation->amount]) }}</span>
|
||||
</div>
|
||||
@if($estimation->status->equals(App\Enums\Credit\EstimationStatus::pending()))
|
||||
<div class="col d-flex justify-content-end">
|
||||
<span class="h4 text-dark">{{ __('En attente') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($estimation->status->equals(App\Enums\Credit\EstimationStatus::accepted()))
|
||||
<div class="col d-flex justify-content-end">
|
||||
<span class="h4 text-success">{{ __('Validé par :user', ['user' => optional($estimation->validator)->name ?? 'personne supprimée']) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($estimation->status->equals(App\Enums\Credit\EstimationStatus::refused()))
|
||||
<div class="col d-flex justify-content-end">
|
||||
<span class="h4 text-danger">{{ __('Refusé par :user', ['user' => optional($estimation->validator)->name ?? 'personne supprimée']) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@can('validateEstimate', $ticket)
|
||||
@can('answer', $estimation)
|
||||
<div class="col d-flex justify-content-end">
|
||||
<form class="mx-1" action="{{ route('ticket.estimation.estimatePendingSubmit', $ticket) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="status" value="accept">
|
||||
<button type="submit" class="btn btn-primary">{{ __('J\'accepte') }}</button>
|
||||
</form>
|
||||
<form class="mx-1" action="{{ route('ticket.estimation.estimatePendingSubmit', $ticket) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="status" value="refuse">
|
||||
<button type="submit" class="btn btn-warning">{{ __('Je refuse') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
@endcan
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -18,8 +18,9 @@
|
|||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<!-- Styles -->
|
||||
<link href="{{ asset('css/bootstrap.css') }}" rel="stylesheet">
|
||||
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
|
||||
<link href="{{ mix('css/bootstrap.css') }}" rel="stylesheet">
|
||||
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
|
||||
<link href="{{ mix('css/fontawesome.min.css') }}" rel="stylesheet">
|
||||
@crudPoliciesCSS
|
||||
@stack('customCss')
|
||||
</head>
|
||||
|
|
@ -40,7 +41,8 @@
|
|||
</div>
|
||||
@include('layout.footer')
|
||||
@stack('scriptsConstants')
|
||||
<script src="{{ asset('js/app.js') }}"></script>
|
||||
<script src="{{ mix('js/app.js') }}"></script>
|
||||
<script src="{{ mix('js/fontawesome.min.js') }}"></script>
|
||||
@stack('scripts')
|
||||
@crudPoliciesJS()
|
||||
@auth
|
||||
|
|
|
|||
|
|
@ -11,19 +11,47 @@
|
|||
@auth
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto">
|
||||
@if (Auth::user()->isAdmin())
|
||||
@can('viewAny', App\Models\Ticket::class)
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @if(strpos(request()->route()->getName(), 'ticket') === 0)active @endif" href="{{ route('ticket.index') }}">Tickets</a>
|
||||
<a class="nav-link @if(strpos(request()->route()->getName(), 'ticket') === 0)active @endif" href="{{ route('ticket.index') }}">{{ __('models.classes.tickets') }}</a>
|
||||
</li>
|
||||
@endcan
|
||||
@if(
|
||||
auth()->user()->can('viewAny', \App\Models\Brand::class) or
|
||||
auth()->user()->can('viewAny', \App\Models\Site::class) or
|
||||
auth()->user()->can('viewAny', \App\Models\Project::class)
|
||||
)
|
||||
<li class="nav-item dropdown user-dropdown">
|
||||
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ __('Gestion') }} <span class="caret"></span>
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
|
||||
@can('viewAny', App\Models\Brand::class)
|
||||
<a class="dropdown-item @if(strpos(request()->route()->getName(), 'brands') === 0)active @endif" href="{{ route('brands.index') }}">{{ __('models.classes.brands') }}</a>
|
||||
@endcan
|
||||
@can('viewAny', App\Models\Site::class)
|
||||
<a class="dropdown-item @if(strpos(request()->route()->getName(), 'sites') === 0)active @endif" href="{{ route('sites.index') }}">{{ __('models.classes.sites') }}</a>
|
||||
@endcan
|
||||
@can('viewAny', App\Models\Project::class)
|
||||
<a class="dropdown-item @if(strpos(request()->route()->getName(), 'projects') === 0)active @endif" href="{{ route('projects.index') }}">{{ __('models.classes.projects') }}</a>
|
||||
@endcan
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
@can('viewAny', App\Models\User::class)
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @if(strpos(request()->route()->getName(), 'sites') === 0)active @endif" href="{{ route('sites.index') }}">Sites</a>
|
||||
<a class="nav-link @if(strpos(request()->route()->getName(), 'users') === 0)active @endif" href="{{ route('users.index') }}">{{ __('models.classes.users') }}</a>
|
||||
</li>
|
||||
@endcan
|
||||
@if(!Auth::user()->isAdmin())
|
||||
@can('viewAny', App\Models\Credit\Account::class)
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @if(strpos(request()->route()->getName(), 'projects') === 0)active @endif" href="{{ route('projects.index') }}">Projets</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @if(strpos(request()->route()->getName(), 'users') === 0)active @endif" href="{{ route('users.index') }}">Utilisateurs</a>
|
||||
<a class="nav-link @if(strpos(request()->route()->getName(), 'users') === 0)active @endif" href="{{ route('credit_accounts.index') }}">{{ __('models.classes.accounts') }}</a>
|
||||
</li>
|
||||
@endcan
|
||||
@endif
|
||||
@if(Auth::user()->isAdmin())
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @if(strpos(request()->route()->getName(), 'schedule') === 0)active @endif" href="{{ route('schedule.index') }}">{{ __('Planning') }}</a>
|
||||
</li>
|
||||
|
|
@ -34,7 +62,7 @@
|
|||
</ul>
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<span class="nav-link text-info fw-bold">{{ __('Tickets') }}</span>
|
||||
<span class="nav-link text-info fw-bold">{{ __('models.classes.tickets') }}</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span class="nav-link text-light">{{ __('Nouveau') }} : <span class="text-info">{{ $ticketsNew ?? 0 }}</span></span>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="container container-xxl vueComponent">
|
||||
@php
|
||||
$dataVue = [
|
||||
'isAdmin' => Auth::user()->isAdmin() ? '1' : '0',
|
||||
'isAdmin' => Auth::user()->isAdmin(),
|
||||
'today' => now(),
|
||||
'startDay' => $date,
|
||||
'loadCollection' => $loadCollection,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
@if(request()->project)
|
||||
<a href="{{ route('projects.show', request()->project) }}" class="btn btn-success float-end">{{ __('Projet') }}</a>
|
||||
<a href="{{ route('projects.macro_markers.index', request()->project) }}" class="btn btn-warning float-end">{{ __('models.classes.markers') }}</a>
|
||||
<a href="{{ route('projects.macro_markers.index', request()->project) }}" class="btn btn-warning float-end">{{ Str::singular(__('models.classes.markers')) }}</a>
|
||||
<a href="{{ route('projects.logs.index', request()->project) }}" class="btn btn-dark float-end">{{ __('models.classes.logs') }}</a>
|
||||
<a href="{{ route('projects.files.index', request()->project) }}" class="btn btn-dark float-end">{{ __('models.classes.files') }}</a>
|
||||
@endif
|
||||
@if(request()->site)
|
||||
<a href="{{ route('sites.show', request()->site) }}" class="btn btn-success float-end">{{ __('models.classes.sites') }}</a>
|
||||
<a href="{{ route('sites.show', request()->site) }}" class="btn btn-success float-end">{{ Str::singular(__('models.classes.sites')) }}</a>
|
||||
<a href="{{ route('sites.logs.index', request()->site) }}" class="btn btn-dark float-end">{{ __('models.classes.logs') }}</a>
|
||||
<a href="{{ route('sites.files.index', request()->site) }}" class="btn btn-dark float-end">{{ __('models.classes.files') }}</a>
|
||||
@endif
|
||||
@if(request()->brand)
|
||||
<a href="{{ route('brands.show', request()->brand) }}" class="btn btn-success float-end">{{ Str::singular(__('models.classes.brands')) }}</a>
|
||||
<a href="{{ route('brands.credit_accounts.index', request()->brand) }}" class="btn btn-dark float-end">{{ __('models.classes.accounts') }}</a>
|
||||
@endif
|
||||
|
|
|
|||
8
resources/views/modules/input-error.blade.php
Normal file
8
resources/views/modules/input-error.blade.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
@if($errors->has($inputName))
|
||||
<div class="col-12">
|
||||
<small class="text-danger">{{ implode(',', $errors->get($inputName)) }}</small>
|
||||
</div>
|
||||
@endif
|
||||
@if(isset($helper))
|
||||
<small @if(isset($helperId)) id="{{ $helperId }}" @endif class="form-text text-muted">{{ $helper }}</small>
|
||||
@endif
|
||||
|
|
@ -4,7 +4,12 @@
|
|||
|
||||
@section('content')
|
||||
<div class="card">
|
||||
<div class="card-header">{{ __('Créer un ticket') }}<a href="{{ route('home') }}" class="float-end btn btn-sm btn-primary">{{ __('Retour') }}</a></div>
|
||||
<div class="card-header">
|
||||
<h2 class="col h4">
|
||||
<a href="{{ route('ticket.index') }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ _('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
|
||||
<span> {{ __('Créer un ticket') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('ticket.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@
|
|||
|
||||
@section('content')
|
||||
<div class="card">
|
||||
<div class="card-header">{{ __('Modifier un ticket') }}<a href="{{ route('home') }}" class="float-end btn btn-sm btn-primary">{{ __('Retour') }}</a></div>
|
||||
<div class="card-header">
|
||||
<h2 class="col h4">
|
||||
<a href="{{ route('ticket.index') }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ _('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
|
||||
<span> {{ __('Modifier un ticket') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('ticket.update', $ticket->id) }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ $userPr = Auth::user()->sites ?? collect([]);
|
|||
</div>
|
||||
@can('changeStatus', App\Model\Ticket::class)
|
||||
<div class="form-group col">
|
||||
<label for="status" class="col-form-label">Status</label>
|
||||
<label for="status" class="col-form-label">{{ __('Status') }}</label>
|
||||
<select class="form-select" name="status" id="status">
|
||||
@foreach(TicketStatus::toArray() as $ticketStatus)
|
||||
<option value="{{ $ticketStatus->value }}" @if($ticketType->value == old('status') or (isset($ticket) and $ticketStatus->equals($ticket->status)))selected @endif>{{ $ticketStatus->label }}</option>
|
||||
|
|
@ -34,6 +34,23 @@ $userPr = Auth::user()->sites ?? collect([]);
|
|||
@else
|
||||
<input type="hidden" name="status" value="{{ \App\Enums\TicketStatus::new() }}" required>
|
||||
@endcan
|
||||
|
||||
@can('assignLabels', App\Model\Ticket::class)
|
||||
<div class="form-group col">
|
||||
<label for="labels" class="col-form-label">{{ __('Étiquettes') }}</label>
|
||||
@php
|
||||
$dataVue = [
|
||||
'name' => 'labels',
|
||||
'value' => old('labels', optional($ticket)->labels),
|
||||
'labels' => \App\Models\Label::all()
|
||||
];
|
||||
@endphp
|
||||
<div
|
||||
id="label-select"
|
||||
class="sticky-top"
|
||||
data-json='@json($dataVue)'></div>
|
||||
</div>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-5">
|
||||
|
|
|
|||
|
|
@ -1,212 +1,241 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('bodyClass', 'content ticketsIndex')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$userPr = Auth::user()->sites ?? collect([]);
|
||||
@endphp
|
||||
<div class="container container-xxl">
|
||||
<h2>
|
||||
Gestion des tickets @if(request()->status) (Archivés) @endif
|
||||
<div class="float-end">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="btn-group float-end">
|
||||
<a href="{{ route('ticket.create') }}" class="btn btn-primary">{{ __('Créer un ticket') }}</a>
|
||||
@if(session('tickets.st') == TicketStatus::archived())
|
||||
<a href="{{ route('ticket.index', ['sts' => -1 ]) }}" class="btn btn-success">{{ __('Tickets Actifs') }}</a>
|
||||
@else
|
||||
<a href="{{ route('ticket.index', ['st' => TicketStatus::archived()]) }}" class="float-end btn btn-secondary">{{ __('Tickets Archivés') }}</a>
|
||||
@endif
|
||||
<a href="{{ route('ticket.index', ['rst' => 1]) }}" class="btn btn-dark">{{ __('Réinitialiser le tri') }}</a>
|
||||
</div>
|
||||
<div class="float-end me-2">
|
||||
<select class="form-select" onchange="window.location='{{ route('ticket.index') }}?'+(this.value == -1 ? 'prs=-1' : 'pr='+this.value);">
|
||||
<option value="-1">{{ __('Tous') }}</option>
|
||||
@foreach($sites as $site)
|
||||
@if (Auth::user()->isAdmin())
|
||||
<option value="{{ $site->id }}" @if(intval(session('tickets.pr'))==$site->id)selected="selected" @endif>{{ $site->name }}</option>
|
||||
@elseif ($userPr->filter(function($pr) use($site) { return $pr->id === $site->id; })->count())
|
||||
<option value="{{ $site->id }}" @if(intval(session('tickets.pr'))==$site->id)selected="selected" @endif>{{ $site->name }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="float-end me-2">
|
||||
<select class="form-select" onchange="window.location='{{ route('ticket.index') }}?'+(this.value == -1 ? 'sts=-1' : 'st='+this.value);">
|
||||
<option value="-1">{{ __('Tous') }}</option>
|
||||
@foreach($status as $statut)
|
||||
<option value="{{ $statut->value }}" @if(intval(session('tickets.st'))==$statut->value)selected="selected" @endif>{{ $statut->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="float-end mt-3 me-2">
|
||||
<select class="form-select" onchange="window.location='{{ route('ticket.index') }}?'+(this.value == -1 ? 'assr=-1' : 'ass='+this.value);">
|
||||
<option value="-1">{{ __('Tous') }}</option>
|
||||
@foreach(\App\Models\User::adminUsers() as $user)
|
||||
<option value="{{ $user->id }}" @if(intval(session('tickets.ass'))==$user->id)selected="selected" @endif>{{ $user->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<form id="searchTicketForm" class="float-end mt-3 w-75" action="{{ request()->fullUrlWithQuery(['search' => '']) }}">
|
||||
<div class="input-group mb-3">
|
||||
<input id="searchTicket" type="text" name="search" class="form-control" placeholder="{{ __('Recherche de ticket..') }}" aria-label="{{ __('Recherche de ticket..') }}" aria-describedby="searchTicket" value="{{ session()->get('tickets.search') }}">
|
||||
<button class="btn btn-outline-danger" type="button" onclick="document.getElementById('searchTicket').value = '';document.getElementById('searchTicketForm').submit();">⌫</button>
|
||||
<button class="btn btn-outline-primary" type="submit">🔍</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
#
|
||||
@include('modules.sorter', ['colname' => 'id'])
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{__('Type') }}
|
||||
@include('modules.sorter', ['colname' => 'type'])
|
||||
</th>
|
||||
<th scope="col">{{__('Fichiers') }}</th>
|
||||
<th scope="col">
|
||||
{{__('Crée le') }}
|
||||
@include('modules.sorter', ['colname' => 'created_at'])
|
||||
</th>
|
||||
@if(Auth::user()->isAdmin())
|
||||
<th scope="col">
|
||||
{{__('Assigné à') }}
|
||||
@include('modules.sorter', ['colname' => 'assigned_to'])
|
||||
</th>
|
||||
@endif
|
||||
<th scope="col">
|
||||
{{__('Créé par') }}
|
||||
@include('modules.sorter', ['colname' => 'created_by'])
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{__('Statut') }}
|
||||
@include('modules.sorter', ['colname' => 'status'])
|
||||
</th>
|
||||
<th scope="col">{{__('Contenu') }}</th>
|
||||
<th>{{__('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!count($tickets))
|
||||
<tr>
|
||||
<td colspan="10" class="text-center">
|
||||
<h2>{{ __('Aucun ticket') }}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@foreach ($tickets as $ticket)
|
||||
@php
|
||||
$cls = $ticket->status == TicketStatus::archived() ? 'text-secondary' : null;
|
||||
$cls = (!$cls && $ticket->status == TicketStatus::new()) ? 'text-danger' : $cls;
|
||||
$cls = (!$cls && $ticket->status == TicketStatus::processed()) ? 'text-info' : $cls;
|
||||
$cls = (!$cls && $ticket->status == TicketStatus::recette()) ? 'text-primary' : $cls;
|
||||
$cls = (!$cls && $ticket->status == TicketStatus::production()) ? 'text-success' : $cls;
|
||||
$cls = (!$cls && $ticket->status == TicketStatus::inprogress()) ? 'text-dark' : $cls;
|
||||
$cls = (!$cls && $ticket->status == TicketStatus::waiting()) ? 'text-white bg-danger' : $cls;
|
||||
$ticketClass = 'bg-primary';
|
||||
switch($ticket->type) {
|
||||
case TicketType::bug():
|
||||
$ticketClass = 'bg-danger';
|
||||
break;
|
||||
case TicketType::evolution():
|
||||
$ticketClass = 'bg-success';
|
||||
break;
|
||||
case TicketType::newsletter():
|
||||
$ticketClass = 'bg-warning';
|
||||
break;
|
||||
}
|
||||
@endphp
|
||||
<tr>
|
||||
<td scope="row">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="me-1">{{ $ticket->id }} @if($ticket->is_internal)🔒@endif</span>
|
||||
<span class="text-end">{{ $ticket->site->name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {{ $ticketClass }}">{{ $ticket->type->label }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@if ($ticket->file !== null)
|
||||
<a data-fslightbox="gallery" href="{{ asset($ticket->file) }}" class="btn btn-sm btn-outline-primary" target="_blank">Fichier</a>
|
||||
@endif
|
||||
@if ($ticket->file2 !== null)
|
||||
<a data-fslightbox="gallery" href="{{ asset($ticket->file2) }}" class="btn btn-sm btn-outline-primary" target="_blank">Fichier 2</a>
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }}</td>
|
||||
@if(Auth::user()->isAdmin())
|
||||
<td>{{ $ticket->assignedUser ? $ticket->assignedUser->name : __('Personne') }}</td>
|
||||
@endif
|
||||
<td>{{ $ticket->user ? $ticket->user->name : __('Utilisateur supprimé') }}</td>
|
||||
<td><span class="rounded {{ $cls }}">{{ $ticket->status->label }}</span></td>
|
||||
<td>
|
||||
{{-- Modal du ticket --}}
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#ticket{{ $ticket->id }}Modal">
|
||||
{{ __('Aperçu') }}
|
||||
</button>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="ticket{{ $ticket->id }}Modal" tabindex="-1" aria-labelledby="ticket{{ $ticket->id }}ModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="ticket{{ $ticket->id }}ModalLabel">
|
||||
<b>{{ $ticket->site->name }}</b> -
|
||||
<u>
|
||||
{{__('Ticket n°') }} {{ $ticket->id }}
|
||||
@if ($ticket->is_internal)
|
||||
🔒
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="d-inline">{{ __('Gestion des tickets') }} @if(request()->st) (Archivés) @endif</h2>
|
||||
<div class="float-end">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="btn-group float-end">
|
||||
@can('viewAny', App\Models\Label::class)
|
||||
<a href="{{ route('labels.index') }}" class="btn btn-info"><i class="fa-solid fa-tags"></i></a>
|
||||
@endif
|
||||
<a href="{{ route('ticket.create') }}" class="btn btn-primary"><i class="fa-solid fa-square-plus"></i> {{ __('ticket') }}</a>
|
||||
@if(session('tickets.st') == TicketStatus::archived())
|
||||
<a href="{{ route('ticket.index', ['sts' => -1 ]) }}" class="btn btn-success"><i class="fa-solid fa-inbox"></i></a>
|
||||
@else
|
||||
<a href="{{ route('ticket.index', ['st' => TicketStatus::archived()]) }}" class="float-end btn btn-secondary"><i class="fa-solid fa-box-archive"></i></a>
|
||||
@endif
|
||||
<a href="{{ route('ticket.index', ['rst' => 1]) }}" class="btn btn-dark"><i class="fa-solid fa-eraser"></i> {{ __(' le tri') }}</a>
|
||||
</div>
|
||||
<div class="float-end me-2">
|
||||
<div class="input-group">
|
||||
<label class="input-group-text" for="selectProject"><i class="fa-solid fa-diagram-project"></i></label>
|
||||
<select id="selectProject" class="form-select" onchange="window.location='{{ route('ticket.index') }}?'+(this.value == -1 ? 'prs=-1' : 'pr='+this.value);">
|
||||
<option value="-1">{{ __('Tous') }}</option>
|
||||
@foreach($sites as $site)
|
||||
@if (Auth::user()->isAdmin())
|
||||
<option value="{{ $site->id }}" @if(intval(session('tickets.pr'))==$site->id)selected="selected" @endif>{{ $site->name }}</option>
|
||||
@elseif ($userPr->filter(function($pr) use($site) { return $pr->id === $site->id; })->count())
|
||||
<option value="{{ $site->id }}" @if(intval(session('tickets.pr'))==$site->id)selected="selected" @endif>{{ $site->name }}</option>
|
||||
@endif
|
||||
</u> -
|
||||
<span class="badge {{ $ticketClass }}">
|
||||
{{$ticket->type->label}}
|
||||
</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ __('Fermer') }}"></button>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{!! clean(nl2br($ticket->text)) !!}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-primary" data-bs-dismiss="modal">{{ __('Fermer') }}</button>
|
||||
<div class="float-end me-2">
|
||||
<div class="input-group">
|
||||
<label class="input-group-text" for="selectStatus"><i class="fa-solid fa-sign-hanging"></i></label>
|
||||
<select id="selectStatus" class="form-select" onchange="window.location='{{ route('ticket.index') }}?'+(this.value == -1 ? 'sts=-1' : 'st='+this.value);">
|
||||
<option value="-1">{{ __('Tous') }}</option>
|
||||
@foreach($status as $statut)
|
||||
<option value="{{ $statut->value }}" @if(intval(session('tickets.st'))==$statut->value)selected="selected" @endif>{{ $statut->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@can('assignTickets', App\Models\Ticket::class)
|
||||
<div class="col">
|
||||
<div class="float-end mt-3 me-2">
|
||||
<div class="input-group">
|
||||
<label class="input-group-text" for="selectAssign"><i class="fa-solid fa-hat-wizard"></i></label>
|
||||
<select id="selectAssign" class="form-select" onchange="window.location='{{ route('ticket.index') }}?'+(this.value == -1 ? 'assr=-1' : 'ass='+this.value);">
|
||||
<option value="-1">{{ __('Tous') }}</option>
|
||||
@foreach(\App\Models\User::adminUsers() as $user)
|
||||
<option value="{{ $user->id }}" @if(intval(session('tickets.ass'))==$user->id)selected="selected" @endif>{{ $user->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
<div class="col">
|
||||
<form id="searchTicketForm" class="float-end mt-3 w-75" action="{{ request()->fullUrlWithQuery(['search' => '']) }}">
|
||||
<div class="input-group mb-3">
|
||||
<input id="searchTicket" type="text" name="search" class="form-control" placeholder="{{ __('Recherche de ticket..') }}" aria-label="{{ __('Recherche de ticket..') }}" aria-describedby="searchTicket" value="{{ session()->get('tickets.search') }}">
|
||||
<button class="btn btn-outline-danger" type="button" onclick="document.getElementById('searchTicket').value = '';document.getElementById('searchTicketForm').submit();"><i class="fa-solid fa-delete-left"></i></button>
|
||||
<button class="btn btn-outline-primary" type="submit"><i class="fa-solid fa-magnifying-glass"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ route('ticket.show', $ticket->id) }}" class="btn btn-sm btn-outline-success">{{ __('Messages') }}</a>
|
||||
@if(auth()->user()->isAdmin())
|
||||
<a href="{{ route('ticket.edit', $ticket) }}" class="btn btn-sm btn-outline-primary">{{ __('Modifier') }}</a>
|
||||
@endif
|
||||
@can('delete', $ticket)
|
||||
@if(!$ticket->status->equals(TicketStatus::archived()))
|
||||
<form method="POST" action="{{ route('ticket.archive', $ticket) }}">
|
||||
@method('PUT')
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Êtes-vous sûr(e) ?')">{{ __('Archiver') }}</button>
|
||||
</form>
|
||||
@endif
|
||||
@endcan
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
{{ $tickets->withQueryString()->links() }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<span># </span>
|
||||
<span>@include('modules.sorter', ['colname' => 'id'])</span>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<span>{{__('Type') }} </span>
|
||||
<span>@include('modules.sorter', ['colname' => 'type'])</span>
|
||||
</th>
|
||||
@can('assignLabels', App\Models\Ticket::class)
|
||||
<th scope="col">{{ __('Étiquettes') }}</th>
|
||||
@endcan
|
||||
@if(Auth::user()->isAdmin())
|
||||
<th scope="col">
|
||||
<span>{{__('Assigné à') }} </span>
|
||||
<span>@include('modules.sorter', ['colname' => 'assigned_to'])</span>
|
||||
</th>
|
||||
@endif
|
||||
<th scope="col">
|
||||
<span>{{__('Créé par') }} </span>
|
||||
<span>@include('modules.sorter', ['colname' => 'created_by'])</span>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<span>{{__('Le') }} </span>
|
||||
<span>@include('modules.sorter', ['colname' => 'created_at'])</span>
|
||||
</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!count($tickets))
|
||||
<tr>
|
||||
<td colspan="@if(Auth::user()->isAdmin()) 6 @else 5 @endif" class="text-center">
|
||||
<h2>{{ __('Aucun ticket') }}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@foreach ($tickets as $ticket)
|
||||
<tr class="position-relative">
|
||||
<td scope="row">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="me-1">{{ $ticket->id }} @if($ticket->is_internal)<i class="fa-solid fa-lock"></i>@endif</span>
|
||||
<span class="d-flex flex-column text-end">
|
||||
@if (Auth::user()->isAdmin())
|
||||
<a href="{{ route('sites.show', $ticket->site) }}" title="{{ __('Voir le site :sitename', [
|
||||
'sitename' => $ticket->site->name
|
||||
]) }}">{{ $ticket->site->name }}</a>
|
||||
@else
|
||||
<span class="text-decoration-underline">{{ $ticket->site->name }}</span>
|
||||
@endif
|
||||
<span class="rounded text-center {{ $ticket->status->class }}">{{ $ticket->status->label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge {{ $ticket->type->class }}">{{ $ticket->type->label }}</span>
|
||||
</td>
|
||||
@can('assignLabels', App\Models\Ticket::class)
|
||||
<td>
|
||||
<span class="ticketIndexLabels">
|
||||
@foreach ($ticket->labels as $label)
|
||||
<span class="badge label colorize" data-color="{{ $label->color }}">{{ $label->name }}</span>
|
||||
@endforeach
|
||||
</span>
|
||||
</td>
|
||||
@endcan
|
||||
@if(Auth::user()->isAdmin())
|
||||
<td>{{ $ticket->assignedUser ? $ticket->assignedUser->name : __('Personne') }}</td>
|
||||
@endif
|
||||
<td>{{ $ticket->user ? $ticket->user->name : __('Utilisateur supprimé') }}</td>
|
||||
<td>{{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }}</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<div>
|
||||
<div class="btn-group" role="group">
|
||||
@if ($ticket->file !== null)
|
||||
<a data-fslightbox="gallery{{ $ticket->id }}" href="{{ asset($ticket->file) }}" class="btn btn-outline-dark" target="_blank"><i class="fa-solid fa-file-image"></i></a>
|
||||
@endif
|
||||
@if ($ticket->file2 !== null)
|
||||
<a data-fslightbox="gallery{{ $ticket->id }}" href="{{ asset($ticket->file2) }}" class="btn btn-outline-dark {{ $ticket->file !== null ? 'd-none' : '' }}" target="_blank"></a>
|
||||
@endif
|
||||
{{-- Modal du ticket --}}
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-outline-info" data-bs-toggle="modal" data-bs-target="#ticket{{ $ticket->id }}Modal"><i class="fa-solid fa-eye"></i></button>
|
||||
<a href="{{ route('ticket.show', $ticket->id) }}" class="btn btn-outline-success"><i class="fa-solid fa-comment"></i></a>
|
||||
@if(auth()->user()->isAdmin())
|
||||
<a href="{{ route('ticket.edit', $ticket) }}" class="btn btn-outline-primary"><i class="fa-solid fa-pen"></i></a>
|
||||
@endif
|
||||
@can('delete', $ticket)
|
||||
@if(!$ticket->status->equals(TicketStatus::archived()))
|
||||
<form method="POST" action="{{ route('ticket.archive', $ticket) }}">
|
||||
@method('PUT')
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('Êtes-vous sûr(e) ?')"><i class="fa-solid fa-box-archive"></i></button>
|
||||
</form>
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="ticket{{ $ticket->id }}Modal" tabindex="-1" aria-labelledby="ticket{{ $ticket->id }}ModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="ticket{{ $ticket->id }}ModalLabel">
|
||||
@can('view', App\Models\Site::class)
|
||||
<a href="{{ route('sites.show', $ticket->site) }}" title="{{ __('Voir le site :sitename', [
|
||||
'sitename' => $ticket->site->name
|
||||
]) }}"><b>{{ $ticket->site->name }}</b></a>
|
||||
@else
|
||||
<b>{{ $ticket->site->name }}</b>
|
||||
@endcan -
|
||||
<u>
|
||||
{{__('Ticket n°') }} {{ $ticket->id }}
|
||||
@if ($ticket->is_internal)
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
@endif
|
||||
</u> -
|
||||
<span class="badge {{ $ticket->type->class }}">
|
||||
{{$ticket->type->label}}
|
||||
</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ __('Fermer') }}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@can('assignLabels', App\Models\Ticket::class)
|
||||
<div class="mt-1 mb-3">
|
||||
@foreach ($ticket->labels as $label)
|
||||
<span class="badge label colorize" data-color="{{ $label->color }}">{{ $label->name }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endcan
|
||||
<p>{!! clean(nl2br($ticket->text)) !!}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-primary" data-bs-dismiss="modal">{{ __('Fermer') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
{{ $tickets->withQueryString()->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('bodyClass', 'content ticketsShow')
|
||||
|
||||
@section('content')
|
||||
<div class="form-row row">
|
||||
<h2 class="col">
|
||||
<a href="{{ route('ticket.index') }}" class="text-decoration-none">←</a>
|
||||
{{ __('Ticket n° :ticketId : :ticketSiteName - :ticketTypeLabel', [
|
||||
'ticketId' => $ticket->id,
|
||||
'ticketSiteName' => $ticket->site->name,
|
||||
'ticketTypeLabel' => $ticket->type->label
|
||||
]) }}
|
||||
@if ($ticket->is_internal)🔒@endif
|
||||
<a href="{{ route('ticket.index') }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ _('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
|
||||
{{ $ticket->full_name }}
|
||||
@if ($ticket->is_internal)<i class="fa-solid fa-lock"></i>@endif
|
||||
<span class="badge bg-info">{{ $ticket->status->label }}</span>
|
||||
</h2>
|
||||
<div class="col d-flex flex-row text-end">
|
||||
|
|
@ -25,22 +23,28 @@
|
|||
@can('assignTo', $ticket)
|
||||
<form action="{{ route('ticket.assignTo', ['ticket' => $ticket]) }}" method="POST" id="form_assign" class="col me-2">
|
||||
@csrf
|
||||
<select class="form-select" name="assign" id="assign" onchange="document.getElementById('form_assign').submit();">
|
||||
<option value="" @if(!$ticket->assignedUser)selected @endif>{{ __('Personne n\'est assigné') }}</option>
|
||||
@foreach(\App\Models\User::adminUsers() as $user)
|
||||
<option value="{{ $user->id }}" @if(optional($ticket->assignedUser)->id == $user->id)selected @endif>{{ $user->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="input-group">
|
||||
<label class="input-group-text" for="assign"><i class="fa-solid fa-hat-wizard"></i></label>
|
||||
<select class="form-select" name="assign" id="assign" onchange="document.getElementById('form_assign').submit();">
|
||||
<option value="" @if(!$ticket->assignedUser)selected @endif>{{ __('Personne n\'est assigné') }}</option>
|
||||
@foreach(\App\Models\User::adminUsers() as $user)
|
||||
<option value="{{ $user->id }}" @if(optional($ticket->assignedUser)->id == $user->id)selected @endif>{{ $user->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
@endcan
|
||||
@can('changeStatus', $ticket)
|
||||
<form action="{{ route('ticket.changeStatus', $ticket->id) }}" method="POST" id="form_status" class="col">
|
||||
@csrf
|
||||
<select class="form-select" name="status" id="status" onchange="document.getElementById('form_status').submit();">
|
||||
@foreach(TicketStatus::toArray() as $ticketStatus)
|
||||
<option value="{{ $ticketStatus->value }}" @if($ticketStatus->equals($ticket->status))selected @endif>{{ $ticketStatus->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="input-group">
|
||||
<label class="input-group-text" for="status"><i class="fa-solid fa-sign-hanging"></i></label>
|
||||
<select class="form-select" name="status" id="status" onchange="document.getElementById('form_status').submit();">
|
||||
@foreach(TicketStatus::toArray() as $ticketStatus)
|
||||
<option value="{{ $ticketStatus->value }}" @if($ticketStatus->equals($ticket->status))selected @endif>{{ $ticketStatus->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
@endcan
|
||||
@if($ticket->status->equals(TicketStatus::archived()))
|
||||
|
|
@ -53,12 +57,25 @@
|
|||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@can('assignLabels', App\Models\Ticket::class)
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
@foreach ($ticket->labels as $label)
|
||||
<span class="badge label colorize" data-color="{{ $label->color }}">{{ $label->name }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
<hr>
|
||||
@if($ticket->estimation)
|
||||
@include('estimations.pendingCard', ['ticket' => $ticket, 'estimation' => $ticket->estimation])
|
||||
<hr>
|
||||
@endif
|
||||
<div>
|
||||
<h4>
|
||||
{{ __('Suivi') }}
|
||||
@can('createMessage', $ticket)
|
||||
<button type="button" class="btn btn-sm btn-primary float-end" data-bs-toggle="modal" data-bs-target="#messageModal">
|
||||
<button type="button" class="btn btn-sm btn-primary mx-1 float-end" data-bs-toggle="modal" data-bs-target="#messageModal">
|
||||
{{ __('Répondre') }}
|
||||
</button>
|
||||
<div class="modal fade" id="messageModal" tabindex="-1" aria-labelledby="messageModalLabel" aria-hidden="true">
|
||||
|
|
@ -104,6 +121,9 @@
|
|||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@can('estimate', $ticket)
|
||||
<a href="{{ route('ticket.estimation.estimate', $ticket) }}" class="btn btn-sm btn-info mx-1 float-end">{{ __('Estimation') }}</a>
|
||||
@endcan
|
||||
</h4>
|
||||
@foreach ($messages as $message)
|
||||
@if ($message->user_id === null)
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<?php
|
||||
|
|
@ -1 +0,0 @@
|
|||
<?php
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\BrandController;
|
||||
use App\Http\Controllers\Credit\AccountController;
|
||||
use App\Http\Controllers\Credit\CreditLogController;
|
||||
use App\Http\Controllers\Credit\EstimationController;
|
||||
use App\Http\Controllers\FileController;
|
||||
use App\Http\Controllers\HomeController;
|
||||
use App\Http\Controllers\LabelController;
|
||||
use App\Http\Controllers\LogController;
|
||||
use App\Http\Controllers\MarkerController;
|
||||
use App\Http\Controllers\ScheduleMacroController;
|
||||
|
|
@ -49,7 +54,7 @@ Route::get('/ping', function () {
|
|||
return auth()->check() ? response()->json() : response()->json([], 401);
|
||||
})->withoutMiddleware('auth');
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::middleware('auth:web')->group(function () {
|
||||
|
||||
// * Macro planning
|
||||
Route::get('/macroplanning', [ScheduleMacroController::class, 'show'])->name('macroplanning.show');
|
||||
|
|
@ -65,6 +70,8 @@ Route::middleware(['auth'])->group(function () {
|
|||
// * Resources
|
||||
Route::resource('ticket', TicketController::class)->except(['destroy']);
|
||||
Route::resource('users', UserController::class);
|
||||
Route::resource('labels', LabelController::class)->except('show');
|
||||
Route::resource('brands', BrandController::class);
|
||||
Route::resource('sites', SiteController::class)
|
||||
->withoutMiddleware('xss')
|
||||
->middleware('xss:description,todo');
|
||||
|
|
@ -76,9 +83,34 @@ Route::middleware(['auth'])->group(function () {
|
|||
'macro_markers' => 'marker'
|
||||
]);
|
||||
|
||||
// * Brands and Accounts
|
||||
Route::resource('credit_accounts', AccountController::class)->only(['index'])
|
||||
->parameters([
|
||||
'credit_accounts' => 'account'
|
||||
]);
|
||||
Route::resource('credit_accounts.credit_logs', CreditLogController::class)->only(['index'])
|
||||
->parameters([
|
||||
'credit_accounts' => 'account',
|
||||
'credit_logs' => 'credit_log'
|
||||
]);
|
||||
Route::resource('brands.credit_accounts', AccountController::class)
|
||||
->parameters([
|
||||
'credit_accounts' => 'account'
|
||||
]);
|
||||
Route::get('/brand/{brand}/credit_accounts/{account}/credit', [AccountController::class, 'credit'])
|
||||
->name('brands.credit_accounts.credit');
|
||||
Route::put('/brand/{brand}/credit_accounts/{account}/credit', [AccountController::class, 'creditSubmit'])
|
||||
->name('brands.credit_accounts.creditSubmit');
|
||||
Route::resource('brands.credit_accounts.credit_logs', CreditLogController::class)
|
||||
->only(['index'])
|
||||
->parameters([
|
||||
'credit_accounts' => 'account',
|
||||
'credit_logs' => 'credit_log'
|
||||
]);
|
||||
|
||||
// * LOGS and Files
|
||||
Route::resource('logs', LogController::class);
|
||||
Route::resource('files', FileController::class);
|
||||
Route::resource('logs', LogController::class)->only('index');
|
||||
Route::resource('files', FileController::class)->only('index');
|
||||
Route::resource('projects.logs', LogController::class);
|
||||
Route::resource('projects.files', FileController::class);
|
||||
Route::resource('sites.logs', LogController::class);
|
||||
|
|
@ -86,9 +118,24 @@ Route::middleware(['auth'])->group(function () {
|
|||
|
||||
// * Ticket
|
||||
Route::prefix('/ticket')->name('ticket.')->group(function () {
|
||||
|
||||
// * Estimation
|
||||
Route::name('estimation.')->group(function () {
|
||||
Route::get('/{ticket}/estimate', [EstimationController::class, 'estimate'])
|
||||
->middleware('can:estimate,ticket')
|
||||
->name('estimate');
|
||||
Route::post('/{ticket}/estimate', [EstimationController::class, 'estimateSubmit'])
|
||||
->middleware('can:estimate,ticket')
|
||||
->name('estimateSubmit');
|
||||
Route::post('/{ticket}/estimate/answer', [EstimationController::class, 'estimatePendingSubmit'])
|
||||
->middleware('can:validateEstimate,ticket')
|
||||
->name('estimatePendingSubmit');
|
||||
});
|
||||
|
||||
|
||||
Route::post('/{ticket}/assignTo', [TicketController::class, 'assignTo'])
|
||||
->middleware('can:assignTo,ticket')
|
||||
->name('assignTo');
|
||||
->middleware('can:assignTo,ticket')
|
||||
->name('assignTo');
|
||||
Route::post('/{ticket}/reopen', [TicketController::class, 'reopen'])
|
||||
->middleware('can:reopen,ticket')
|
||||
->name('reopen');
|
||||
|
|
|
|||
4
webpack.mix.js
vendored
4
webpack.mix.js
vendored
|
|
@ -56,3 +56,7 @@ mix.js('resources/js/app.js', 'public/js')
|
|||
|
||||
// Copy Blade Images Assets
|
||||
mix.copyDirectory('resources/assets', 'public/assets')
|
||||
|
||||
// fontawesome compile
|
||||
mix.copy('node_modules/@fortawesome/fontawesome-free/js/all.min.js', 'public/js/fontawesome.min.js')
|
||||
.copy('node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css', 'public/css/fontawesome.min.css');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue