Estimation functionnality

This commit is contained in:
Jérémy Munsch 2022-05-31 14:06:07 +02:00
parent 95fc7c797b
commit 3dc367ccde
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
61 changed files with 2370 additions and 77 deletions

View file

@ -1,3 +1,6 @@
### 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

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

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

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

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

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

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

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

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

@ -57,7 +57,21 @@ class TicketPolicy extends RootPolicy
TicketStaticRules::make('ticketIsNotArchived', $model)
]),
'assignTickets' => URR::make('userIsAdmin', $user),
'assignLabels' => 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,6 +2,8 @@
namespace App\Providers;
use App\Models\Brand;
use App\Models\Credit\Account;
use App\Models\File;
use App\Models\Label;
use App\Models\Log;
@ -39,15 +41,12 @@ 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);

View file

@ -1,6 +1,6 @@
{
"name": "ldcweb/supportweb",
"version": "1.1.0",
"version": "1.2.0",
"type": "project",
"description": "The Laravel Framework.",
"keywords": [
@ -15,7 +15,7 @@
"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",

14
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": "a9e301f0bab428769de214ee3b71cd46",
"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",

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

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

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

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

@ -13,24 +13,44 @@
<ul class="navbar-nav me-auto">
@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
@can('viewAny', App\Models\Site::class)
<li class="nav-item">
<a class="nav-link @if(strpos(request()->route()->getName(), 'sites') === 0)active @endif" href="{{ route('sites.index') }}">Sites</a>
@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>
@endcan
@can('viewAny', App\Models\Project::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>
@endcan
@endif
@can('viewAny', App\Models\User::class)
<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('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(), '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>
@ -42,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

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

@ -6,11 +6,7 @@
<div class="form-row row">
<h2 class="col">
<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 n° :ticketId : :ticketSiteName - :ticketTypeLabel', [
'ticketId' => $ticket->id,
'ticketSiteName' => $ticket->site->name,
'ticketTypeLabel' => $ticket->type->label
]) }}
{{ $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>
@ -71,11 +67,15 @@
</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">
@ -121,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,5 +1,9 @@
<?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;
@ -50,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');
@ -66,10 +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('labels', LabelController::class)->except('show');
Route::resource('brands', BrandController::class);
Route::resource('sites', SiteController::class)
->withoutMiddleware('xss')
->middleware('xss:description,todo');
@ -81,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);
@ -91,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');