make documents polymorphic

shift-build-2464
Nadim Salloum 2021-06-25 17:35:37 +03:00
parent 9c747b2aee
commit a7cb0e9844
20 changed files with 172 additions and 61 deletions

View File

@ -373,6 +373,17 @@ class CarController extends Controller
'known_damage' => $car->known_damage,
'notes' => $car->notes,
'deleted_at' => $car->deleted_at,
'documents' => $car->documents()->orderBy('created_at', 'asc')->get()
->map(function ($document) {
return [
'id' => $document->id,
'name' => $document->name,
'size' => $document->size,
'extension' => $document->extension,
'link' => $document->link,
'created_at' => $document->created_at,
];
}),
'buy_contracts' => $car->buyContracts()
->orderBy('date', 'desc')
->with('contact')

View File

@ -212,6 +212,17 @@ class ContactController extends Controller
'city' => $contact->city,
'country' => $contact->country,
'deleted_at' => $contact->deleted_at,
'documents' => $contact->documents()->orderBy('created_at', 'asc')->get()
->map(function ($document) {
return [
'id' => $document->id,
'name' => $document->name,
'size' => $document->size,
'extension' => $document->extension,
'link' => $document->link,
'created_at' => $document->created_at,
];
}),
'buy_contracts' => $contact->buyContracts()
->orderBy('date', 'desc')
->with('car')

View File

@ -7,10 +7,11 @@ use App\Models\Document;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Database\Eloquent\Relations\Relation;
class DocumentController extends Controller
{
public function show(Contract $contract, Document $document)
public function show(Document $document)
{
if (file_exists($document->path)) {
header('Content-Disposition: filename="' . $document->name . '"');
@ -20,8 +21,14 @@ class DocumentController extends Controller
abort(404);
}
public function store(Request $request, Contract $contract)
public function store(Request $request)
{
$class = $request->get('documentable_type');
$id = $request->get('documentable_id');
if (!in_array($class, ['contracts', 'cars', 'contacts'])) {
return [];
}
$file = $request->file()['document'];
$internalName = date('Y-m-d-H-i-s') . '.' . $file->extension();
$document = Document::create([
@ -29,9 +36,10 @@ class DocumentController extends Controller
'internal_name' => $internalName,
'size' => $file->getSize(),
'extension' => $file->extension() ?? '',
'contract_id' => $contract->id,
'documentable_type' => $class,
'documentable_id' => $id,
]);
$file->move(public_path("documents/contracts/{$contract->id}/"), $internalName);
$file->move(public_path("documents/{$class}/{$id}/"), $internalName);
return [
'id' => $document->id,
@ -43,7 +51,7 @@ class DocumentController extends Controller
];
}
public function destroy(Request $request, Contract $contract)
public function destroy(Request $request)
{
$document = Document::find((int)$request->get('id'));

View File

@ -2,9 +2,8 @@
namespace App\Models;
use App\Enums\ContractType;
use Carbon\Carbon;
use Cknow\Money\Money;
use App\Enums\ContractType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -25,6 +24,11 @@ class Car extends Model
'car_model_id',
];
public function documents()
{
return $this->morphMany(Document::class, 'documentable');
}
public function getNameAttribute()
{
if (!$this->carModel) {

View File

@ -25,6 +25,11 @@ class Contact extends Model
'notes',
];
public function documents()
{
return $this->morphMany(Document::class, 'documentable');
}
public function getNameAttribute()
{
return implode(' ', array_filter([$this->lastname, $this->firstname]));

View File

@ -96,7 +96,7 @@ class Contract extends Model
public function documents()
{
return $this->hasMany(Document::class);
return $this->morphMany(Document::class, 'documentable');
}
public function payments()

View File

@ -15,9 +15,15 @@ class Document extends Model
'internal_name',
'extension',
'size',
'contract_id',
'documentable_type',
'documentable_id',
];
public function documentable()
{
return $this->morphTo();
}
public function getCreatedAtAttribute($created_at)
{
return Carbon::parse($created_at)->format('d.m.Y');
@ -43,16 +49,11 @@ class Document extends Model
public function getLinkAttribute()
{
return route('documents.show', [$this->contract->id, $this->id]);
return route('documents.show', $this->id);
}
public function getPathAttribute()
{
return public_path("documents/contracts/{$this->contract->id}/{$this->internal_name}");
}
public function contract()
{
return $this->belongsTo(Contract::class)->withTrashed();
return public_path("documents/{$this->documentable_type}/{$this->documentable->id}/{$this->internal_name}");
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Relations\Relation;
class MorphServiceProvider extends ServiceProvider
{
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
Relation::morphMap([
'contracts' => 'App\Models\Contract',
'cars' => 'App\Models\Car',
'contacts' => 'App\Models\Contact',
]);
}
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@ -41,15 +41,6 @@ class RouteServiceProvider extends ServiceProvider
$this->configureRateLimiting();
$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::bind('car', function ($value) {
if (in_array(Route::currentRouteName(), ['cars.show', 'cars.restore'])) {
return Car::withTrashed()->find($value);
@ -70,6 +61,15 @@ class RouteServiceProvider extends ServiceProvider
}
return Contract::find($value);
});
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'));
});
}

View File

@ -171,6 +171,7 @@ return [
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\MorphServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
@ -229,7 +230,7 @@ return [
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
'PDF' => Barryvdh\DomPDF\Facade::class,
'PDF' => Barryvdh\DomPDF\Facade::class,
],
];

View File

@ -26,7 +26,8 @@ class DocumentFactory extends Factory
return [
'name' => 'Vertrag.pdf',
'internal_name' => '2021-06-11-13:11:12.pdf',
'contract_id' => $this->faker->numberBetween(1, Contract::count()),
'documentable_id' => $this->faker->numberBetween(1, Contract::count()),
'documentable_type' => 'contracts',
'size' => $this->faker->numberBetween(1, 30000),
'extension' => 'pdf',
];

View File

@ -19,10 +19,7 @@ class CreateDocumentsTable extends Migration
$table->string('internal_name');
$table->integer('size');
$table->string('extension');
$table->foreignId('contract_id')
->onUpdate('cascade')
->onDelete('cascade')
->constrained('contracts');
$table->morphs('documentable');
$table->timestamps();
});
}

