mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Implement Projects API
This commit is contained in:
parent
ee77993006
commit
dd4d14d128
37
app/Factory/ProjectFactory.php
Normal file
37
app/Factory/ProjectFactory.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Factory;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
|
||||||
|
class ProjectFactory
|
||||||
|
{
|
||||||
|
public static function create(int $company_id, int $user_id) :Project
|
||||||
|
{
|
||||||
|
$project = new Project;
|
||||||
|
$project->company_id = $company_id;
|
||||||
|
$project->user_id = $user_id;
|
||||||
|
|
||||||
|
$project->public_notes = '';
|
||||||
|
$project->private_notes = '';
|
||||||
|
$project->budgeted_hours = 0;
|
||||||
|
$project->task_rate = 0;
|
||||||
|
$project->name = '';
|
||||||
|
$project->custom_value1 = '';
|
||||||
|
$project->custom_value2 = '';
|
||||||
|
$project->custom_value3 = '';
|
||||||
|
$project->custom_value4 = '';
|
||||||
|
$project->is_deleted = 0;
|
||||||
|
|
||||||
|
return $project;
|
||||||
|
}
|
||||||
|
}
|
145
app/Filters/ProjectFilters.php
Normal file
145
app/Filters/ProjectFilters.php
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Filters;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProjectFilters.
|
||||||
|
*/
|
||||||
|
class ProjectFilters extends QueryFilters
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Filter based on search text.
|
||||||
|
*
|
||||||
|
* @param string query filter
|
||||||
|
* @return Illuminate\Database\Query\Builder
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
public function filter(string $filter = '') : Builder
|
||||||
|
{
|
||||||
|
if (strlen($filter) == 0) {
|
||||||
|
return $this->builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder->where(function ($query) use ($filter) {
|
||||||
|
$query->where('projects.name', 'like', '%'.$filter.'%')
|
||||||
|
->orWhere('projects.public_notes', 'like', '%'.$filter.'%')
|
||||||
|
->orWhere('projects.private_notes', 'like', '%'.$filter.'%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the list based on the status
|
||||||
|
* archived, active, deleted.
|
||||||
|
*
|
||||||
|
* @param string filter
|
||||||
|
* @return Illuminate\Database\Query\Builder
|
||||||
|
*/
|
||||||
|
public function status(string $filter = '') : Builder
|
||||||
|
{
|
||||||
|
if (strlen($filter) == 0) {
|
||||||
|
return $this->builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = 'projects';
|
||||||
|
$filters = explode(',', $filter);
|
||||||
|
|
||||||
|
return $this->builder->where(function ($query) use ($filters, $table) {
|
||||||
|
$query->whereNull($table.'.id');
|
||||||
|
|
||||||
|
if (in_array(parent::STATUS_ACTIVE, $filters)) {
|
||||||
|
$query->orWhereNull($table.'.deleted_at');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array(parent::STATUS_ARCHIVED, $filters)) {
|
||||||
|
$query->orWhere(function ($query) use ($table) {
|
||||||
|
$query->whereNotNull($table.'.deleted_at');
|
||||||
|
|
||||||
|
if (! in_array($table, ['users'])) {
|
||||||
|
$query->where($table.'.is_deleted', '=', 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array(parent::STATUS_DELETED, $filters)) {
|
||||||
|
$query->orWhere($table.'.is_deleted', '=', 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts the list based on $sort.
|
||||||
|
*
|
||||||
|
* @param string sort formatted as column|asc
|
||||||
|
* @return Illuminate\Database\Query\Builder
|
||||||
|
*/
|
||||||
|
public function sort(string $sort) : Builder
|
||||||
|
{
|
||||||
|
$sort_col = explode('|', $sort);
|
||||||
|
|
||||||
|
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the base query.
|
||||||
|
*
|
||||||
|
* @param int company_id
|
||||||
|
* @return Illuminate\Database\Query\Builder
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
public function baseQuery(int $company_id, User $user) : Builder
|
||||||
|
{
|
||||||
|
$query = DB::table('projects')
|
||||||
|
->join('companies', 'companies.id', '=', 'projects.company_id')
|
||||||
|
->where('projects.company_id', '=', $company_id)
|
||||||
|
//->whereRaw('(projects.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices
|
||||||
|
->select(
|
||||||
|
'projects.id',
|
||||||
|
'projects.name',
|
||||||
|
'projects.public_notes',
|
||||||
|
'projects.private_notes',
|
||||||
|
'projects.created_at',
|
||||||
|
'projects.created_at as project_created_at',
|
||||||
|
'projects.deleted_at',
|
||||||
|
'projects.is_deleted',
|
||||||
|
'projects.user_id',
|
||||||
|
'projects.assigned_user_id',
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If the user does not have permissions to view all invoices
|
||||||
|
* limit the user to only the invoices they have created
|
||||||
|
*/
|
||||||
|
if (Gate::denies('view-list', Project::class)) {
|
||||||
|
$query->where('projects.user_id', '=', $user->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the query by the users company ID.
|
||||||
|
*
|
||||||
|
* @param $company_id The company Id
|
||||||
|
* @return Illuminate\Database\Query\Builder
|
||||||
|
*/
|
||||||
|
public function entityFilter()
|
||||||
|
{
|
||||||
|
//return $this->builder->whereCompanyId(auth()->user()->company()->id);
|
||||||
|
return $this->builder->whereCompanyId(auth()->user()->company()->id)->orWhere('company_id', null);
|
||||||
|
}
|
||||||
|
}
|
485
app/Http/Controllers/ProjectController.php
Normal file
485
app/Http/Controllers/ProjectController.php
Normal file
@ -0,0 +1,485 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Factory\ProjectFactory;
|
||||||
|
use App\Filters\ProjectFilters;
|
||||||
|
use App\Http\Requests\Project\CreateProjectRequest;
|
||||||
|
use App\Http\Requests\Project\DestroyProjectRequest;
|
||||||
|
use App\Http\Requests\Project\EditProjectRequest;
|
||||||
|
use App\Http\Requests\Project\ShowProjectRequest;
|
||||||
|
use App\Http\Requests\Project\StoreProjectRequest;
|
||||||
|
use App\Http\Requests\Project\UpdateProjectRequest;
|
||||||
|
use App\Jobs\Entity\ActionEntity;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Repositories\ProjectRepository;
|
||||||
|
use App\Transformers\ProjectTransformer;
|
||||||
|
use App\Utils\Traits\BulkOptions;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ProjectController.
|
||||||
|
*/
|
||||||
|
class ProjectController extends BaseController
|
||||||
|
{
|
||||||
|
use MakesHash;
|
||||||
|
|
||||||
|
protected $entity_type = Project::class;
|
||||||
|
|
||||||
|
protected $entity_transformer = ProjectTransformer::class;
|
||||||
|
|
||||||
|
protected $project_repo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProjectController constructor.
|
||||||
|
* @param ProjectRepository $projectRepo
|
||||||
|
*/
|
||||||
|
public function __construct(ProjectRepository $project_repo)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->project_repo = $project_repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/projects",
|
||||||
|
* operationId="getProjects",
|
||||||
|
* tags={"projects"},
|
||||||
|
* summary="Gets a list of projects",
|
||||||
|
* description="Lists projects",
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/index"),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="A list of projects",
|
||||||
|
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Project"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||||
|
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="default",
|
||||||
|
* description="Unexpected Error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function index(ProjectFilters $filters)
|
||||||
|
{
|
||||||
|
$projects = Project::filter($filters);
|
||||||
|
|
||||||
|
return $this->listResponse($projects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/projects/{id}",
|
||||||
|
* operationId="showProject",
|
||||||
|
* tags={"projects"},
|
||||||
|
* summary="Shows a project",
|
||||||
|
* description="Displays a project by id",
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="id",
|
||||||
|
* in="path",
|
||||||
|
* description="The Project Hashed ID",
|
||||||
|
* example="D2J234DFA",
|
||||||
|
* required=true,
|
||||||
|
* @OA\Schema(
|
||||||
|
* type="string",
|
||||||
|
* format="string",
|
||||||
|
* ),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Returns the expense object",
|
||||||
|
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Project"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||||
|
*
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="default",
|
||||||
|
* description="Unexpected Error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function show(ShowProjectRequest $request, Project $project)
|
||||||
|
{
|
||||||
|
return $this->itemResponse($project);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/projects/{id}/edit",
|
||||||
|
* operationId="editProject",
|
||||||
|
* tags={"projects"},
|
||||||
|
* summary="Shows a project for editting",
|
||||||
|
* description="Displays a project by id",
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="id",
|
||||||
|
* in="path",
|
||||||
|
* description="The Project Hashed ID",
|
||||||
|
* example="D2J234DFA",
|
||||||
|
* required=true,
|
||||||
|
* @OA\Schema(
|
||||||
|
* type="string",
|
||||||
|
* format="string",
|
||||||
|
* ),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Returns the project object",
|
||||||
|
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Project"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||||
|
*
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="default",
|
||||||
|
* description="Unexpected Error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function edit(EditProjectRequest $request, Project $project)
|
||||||
|
{
|
||||||
|
return $this->itemResponse($project);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param App\Models\Project $project
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @OA\Put(
|
||||||
|
* path="/api/v1/projects/{id}",
|
||||||
|
* operationId="updateProject",
|
||||||
|
* tags={"projects"},
|
||||||
|
* summary="Updates a project",
|
||||||
|
* description="Handles the updating of a project by id",
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="id",
|
||||||
|
* in="path",
|
||||||
|
* description="The Project Hashed ID",
|
||||||
|
* example="D2J234DFA",
|
||||||
|
* required=true,
|
||||||
|
* @OA\Schema(
|
||||||
|
* type="string",
|
||||||
|
* format="string",
|
||||||
|
* ),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Returns the project object",
|
||||||
|
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Project"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||||
|
*
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="default",
|
||||||
|
* description="Unexpected Error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function update(UpdateProjectRequest $request, Project $project)
|
||||||
|
{
|
||||||
|
if ($request->entityIsDeleted($project)) {
|
||||||
|
return $request->disallowUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
$project->fill($request->all());
|
||||||
|
$project->save();
|
||||||
|
|
||||||
|
return $this->itemResponse($project->fresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/projects/create",
|
||||||
|
* operationId="getProjectsCreate",
|
||||||
|
* tags={"projects"},
|
||||||
|
* summary="Gets a new blank project object",
|
||||||
|
* description="Returns a blank object with default values",
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="A blank project object",
|
||||||
|
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Project"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||||
|
*
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="default",
|
||||||
|
* description="Unexpected Error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function create(CreateProjectRequest $request)
|
||||||
|
{
|
||||||
|
$project = ProjectFactory::create(auth()->user()->company()->id, auth()->user()->id);
|
||||||
|
|
||||||
|
return $this->itemResponse($project);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/v1/projects",
|
||||||
|
* operationId="storeProject",
|
||||||
|
* tags={"projects"},
|
||||||
|
* summary="Adds a project",
|
||||||
|
* description="Adds an project to a company",
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Returns the saved project object",
|
||||||
|
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Project"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||||
|
*
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="default",
|
||||||
|
* description="Unexpected Error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function store(StoreProjectRequest $request)
|
||||||
|
{
|
||||||
|
$project = ProjectFactory::create(auth()->user()->company()->id, auth()->user()->id);
|
||||||
|
$project->fill($request->all());
|
||||||
|
$project->save();
|
||||||
|
|
||||||
|
return $this->itemResponse($project->fresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @OA\Delete(
|
||||||
|
* path="/api/v1/projects/{id}",
|
||||||
|
* operationId="deleteProject",
|
||||||
|
* tags={"projects"},
|
||||||
|
* summary="Deletes a project",
|
||||||
|
* description="Handles the deletion of a project by id",
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="id",
|
||||||
|
* in="path",
|
||||||
|
* description="The Project Hashed ID",
|
||||||
|
* example="D2J234DFA",
|
||||||
|
* required=true,
|
||||||
|
* @OA\Schema(
|
||||||
|
* type="string",
|
||||||
|
* format="string",
|
||||||
|
* ),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Returns a HTTP status",
|
||||||
|
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||||
|
*
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="default",
|
||||||
|
* description="Unexpected Error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function destroy(DestroyProjectRequest $request, Project $project)
|
||||||
|
{
|
||||||
|
//may not need these destroy routes as we are using actions to 'archive/delete'
|
||||||
|
$project->is_deleted = true;
|
||||||
|
$project->delete();
|
||||||
|
$project->save();
|
||||||
|
|
||||||
|
return response()->json([], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform bulk actions on the list view.
|
||||||
|
*
|
||||||
|
* @param BulkProjectRequest $request
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/v1/projects/bulk",
|
||||||
|
* operationId="bulkProjects",
|
||||||
|
* tags={"projects"},
|
||||||
|
* summary="Performs bulk actions on an array of projects",
|
||||||
|
* description="",
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/index"),
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* description="User credentials",
|
||||||
|
* required=true,
|
||||||
|
* @OA\MediaType(
|
||||||
|
* mediaType="application/json",
|
||||||
|
* @OA\Schema(
|
||||||
|
* type="array",
|
||||||
|
* @OA\Items(
|
||||||
|
* type="integer",
|
||||||
|
* description="Array of hashed IDs to be bulk 'actioned",
|
||||||
|
* example="[0,1,2,3]",
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="The Project User response",
|
||||||
|
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Project"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="default",
|
||||||
|
* description="Unexpected Error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function bulk()
|
||||||
|
{
|
||||||
|
$action = request()->input('action');
|
||||||
|
|
||||||
|
$ids = request()->input('ids');
|
||||||
|
|
||||||
|
$projects = Project::withTrashed()->find($this->transformKeys($ids));
|
||||||
|
|
||||||
|
$projects->each(function ($project, $key) use ($action) {
|
||||||
|
if (auth()->user()->can('edit', $project)) {
|
||||||
|
$this->project_repo->{$action}($project);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->listResponse(Project::withTrashed()->whereIn('id', $this->transformKeys($ids)));
|
||||||
|
}
|
||||||
|
}
|
@ -1,47 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests\Client;
|
|
||||||
|
|
||||||
use App\Models\Client;
|
|
||||||
use App\Utils\Traits\BulkOptions;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class BulkClientRequest extends FormRequest
|
|
||||||
{
|
|
||||||
use BulkOptions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function authorize()
|
|
||||||
{
|
|
||||||
if (! $this->has('action')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! in_array($this->action, $this->getBulkOptions(), true)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return auth()->user()->can(auth()->user()->isAdmin(), Client::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the validation rules that apply to the request.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function rules()
|
|
||||||
{
|
|
||||||
$rules = $this->getGlobalRules();
|
|
||||||
|
|
||||||
/* We don't require IDs on bulk storing. */
|
|
||||||
if ($this->action !== self::$STORE_METHOD) {
|
|
||||||
$rules['ids'] = ['required'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rules;
|
|
||||||
}
|
|
||||||
}
|
|
28
app/Http/Requests/Project/CreateProjectRequest.php
Normal file
28
app/Http/Requests/Project/CreateProjectRequest.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Project;
|
||||||
|
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
use App\Models\Project;
|
||||||
|
|
||||||
|
class CreateProjectRequest extends Request
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize() : bool
|
||||||
|
{
|
||||||
|
return auth()->user()->isAdmin();
|
||||||
|
}
|
||||||
|
}
|
27
app/Http/Requests/Project/DestroyProjectRequest.php
Normal file
27
app/Http/Requests/Project/DestroyProjectRequest.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Project;
|
||||||
|
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
|
||||||
|
class DestroyProjectRequest extends Request
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize() : bool
|
||||||
|
{
|
||||||
|
return auth()->user()->isAdmin();
|
||||||
|
}
|
||||||
|
}
|
39
app/Http/Requests/Project/EditProjectRequest.php
Normal file
39
app/Http/Requests/Project/EditProjectRequest.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Project;
|
||||||
|
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
|
||||||
|
class EditProjectRequest extends Request
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize() : bool
|
||||||
|
{
|
||||||
|
return auth()->user()->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
39
app/Http/Requests/Project/ShowProjectRequest.php
Normal file
39
app/Http/Requests/Project/ShowProjectRequest.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Project;
|
||||||
|
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
|
||||||
|
class ShowProjectRequest extends Request
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize() : bool
|
||||||
|
{
|
||||||
|
return auth()->user()->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
53
app/Http/Requests/Project/StoreProjectRequest.php
Normal file
53
app/Http/Requests/Project/StoreProjectRequest.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Project;
|
||||||
|
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
|
|
||||||
|
class StoreProjectRequest extends Request
|
||||||
|
{
|
||||||
|
use MakesHash;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize() : bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('create', Project::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
$rules = [];
|
||||||
|
|
||||||
|
$rules['name'] ='required|unique:projects,name,null,null,company_id,'.auth()->user()->companyId();
|
||||||
|
$rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id;
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation()
|
||||||
|
{
|
||||||
|
$input = $this->all();
|
||||||
|
|
||||||
|
if (array_key_exists('client_id', $input) && is_string($input['client_id'])) {
|
||||||
|
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$this->replace($input);
|
||||||
|
}
|
||||||
|
}
|
43
app/Http/Requests/Project/UpdateProjectRequest.php
Normal file
43
app/Http/Requests/Project/UpdateProjectRequest.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Project;
|
||||||
|
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Utils\Traits\ChecksEntityStatus;
|
||||||
|
|
||||||
|
class UpdateProjectRequest extends Request
|
||||||
|
{
|
||||||
|
use ChecksEntityStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize() : bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('edit', $this->project);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation()
|
||||||
|
{
|
||||||
|
$input = $this->all();
|
||||||
|
|
||||||
|
$this->replace($input);
|
||||||
|
}
|
||||||
|
}
|
@ -117,7 +117,7 @@ class Gateway extends StaticModel
|
|||||||
return ['methods' => [GatewayType::CREDIT_CARD], 'refund' => true, 'token_billing' => true ]; //Checkout
|
return ['methods' => [GatewayType::CREDIT_CARD], 'refund' => true, 'token_billing' => true ]; //Checkout
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return [];
|
return ['methods' => [], 'refund' => false, 'token_billing' => false];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\Filterable;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Laracasts\Presenter\PresentableTrait;
|
use Laracasts\Presenter\PresentableTrait;
|
||||||
|
|
||||||
@ -13,7 +14,8 @@ class Project extends BaseModel
|
|||||||
// Expense Categories
|
// Expense Categories
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
use PresentableTrait;
|
use PresentableTrait;
|
||||||
|
use Filterable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
@ -24,17 +26,16 @@ class Project extends BaseModel
|
|||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
|
'client_id',
|
||||||
'task_rate',
|
'task_rate',
|
||||||
'private_notes',
|
'private_notes',
|
||||||
|
'public_notes',
|
||||||
'due_date',
|
'due_date',
|
||||||
'budgeted_hours',
|
'budgeted_hours',
|
||||||
'custom_value1',
|
'custom_value1',
|
||||||
'custom_value2',
|
'custom_value2',
|
||||||
];
|
'custom_value3',
|
||||||
|
'custom_value4',
|
||||||
protected $casts = [
|
|
||||||
'updated_at' => 'timestamp',
|
|
||||||
'created_at' => 'timestamp',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function getEntityType()
|
public function getEntityType()
|
||||||
|
41
app/Policies/ProjectPolicy.php
Normal file
41
app/Policies/ProjectPolicy.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
|
class ProjectPolicy extends EntityPolicy
|
||||||
|
{
|
||||||
|
use HandlesAuthorization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new policy instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user has create permissions.
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function create(User $user) : bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin() || $user->hasPermission('create_project') || $user->hasPermission('create_all');
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,7 @@ use App\Models\Invoice;
|
|||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
use App\Models\PaymentTerm;
|
use App\Models\PaymentTerm;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
|
use App\Models\Project;
|
||||||
use App\Models\Quote;
|
use App\Models\Quote;
|
||||||
use App\Models\RecurringInvoice;
|
use App\Models\RecurringInvoice;
|
||||||
use App\Models\RecurringQuote;
|
use App\Models\RecurringQuote;
|
||||||
@ -46,6 +47,7 @@ use App\Policies\InvoicePolicy;
|
|||||||
use App\Policies\PaymentPolicy;
|
use App\Policies\PaymentPolicy;
|
||||||
use App\Policies\PaymentTermPolicy;
|
use App\Policies\PaymentTermPolicy;
|
||||||
use App\Policies\ProductPolicy;
|
use App\Policies\ProductPolicy;
|
||||||
|
use App\Policies\ProjectPolicy;
|
||||||
use App\Policies\QuotePolicy;
|
use App\Policies\QuotePolicy;
|
||||||
use App\Policies\RecurringInvoicePolicy;
|
use App\Policies\RecurringInvoicePolicy;
|
||||||
use App\Policies\RecurringQuotePolicy;
|
use App\Policies\RecurringQuotePolicy;
|
||||||
@ -79,6 +81,7 @@ class AuthServiceProvider extends ServiceProvider
|
|||||||
Payment::class => PaymentPolicy::class,
|
Payment::class => PaymentPolicy::class,
|
||||||
PaymentTerm::class => PaymentTermPolicy::class,
|
PaymentTerm::class => PaymentTermPolicy::class,
|
||||||
Product::class => ProductPolicy::class,
|
Product::class => ProductPolicy::class,
|
||||||
|
Project::class => ProjectPolicy::class,
|
||||||
Quote::class => QuotePolicy::class,
|
Quote::class => QuotePolicy::class,
|
||||||
RecurringInvoice::class => RecurringInvoicePolicy::class,
|
RecurringInvoice::class => RecurringInvoicePolicy::class,
|
||||||
RecurringQuote::class => RecurringQuotePolicy::class,
|
RecurringQuote::class => RecurringQuotePolicy::class,
|
||||||
|
31
app/Repositories/ProjectRepository.php
Normal file
31
app/Repositories/ProjectRepository.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for project repository.
|
||||||
|
*/
|
||||||
|
class ProjectRepository extends BaseRepository
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the class name.
|
||||||
|
*
|
||||||
|
* @return string The class name.
|
||||||
|
*/
|
||||||
|
public function getClassName()
|
||||||
|
{
|
||||||
|
return Project::class;
|
||||||
|
}
|
||||||
|
}
|
@ -51,6 +51,7 @@ class GatewayTransformer extends EntityTransformer
|
|||||||
'fields' => (string) $gateway->fields ?: '',
|
'fields' => (string) $gateway->fields ?: '',
|
||||||
'updated_at' => (int) $gateway->updated_at,
|
'updated_at' => (int) $gateway->updated_at,
|
||||||
'created_at' => (int) $gateway->created_at,
|
'created_at' => (int) $gateway->created_at,
|
||||||
|
'options' => $gateway->getMethods(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,20 +34,23 @@ class ProjectTransformer extends EntityTransformer
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => (string) $this->encodePrimaryKey($project->id),
|
'id' => (string) $this->encodePrimaryKey($project->id),
|
||||||
'name' => $project->name ?: '',
|
'user_id' => (string) $this->encodePrimaryKey($project->user_id),
|
||||||
|
'assigned_user_id' => (string) $this->encodePrimaryKey($project->assigned_user_id),
|
||||||
'client_id' => (string) $this->encodePrimaryKey($project->client_id),
|
'client_id' => (string) $this->encodePrimaryKey($project->client_id),
|
||||||
|
'name' => $project->name ?: '',
|
||||||
'created_at' => (int) $project->created_at,
|
'created_at' => (int) $project->created_at,
|
||||||
'updated_at' => (int) $project->updated_at,
|
'updated_at' => (int) $project->updated_at,
|
||||||
'archived_at' => (int) $project->deleted_at,
|
'archived_at' => (int) $project->deleted_at,
|
||||||
'is_deleted' => (bool) $project->is_deleted,
|
'is_deleted' => (bool) $project->is_deleted,
|
||||||
'task_rate' => (float) $project->task_rate,
|
'task_rate' => (float) $project->task_rate,
|
||||||
'due_date' => $project->due_date ?: '',
|
'due_date' => $project->due_date ?: '',
|
||||||
'private_notes' => $project->private_notes ?: '',
|
'private_notes' => (string) $project->private_notes ?: '',
|
||||||
|
'public_notes' => (string) $project->public_notes ?: '',
|
||||||
'budgeted_hours' => (float) $project->budgeted_hours,
|
'budgeted_hours' => (float) $project->budgeted_hours,
|
||||||
'custom_value1' => $project->custom_value1 ?: '',
|
'custom_value1' => (string) $project->custom_value1 ?: '',
|
||||||
'custom_value2' => $project->custom_value2 ?: '',
|
'custom_value2' => (string) $project->custom_value2 ?: '',
|
||||||
'custom_value3' => $project->custom_value3 ?: '',
|
'custom_value3' => (string) $project->custom_value3 ?: '',
|
||||||
'custom_value4' => $project->custom_value4 ?: '',
|
'custom_value4' => (string) $project->custom_value4 ?: '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
composer.lock
generated
38
composer.lock
generated
@ -108,16 +108,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "aws/aws-sdk-php",
|
"name": "aws/aws-sdk-php",
|
||||||
"version": "3.158.2",
|
"version": "3.158.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||||
"reference": "b80957465d94c127254e36061dd3d0c3ccc94cc1"
|
"reference": "c0c0df79edc0a646a7ccd6b2e8d1723ff4ba88e2"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b80957465d94c127254e36061dd3d0c3ccc94cc1",
|
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c0c0df79edc0a646a7ccd6b2e8d1723ff4ba88e2",
|
||||||
"reference": "b80957465d94c127254e36061dd3d0c3ccc94cc1",
|
"reference": "c0c0df79edc0a646a7ccd6b2e8d1723ff4ba88e2",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -189,7 +189,7 @@
|
|||||||
"s3",
|
"s3",
|
||||||
"sdk"
|
"sdk"
|
||||||
],
|
],
|
||||||
"time": "2020-10-05T18:13:27+00:00"
|
"time": "2020-10-07T18:12:22+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@ -1814,16 +1814,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "google/auth",
|
"name": "google/auth",
|
||||||
"version": "v1.14.0",
|
"version": "v1.14.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/googleapis/google-auth-library-php.git",
|
"url": "https://github.com/googleapis/google-auth-library-php.git",
|
||||||
"reference": "95c23ebd89a0a4d1b511aed81426f57388ab7268"
|
"reference": "2df57c61c2fd739a15a81f792b1ccedc3e06d2b6"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/95c23ebd89a0a4d1b511aed81426f57388ab7268",
|
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/2df57c61c2fd739a15a81f792b1ccedc3e06d2b6",
|
||||||
"reference": "95c23ebd89a0a4d1b511aed81426f57388ab7268",
|
"reference": "2df57c61c2fd739a15a81f792b1ccedc3e06d2b6",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -1862,7 +1862,7 @@
|
|||||||
"google",
|
"google",
|
||||||
"oauth2"
|
"oauth2"
|
||||||
],
|
],
|
||||||
"time": "2020-10-02T22:20:36+00:00"
|
"time": "2020-10-06T18:10:43+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "graham-campbell/result-type",
|
"name": "graham-campbell/result-type",
|
||||||
@ -2497,16 +2497,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/framework",
|
"name": "laravel/framework",
|
||||||
"version": "v8.8.0",
|
"version": "v8.9.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/framework.git",
|
"url": "https://github.com/laravel/framework.git",
|
||||||
"reference": "0bdd5c6f12cb7cb6644e484169656245af417735"
|
"reference": "8a6bf870bcfa1597e514a9c7ee6df44db98abb54"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/framework/zipball/0bdd5c6f12cb7cb6644e484169656245af417735",
|
"url": "https://api.github.com/repos/laravel/framework/zipball/8a6bf870bcfa1597e514a9c7ee6df44db98abb54",
|
||||||
"reference": "0bdd5c6f12cb7cb6644e484169656245af417735",
|
"reference": "8a6bf870bcfa1597e514a9c7ee6df44db98abb54",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -2656,7 +2656,7 @@
|
|||||||
"framework",
|
"framework",
|
||||||
"laravel"
|
"laravel"
|
||||||
],
|
],
|
||||||
"time": "2020-10-02T14:33:08+00:00"
|
"time": "2020-10-06T14:22:36+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/slack-notification-channel",
|
"name": "laravel/slack-notification-channel",
|
||||||
@ -6504,12 +6504,12 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/console.git",
|
"url": "https://github.com/symfony/console.git",
|
||||||
"reference": "04c3a31fe8ea94b42c9e2d1acc93d19782133b00"
|
"reference": "ae789a8a2ad189ce7e8216942cdb9b77319f5eb8"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/console/zipball/04c3a31fe8ea94b42c9e2d1acc93d19782133b00",
|
"url": "https://api.github.com/repos/symfony/console/zipball/ae789a8a2ad189ce7e8216942cdb9b77319f5eb8",
|
||||||
"reference": "04c3a31fe8ea94b42c9e2d1acc93d19782133b00",
|
"reference": "ae789a8a2ad189ce7e8216942cdb9b77319f5eb8",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -6589,7 +6589,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2020-09-18T14:27:32+00:00"
|
"time": "2020-10-07T15:23:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/css-selector",
|
"name": "symfony/css-selector",
|
||||||
|
@ -40,6 +40,7 @@ class UpdateGatewayTableVisibleColumn extends Migration
|
|||||||
$t->text('public_notes')->nullable();
|
$t->text('public_notes')->nullable();
|
||||||
$t->dropColumn('description');
|
$t->dropColumn('description');
|
||||||
$t->decimal('budgeted_hours', 12,2)->change();
|
$t->decimal('budgeted_hours', 12,2)->change();
|
||||||
|
$t->boolean('is_deleted')->default(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,9 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
|
|||||||
|
|
||||||
Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk');
|
Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk');
|
||||||
|
|
||||||
|
Route::resource('projects', 'ProjectController'); // name = (projects. index / create / show / update / destroy / edit
|
||||||
|
Route::post('projects/bulk', 'ProjectController@bulk')->name('projects.bulk');
|
||||||
|
|
||||||
Route::resource('vendors', 'VendorController'); // name = (vendors. index / create / show / update / destroy / edit
|
Route::resource('vendors', 'VendorController'); // name = (vendors. index / create / show / update / destroy / edit
|
||||||
|
|
||||||
Route::post('vendors/bulk', 'VendorController@bulk')->name('vendors.bulk');
|
Route::post('vendors/bulk', 'VendorController@bulk')->name('vendors.bulk');
|
||||||
@ -157,13 +160,6 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
|
|||||||
Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk');
|
Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk');
|
||||||
|
|
||||||
|
|
||||||
Route::resource('credits', 'CreditController'); // name = (credits. index / create / show / update / destroy / edit
|
|
||||||
|
|
||||||
Route::post('credits/bulk', 'CreditController@bulk')->name('credits.bulk');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Route::get('settings', 'SettingsController@index')->name('user.settings');
|
Route::get('settings', 'SettingsController@index')->name('user.settings');
|
||||||
*/
|
*/
|
||||||
Route::get('scheduler', 'SchedulerController@index');
|
Route::get('scheduler', 'SchedulerController@index');
|
||||||
|
155
tests/Feature/ProjectApiTest.php
Normal file
155
tests/Feature/ProjectApiTest.php
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\DataMapper\DefaultSettings;
|
||||||
|
use App\Models\Account;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectContact;
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use Faker\Factory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Foundation\Testing\WithFaker;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
use Tests\MockAccountData;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @covers App\Http\Controllers\ProjectController
|
||||||
|
*/
|
||||||
|
class ProjectApiTest extends TestCase
|
||||||
|
{
|
||||||
|
use MakesHash;
|
||||||
|
use DatabaseTransactions;
|
||||||
|
use MockAccountData;
|
||||||
|
|
||||||
|
public function setUp() :void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->makeTestData();
|
||||||
|
|
||||||
|
Session::start();
|
||||||
|
|
||||||
|
$this->faker = \Faker\Factory::create();
|
||||||
|
|
||||||
|
Model::reguard();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProjectGet()
|
||||||
|
{
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->get('/api/v1/projects/'.$this->encodePrimaryKey($this->project->id));
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testProjectPost()
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'name' => $this->faker->firstName,
|
||||||
|
'client_id' => $this->client->hashed_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->post('/api/v1/projects', $data);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProjectPut()
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'name' => $this->faker->firstName,
|
||||||
|
'public_notes' => 'Coolio',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->put('/api/v1/projects/'.$this->encodePrimaryKey($this->project->id), $data);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testProjectNotArchived()
|
||||||
|
{
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->get('/api/v1/projects/'.$this->encodePrimaryKey($this->project->id));
|
||||||
|
|
||||||
|
$arr = $response->json();
|
||||||
|
|
||||||
|
$this->assertEquals(0, $arr['data']['archived_at']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProjectArchived()
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'ids' => [$this->encodePrimaryKey($this->project->id)],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->post('/api/v1/projects/bulk?action=archive', $data);
|
||||||
|
|
||||||
|
$arr = $response->json();
|
||||||
|
|
||||||
|
$this->assertNotNull($arr['data'][0]['archived_at']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProjectRestored()
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'ids' => [$this->encodePrimaryKey($this->project->id)],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->post('/api/v1/projects/bulk?action=restore', $data);
|
||||||
|
|
||||||
|
$arr = $response->json();
|
||||||
|
|
||||||
|
$this->assertEquals(0, $arr['data'][0]['archived_at']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProjectDeleted()
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'ids' => [$this->encodePrimaryKey($this->project->id)],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->post('/api/v1/projects/bulk?action=delete', $data);
|
||||||
|
|
||||||
|
$arr = $response->json();
|
||||||
|
|
||||||
|
$this->assertTrue($arr['data'][0]['is_deleted']);
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,7 @@ use App\Models\Expense;
|
|||||||
use App\Models\GroupSetting;
|
use App\Models\GroupSetting;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\InvoiceInvitation;
|
use App\Models\InvoiceInvitation;
|
||||||
|
use App\Models\Project;
|
||||||
use App\Models\Quote;
|
use App\Models\Quote;
|
||||||
use App\Models\QuoteInvitation;
|
use App\Models\QuoteInvitation;
|
||||||
use App\Models\RecurringInvoice;
|
use App\Models\RecurringInvoice;
|
||||||
@ -200,7 +201,10 @@ trait MockAccountData
|
|||||||
'send_email' => true,
|
'send_email' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->project = Project::factory()->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'company_id' => $this->company->id,
|
||||||
|
]);
|
||||||
|
|
||||||
$this->expense = Expense::factory()->create([
|
$this->expense = Expense::factory()->create([
|
||||||
'user_id' => $this->user->id,
|
'user_id' => $this->user->id,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user