Merge branch 'develop' into trunk

This commit is contained in:
Jérémy Munsch 2022-06-08 08:58:10 +02:00
commit 347bf77d43
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
88 changed files with 3373 additions and 325 deletions

View file

@ -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
View 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);
}
}

View 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')
];
}
}

View 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')
];
}
}

View 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'),
];
}
}

View file

@ -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 [

View file

@ -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);
}
}

View file

@ -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);
}
}

View 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());
}
}

View 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();
}
}

View 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());
}
}

View 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();
}
}

View file

@ -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();

View 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';
}

View file

@ -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();

View file

@ -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();

View file

@ -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,
],
]
];
/**

View file

@ -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',

View 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)");
}
}
],
];
}
}

View 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'
];
}
}

View 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'
];
}
}

View file

@ -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();
}
/**

View file

@ -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'
];
}
}

View file

@ -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
View 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);
}
}

View 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);
}
}

View 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');
}
}

View 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');
}
}

View 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
View 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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View 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)
]
];
}
}

View 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)
]
];
}
}

View 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')
]
];
}
}

View 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)
]
];
}
}

View 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)
]
];
}
}

View file

@ -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)
{
/**

View 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());
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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)
{
/**

View file

@ -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)
])
]
];

View file

@ -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);

View file

@ -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
View file

@ -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",

View file

@ -39,13 +39,7 @@ return [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
]
],
/*

50
config/crud-policies.php Normal file
View 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
];

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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');
});
}
}

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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
View file

@ -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",

View file

@ -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
View file

@ -2,3 +2,4 @@ require('./bootstrap')
require('./session_watch')
require('./vue')
require('./files_add')
require('./label_colorize')

View file

@ -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')

View 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
View 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
View 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')
}
},
}

View file

@ -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',
]
];

View file

@ -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',
]
];

View file

@ -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;
}

View 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 }}&nbsp;:
{{ $account->label }}&nbsp;
&nbsp;<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

View 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

View 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

View 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

View 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
&nbsp;<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

View 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>

View 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>

View file

@ -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

View file

@ -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') }}&nbsp;<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') }}&nbsp;:&nbsp;<span class="text-info">{{ $ticketsNew ?? 0 }}</span></span>

View file

@ -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,

View file

@ -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

View 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

View file

@ -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>&nbsp;{{ __('Créer un ticket') }}</span>
</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ route('ticket.store') }}" enctype="multipart/form-data">
@csrf

View file

@ -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>&nbsp;{{ __('Modifier un ticket') }}</span>
</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ route('ticket.update', $ticket->id) }}" enctype="multipart/form-data">
@csrf

View file

@ -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">

View file

@ -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();">&#9003;</button>
<button class="btn btn-outline-primary" type="submit">&#128269;</button>
</div>
</form>
</div>
</div>
</div>
</h2>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">
#&nbsp;
@include('modules.sorter', ['colname' => 'id'])
</th>
<th scope="col">
{{__('Type') }}&nbsp;
@include('modules.sorter', ['colname' => 'type'])
</th>
<th scope="col">{{__('Fichiers') }}</th>
<th scope="col">
{{__('Crée le') }}&nbsp;
@include('modules.sorter', ['colname' => 'created_at'])
</th>
@if(Auth::user()->isAdmin())
<th scope="col">
{{__('Assigné à') }}&nbsp;
@include('modules.sorter', ['colname' => 'assigned_to'])
</th>
@endif
<th scope="col">
{{__('Créé par') }}&nbsp;
@include('modules.sorter', ['colname' => 'created_by'])
</th>
<th scope="col">
{{__('Statut') }}&nbsp;
@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 }}&nbsp;@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>&nbsp;&nbsp;{{ __('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>&nbsp;{{ __(' 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>&nbsp;-&nbsp;
<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>#&nbsp;</span>
<span>@include('modules.sorter', ['colname' => 'id'])</span>
</th>
<th scope="col">
<span>{{__('Type') }}&nbsp;</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é à') }}&nbsp;</span>
<span>@include('modules.sorter', ['colname' => 'assigned_to'])</span>
</th>
@endif
<th scope="col">
<span>{{__('Créé par') }}&nbsp;</span>
<span>@include('modules.sorter', ['colname' => 'created_by'])</span>
</th>
<th scope="col">
<span>{{__('Le') }}&nbsp;</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 }}&nbsp;&nbsp;@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>&nbsp;-&nbsp;
<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

View file

@ -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)

View file

@ -1 +0,0 @@
<?php

View file

@ -1 +0,0 @@
<?php

View file

@ -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
View file

@ -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');