View File

@ -73,10 +73,6 @@ class DatabaseSeeder extends Seeder
$payments = Payment::factory()
->count(60)
->create();
$documents = Document::factory()
->count(40)
->create();
}
public function getBrands(): array

51
public/js/app.js vendored
View File

@ -18486,6 +18486,7 @@ var STATUS_FAILED = 2;
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({
props: {
id: Number,
documentable_type: String,
documents: Object
},
data: function data() {
@ -18517,7 +18518,8 @@ var STATUS_FAILED = 2;
// upload data to the server
this.currentStatus = STATUS_SAVING;
axios.post(this.route('documents.store', this.id), formData).then(function (response) {
console.log(this.route('documents.store'));
axios.post(this.route('documents.store'), formData).then(function (response) {
_this.documents.push(response.data);
_this.reset();
@ -18529,7 +18531,9 @@ var STATUS_FAILED = 2;
filesChange: function filesChange(fieldName, fileList) {
// handle file changes
var formData = new FormData();
if (!fileList.length) return; // append the files to FormData
if (!fileList.length) return;
formData.append('documentable_type', this.documentable_type);
formData.append('documentable_id', this.id); // append the files to FormData
Array.from(Array(fileList.length).keys()).map(function (x) {
formData.append(fieldName, fileList[x], fileList[x].name);
@ -18573,6 +18577,7 @@ __webpack_require__.r(__webpack_exports__);
props: {
initial_documents: Object,
id: Number,
documentable_type: String,
show_upload: Boolean
},
data: function data() {
@ -20737,6 +20742,8 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _Components_Buttons_RestoreButton_vue__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @/Components/Buttons/RestoreButton.vue */ "./resources/js/Components/Buttons/RestoreButton.vue");
/* harmony import */ var _Components_Contracts_ContractTable_vue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../Components/Contracts/ContractTable.vue */ "./resources/js/Components/Contracts/ContractTable.vue");
/* harmony import */ var _Components_SmallTitle_vue__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../Components/SmallTitle.vue */ "./resources/js/Components/SmallTitle.vue");
/* harmony import */ var _Components_Documents_View_vue__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! @/Components/Documents/View.vue */ "./resources/js/Components/Documents/View.vue");
@ -20754,7 +20761,8 @@ __webpack_require__.r(__webpack_exports__);
DeleteButton: _Components_Buttons_DeleteButton_vue__WEBPACK_IMPORTED_MODULE_4__.default,
RestoreButton: _Components_Buttons_RestoreButton_vue__WEBPACK_IMPORTED_MODULE_5__.default,
ContractTable: _Components_Contracts_ContractTable_vue__WEBPACK_IMPORTED_MODULE_6__.default,
SmallTitle: _Components_SmallTitle_vue__WEBPACK_IMPORTED_MODULE_7__.default
SmallTitle: _Components_SmallTitle_vue__WEBPACK_IMPORTED_MODULE_7__.default,
DocumentsView: _Components_Documents_View_vue__WEBPACK_IMPORTED_MODULE_8__.default
},
props: {
car: Object
@ -21276,6 +21284,8 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _Components_Buttons_RestoreButton_vue__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @/Components/Buttons/RestoreButton.vue */ "./resources/js/Components/Buttons/RestoreButton.vue");
/* harmony import */ var _Components_Contracts_ContractTable_vue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../Components/Contracts/ContractTable.vue */ "./resources/js/Components/Contracts/ContractTable.vue");
/* harmony import */ var _Components_SmallTitle_vue__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../Components/SmallTitle.vue */ "./resources/js/Components/SmallTitle.vue");
/* harmony import */ var _Components_Documents_View_vue__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! @/Components/Documents/View.vue */ "./resources/js/Components/Documents/View.vue");
@ -21293,7 +21303,8 @@ __webpack_require__.r(__webpack_exports__);
DeleteButton: _Components_Buttons_DeleteButton_vue__WEBPACK_IMPORTED_MODULE_4__.default,
RestoreButton: _Components_Buttons_RestoreButton_vue__WEBPACK_IMPORTED_MODULE_5__.default,
ContractTable: _Components_Contracts_ContractTable_vue__WEBPACK_IMPORTED_MODULE_6__.default,
SmallTitle: _Components_SmallTitle_vue__WEBPACK_IMPORTED_MODULE_7__.default
SmallTitle: _Components_SmallTitle_vue__WEBPACK_IMPORTED_MODULE_7__.default,
DocumentsView: _Components_Documents_View_vue__WEBPACK_IMPORTED_MODULE_8__.default
},
props: {
contact: Object
@ -23546,7 +23557,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
var _component_document_upload = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)("document-upload");
return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_small_title, {
return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)("div", null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_small_title, {
title: "Dokumente",
"class": "mb-3"
}), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)("div", _hoisted_1, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)($data.documents, function (document) {
@ -23562,12 +23573,11 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
)), $props.show_upload ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(_component_document_upload, {
key: 0,
id: $props.id,
documentable_type: $props.documentable_type,
documents: $data.documents
}, null, 8
/* PROPS */
, ["id", "documents"])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true)])], 64
/* STABLE_FRAGMENT */
);
, ["id", "documentable_type", "documents"])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true)])]);
}
/***/ }),
@ -28417,6 +28427,8 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
var _component_contract_table = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)("contract-table");
var _component_documents_view = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)("documents-view");
var _component_show_page = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)("show-page");
return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(_component_show_page, null, {
@ -28478,7 +28490,15 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
title: $props.car.sell_contracts.length > 1 ? $props.car.sell_contracts.length + ' Verkaufsverträge' : 'Verkaufsvertrag'
}, null, 8
/* PROPS */
, ["contracts", "carId", "show_upload", "title"])];
, ["contracts", "carId", "show_upload", "title"]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_documents_view, {
"class": "mt-5",
initial_documents: $props.car.documents,
id: $props.car.id,
documentable_type: "cars",
show_upload: !$props.car.deleted_at
}, null, 8
/* PROPS */
, ["initial_documents", "id", "show_upload"])];
}),
_: 1
/* STABLE */
@ -29307,6 +29327,8 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
var _component_contract_table = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)("contract-table");
var _component_documents_view = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)("documents-view");
var _component_show_page = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)("show-page");
return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(_component_show_page, null, {
@ -29368,7 +29390,15 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
title: $props.contact.sell_contracts.length > 1 ? $props.contact.sell_contracts.length + ' Verkaufsverträge' : 'Verkaufsvertrag'
}, null, 8
/* PROPS */
, ["contracts", "contactId", "show_upload", "title"])];
, ["contracts", "contactId", "show_upload", "title"]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_documents_view, {
"class": "mt-5",
initial_documents: $props.contact.documents,
id: $props.contact.id,
documentable_type: "contacts",
show_upload: !$props.contact.deleted_at
}, null, 8
/* PROPS */
, ["initial_documents", "id", "show_upload"])];
}),
_: 1
/* STABLE */
@ -30069,6 +30099,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
, ["payments", "contract"]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_documents_view, {
initial_documents: $props.contract.documents,
id: $props.contract.id,
documentable_type: "contracts",
show_upload: !$props.contract.deleted_at
}, null, 8
/* PROPS */

View File

@ -24,6 +24,7 @@ const STATUS_INITIAL = 0; const STATUS_SAVING = 1; const
export default {
props: {
id: Number,
documentable_type: String,
documents: Object,
},
data() {
@ -53,7 +54,8 @@ export default {
save(formData) {
// upload data to the server
this.currentStatus = STATUS_SAVING;
axios.post(this.route('documents.store', this.id), formData)
console.log(this.route('documents.store'));
axios.post(this.route('documents.store'), formData)
.then((response) => {
this.documents.push(response.data);
this.reset();
@ -69,6 +71,9 @@ export default {
if (!fileList.length) return;
formData.append('documentable_type', this.documentable_type);
formData.append('documentable_id', this.id);
// append the files to FormData
Array
.from(Array(fileList.length).keys())

View File

@ -1,11 +1,13 @@
<template>
<small-title title="Dokumente" class="mb-3" />
<div class="grid md:grid-cols-6 sm:grid-cols-4 grid-cols-2 gap-3">
<template v-for="document in documents" :key="document.id">
<document-item @delete="deleteDocument" :document="document" />
</template>
<document-upload v-if="show_upload" :id="id" :documents="documents" />
</div>
<div>
<small-title title="Dokumente" class="mb-3" />
<div class="grid md:grid-cols-6 sm:grid-cols-4 grid-cols-2 gap-3">
<template v-for="document in documents" :key="document.id">
<document-item @delete="deleteDocument" :document="document" />
</template>
<document-upload v-if="show_upload" :id="id" :documentable_type="documentable_type" :documents="documents" />
</div>
</div>
</template>
<script>
@ -23,6 +25,7 @@ export default {
props: {
initial_documents: Object,
id: Number,
documentable_type: String,
show_upload: Boolean,
},
data() {

View File

@ -34,6 +34,7 @@
:show_upload="!car.deleted_at"
:title="car.sell_contracts.length > 1 ? car.sell_contracts.length + ' Verkaufsverträge' : 'Verkaufsvertrag'"
/>
<documents-view class="mt-5" :initial_documents="car.documents" :id="car.id" documentable_type="cars" :show_upload="!car.deleted_at" />
</template>
</show-page>
</template>
@ -47,6 +48,7 @@ import DeleteButton from '@/Components/Buttons/DeleteButton.vue';
import RestoreButton from '@/Components/Buttons/RestoreButton.vue';
import ContractTable from '../../Components/Contracts/ContractTable.vue';
import SmallTitle from '../../Components/SmallTitle.vue';
import DocumentsView from '@/Components/Documents/View.vue';
export default {
components: {
@ -58,6 +60,7 @@ export default {
RestoreButton,
ContractTable,
SmallTitle,
DocumentsView,
},
props: {
car: Object,

View File

@ -37,6 +37,7 @@
:show_upload="!contact.deleted_at"
:title="contact.sell_contracts.length > 1 ? contact.sell_contracts.length + ' Verkaufsverträge' : 'Verkaufsvertrag'"
/>
<documents-view class="mt-5" :initial_documents="contact.documents" :id="contact.id" documentable_type="contacts" :show_upload="!contact.deleted_at" />
</template>
</show-page>
</template>
@ -50,6 +51,7 @@ import DeleteButton from '@/Components/Buttons/DeleteButton.vue';
import RestoreButton from '@/Components/Buttons/RestoreButton.vue';
import ContractTable from '../../Components/Contracts/ContractTable.vue';
import SmallTitle from '../../Components/SmallTitle.vue';
import DocumentsView from '@/Components/Documents/View.vue';
export default {
components: {
@ -61,6 +63,7 @@ export default {
RestoreButton,
ContractTable,
SmallTitle,
DocumentsView,
},
props: {

View File

@ -81,7 +81,7 @@
<payments-upload v-if="!contract.deleted_at" :show_upload="!contract.deleted_at" :contract="contract" />
</span>
<payments-view :payments="contract.payments" :contract="contract" />
<documents-view :initial_documents="contract.documents" :id="contract.id" :show_upload="!contract.deleted_at" />
<documents-view :initial_documents="contract.documents" :id="contract.id" documentable_type="contracts" :show_upload="!contract.deleted_at" />
</template>
</show-page>
</template>

View File

@ -9,7 +9,6 @@ use App\Http\Controllers\PaymentController;
use App\Http\Controllers\ContractController;
use App\Http\Controllers\DocumentController;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::middleware(['auth:sanctum', 'verified'])->group(function () {
Route::get('/', [ContractController::class, 'dashboard'])->name('dashboard');
@ -75,13 +74,13 @@ Route::middleware(['auth:sanctum', 'verified'])->group(function () {
Route::post('/', [PaymentController::class, 'store'])->name('payments.store');
});
Route::prefix('documents')->group(function () {
Route::get('{document}', [DocumentController::class, 'show'])->name('documents.show');
Route::delete('delete', [DocumentController::class, 'destroy'])->name('documents.destroy');
Route::post('/', [DocumentController::class, 'store'])->name('documents.store');
});
});
});
Route::prefix('documents')->group(function () {
Route::get('{document}', [DocumentController::class, 'show'])->name('documents.show');
Route::delete('delete', [DocumentController::class, 'destroy'])->name('documents.destroy');
Route::post('upload', [DocumentController::class, 'store'])->name('documents.store');
});
Route::post('brands', [BrandController::class, 'store'])->name('brands.store');