add documents functionality

shift-build-2464
Nadim Salloum 2021-06-11 22:50:02 +03:00
parent f54f3c6ca5
commit f536996605
16 changed files with 3135 additions and 1517 deletions

2
.gitignore vendored
View File

@ -11,3 +11,5 @@ Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
.DS_Store
public/documents/*

View File

@ -1,19 +0,0 @@
<?php
namespace App\Enums;
use BenSampo\Enum\Enum;
/**
* @method static static BuyContractUnsigned()
* @method static static BuyContractSigned()
* @method static static SellContractUnSigned()
* @method static static SellContractSigned()
* @method static static Other()
*/
final class DocumentType extends Enum
{
const ContractUnsigned = 0;
const ContractSigned = 1;
const Other = 3;
}

View File

@ -190,6 +190,17 @@ class ContractController extends Controller
'price' => $contract->price->format(),
'type' => $contract->type,
'is_sell_contract' => $contract->isSellContract(),
'documents' => $contract->documents()->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,
];
}),
'insurance_type' => $contract->insurance_type ? InsuranceType::fromValue((int)$contract->insurance_type)->key : null,
'deleted_at' => $contract->deleted_at,
'contact' => [

View File

@ -2,9 +2,48 @@
namespace App\Http\Controllers;
use App\Models\Contract;
use App\Models\Document;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Redirect;
class DocumentController extends Controller
{
//
public function show(Document $document)
{
header('Content-Disposition: filename="' . $document->name . '"');
return response()->file($document->path);
}
public function store(Request $request, Contract $contract)
{
$file = $request->file()['document'];
$internalName = date('Y-m-d-H-i-s') . '.' . $file->extension();
$document = Document::create([
'name' => $file->getClientOriginalName(),
'internal_name' => $internalName,
'size' => $file->getSize(),
'extension' => $file->extension(),
'contract_id' => $contract->id,
]);
$file->move(public_path("documents/contracts/{$contract->id}/"), $internalName);
return [
'id' => $document->id,
'name' => $document->name,
'size' => $document->size,
'extension' => $document->extension,
'link' => $document->link,
'created_at' => $document->created_at,
];
}
public function destroy(Document $document)
{
unlink($document->path);
$document->delete();
session()->flash('flash.banner', 'Dokument gelöscht.');
return Redirect::back();
}
}

View File

@ -90,11 +90,6 @@ class Car extends Model
return $this->latestSellContract()->price->subtract($this->latestBuyContract()->price)->format();
}
public function documents()
{
return $this->morphMany(Document::class, 'documentable');
}
public function contracts()
{
return $this->hasMany(Contract::class);

View File

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

View File

@ -2,8 +2,9 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Document extends Model
{
@ -11,12 +12,42 @@ class Document extends Model
protected $fillable = [
'name',
'document_type',
'car_id',
'internal_name',
'extension',
'size',
'contract_id',
];
public function documentable()
public function getCreatedAtAttribute($created_at)
{
return $this->morphTo();
return Carbon::parse($created_at)->format('d.m.Y');
}
public function getExtensionAttribute($extension)
{
return strtoupper($extension);
}
public function getSizeAttribute($size)
{
if ($size / 1024 / 1024 < 1) {
return (string)floor($size / 1024) . " KB";
}
return (string)floor($size / 1024 / 1024) . " MB";
}
public function getLinkAttribute()
{
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();
}
}

View File

@ -5,7 +5,6 @@ namespace Database\Factories;
use App\Models\Document;
use App\Models\Contract;
use Illuminate\Database\Eloquent\Factories\Factory;
use App\Enums\DocumentType;
class DocumentFactory extends Factory
@ -25,10 +24,11 @@ class DocumentFactory extends Factory
public function definition()
{
return [
'name' => $this->faker->name(),
'documentable_id' => $this->faker->numberBetween(1, Contract::count()),
'documentable_type' => 'App\Models\Contract',
'type' => (string)DocumentType::getRandomValue(),
'name' => 'Vertrag.pdf',
'internal_name' => '2021-06-11-13:11:12.pdf',
'contract_id' => $this->faker->numberBetween(1, Contract::count()),
'size' => $this->faker->numberBetween(1, 30000),
'extension' => 'pdf',
];
}
}

View File

@ -3,7 +3,6 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Enums\DocumentType;
class CreateDocumentsTable extends Migration
{
@ -17,10 +16,13 @@ class CreateDocumentsTable extends Migration
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->enum('type', DocumentType::getValues())
->default(DocumentType::Other);
$table->integer('documentable_id');
$table->string('documentable_type');
$table->string('internal_name');
$table->integer('size');
$table->string('extension');
$table->foreignId('contract_id')
->onUpdate('cascade')
->onDelete('cascade')
->constrained('contracts');
$table->timestamps();
});
}

4265
public/js/app.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
<template>
<a target="_blank" :href="document.link" class="p-3 col-span-2 grid relative grid-flow-rows cursor-pointer group auto-rows-max hover:bg-white bg-gray-50 transition shadow rounded-md font-medium">
<inertia-link :href="route('documents.destroy', document.id)" class="absolute right-0 opacity-0 group-hover:opacity-80 transition">
<unicon fill="red" class="p-2" height="40" width="40" name="trash-alt"></unicon>
</inertia-link>
<span class="justify-center inline-flex items-center mx-auto">
<unicon fill="royalblue" class="p-2" height="70" width="70" name="file-alt"></unicon>
<span class="text-blue-800 text-xl font-bold">{{ document.extension }}</span>
</span>
<span class="overflow-ellipsis overflow-hidden"> {{ document.name }}</span>
<span class="text-xs text-grey">{{ document.created_at }}, {{ document.size }}</span>
</a>
</template>
<script>
export default {
components: {
},
props: {
document: Object,
},
data() {
return {
}
},
}
</script>

View File

@ -0,0 +1,86 @@
<template>
<span v-if="!isFailed" class="col-span-2 grid grid-flow-rows relative cursor-pointer auto-rows-max py-3 inline-flex items-center px-4 bg-gray-100 border-dashed border-4 font-semibold justify-center text-md text-gray-500 uppercase tracking-widest hover:bg-gray-200 focus:outline-none focus:border-gray-500 focus:ring focus:ring-gray-300 disabled:opacity-25 transition" >
<input type="file" :name="uploadFieldName" :disabled="isSaving" @change="filesChange($event.target.name, $event.target.files);" class="opacity-0 absolute top-0 left-0 w-full h-full cursor-pointer">
<unicon fill="grey" class="p-2 my-5 mx-auto" height="45%" width="45%" name="file-upload-alt"></unicon>
<span v-if="isInitial" class="text-center">Dokument hochladen</span>
<span v-if="isSaving">Lade Dokument hoch...</span> </span>
<span v-else class="col-span-2">
<!--FAILED-->
<span v-if="isFailed" class="col-span-2 grid grid-flow-rows relative auto-rows-max py-3 inline-flex items-center px-4 bg-gray-100 border-dashed border-4 font-semibold justify-center text-md text-gray-500 uppercase tracking-widest hover:bg-gray-200 focus:outline-none focus:border-gray-500 focus:ring focus:ring-gray-300 disabled:opacity-25 transition" >
<p>Upload fehlgeschlagen.</p>
<a href="javascript:void(0)" @click="reset()">Erneut versuchen</a>
</span>
<span v-if="isFailed">
<pre>{{ uploadError }}</pre>
</span>
</span>
</template>
<script>
const STATUS_INITIAL = 0, STATUS_SAVING = 1, STATUS_FAILED = 2;
export default {
props: {
contract: Object,
documents: Object,
},
data() {
return {
uploadError: null,
currentStatus: null,
uploadFieldName: 'document',
}
},
computed: {
isInitial() {
return this.currentStatus === STATUS_INITIAL;
},
isSaving() {
return this.currentStatus === STATUS_SAVING;
},
isFailed() {
return this.currentStatus === STATUS_FAILED;
},
},
methods: {
reset() {
// reset form to initial state
this.currentStatus = STATUS_INITIAL;
this.uploadError = null;
},
save(formData) {
// upload data to the server
this.currentStatus = STATUS_SAVING;
axios.post(this.route('documents.store', this.contract.id), formData)
.then(response => {
this.documents.push(response.data);
this.reset();
})
.catch(err => {
this.uploadError = err.response;
this.currentStatus = STATUS_FAILED;
});
},
filesChange(fieldName, fileList) {
// handle file changes
const formData = new FormData();
if (!fileList.length) return;
// append the files to FormData
Array
.from(Array(fileList.length).keys())
.map(x => {
formData.append(fieldName, fileList[x], fileList[x].name);
});
// save it
this.save(formData);
},
},
mounted() {
this.reset();
},
}
</script>

View File

@ -42,6 +42,15 @@
</div>
</template>
<template #more>
<div class="col-span-12">
<h3 class="mb-3">Dokumente</h3>
<div class="w-full grid grid-cols-12 xs:grid-cols-4 gap-3">
<template v-for="document in documents" :key="document.id">
<document-item :document="document" />
</template>
<document-upload :contract="contract" :documents="documents" />
</div>
</div>
<div class="col-span-6 xs:col-span-12">
<h3 class="mb-3">Auto</h3>
<car-card :car="contract.car" />
@ -63,7 +72,9 @@ import RestoreButton from '@/Components/Buttons/RestoreButton.vue'
import CarCard from '@/Components/CarCard.vue'
import PrintButton from '@/Components/Buttons/PrintButton.vue'
import ContactCard from '@/Components/ContactCard.vue'
import EditButton from '../../Components/Buttons/EditButton.vue'
import EditButton from '@/Components/Buttons/EditButton.vue'
import DocumentItem from '@/Components/Documents/Item.vue'
import DocumentUpload from '@/Components/Documents/Upload.vue'
export default {
components: {
@ -77,6 +88,8 @@ export default {
ContactCard,
EditButton,
CarCard,
DocumentItem,
DocumentUpload,
},
props: {
contract: Object,
@ -92,6 +105,7 @@ export default {
data() {
return {
currentRoute: 'contracts.show',
documents: this.contract.documents,
buyContractsColumns: [
{key: 'contact', value: 'Verkäufer'},
{key: 'date', value: 'Kaufdatum'},

4
resources/js/app.js vendored
View File

@ -6,9 +6,9 @@ import { App as InertiaApp, plugin as InertiaPlugin } from '@inertiajs/inertia-v
import { InertiaProgress } from '@inertiajs/progress';
import Unicon from 'vue-unicons';
import { createStore } from 'vuex'
import { uniPalette, uniCalendarAlt, uniPlusCircle, uniMeh, uniUsersAlt, uniCarSideview, uniDashboard, uniSearch, uniFilter, uniFilterSlash, uniTrashAlt, uniPen, uniExclamationTriangle, uniMapMarker, uniPhone, uniEnvelope, uniFileDownload, uniArrowDown, uniArrowUp, uniArrowRight, uniAngleRight, uniAngleUp, uniAngleDown, uniAngleLeft, uniFileUploadAlt } from 'vue-unicons/dist/icons'
import { uniFileAlt, uniPalette, uniCalendarAlt, uniPlusCircle, uniMeh, uniUsersAlt, uniCarSideview, uniDashboard, uniSearch, uniFilter, uniFilterSlash, uniTrashAlt, uniPen, uniExclamationTriangle, uniMapMarker, uniPhone, uniEnvelope, uniFileDownload, uniArrowDown, uniArrowUp, uniArrowRight, uniAngleRight, uniAngleUp, uniAngleDown, uniAngleLeft, uniFileUploadAlt } from 'vue-unicons/dist/icons'
Unicon.add([uniPalette, uniCalendarAlt, uniPlusCircle, uniMeh, uniUsersAlt, uniCarSideview, uniDashboard, uniSearch, uniFilter, uniFilterSlash, uniTrashAlt, uniPen, uniExclamationTriangle, uniMapMarker, uniPhone, uniEnvelope, uniFileDownload, uniArrowDown, uniArrowUp, uniArrowRight, uniAngleRight, uniAngleUp, uniAngleDown, uniAngleLeft, uniFileUploadAlt])
Unicon.add([uniFileAlt, uniPalette, uniCalendarAlt, uniPlusCircle, uniMeh, uniUsersAlt, uniCarSideview, uniDashboard, uniSearch, uniFilter, uniFilterSlash, uniTrashAlt, uniPen, uniExclamationTriangle, uniMapMarker, uniPhone, uniEnvelope, uniFileDownload, uniArrowDown, uniArrowUp, uniArrowRight, uniAngleRight, uniAngleUp, uniAngleDown, uniAngleLeft, uniFileUploadAlt])
// Create a new store instance.
const store = createStore({

View File

@ -6,6 +6,7 @@ use App\Http\Controllers\CarController;
use App\Http\Controllers\BrandController;
use App\Http\Controllers\CarModelController;
use App\Http\Controllers\ContractController;
use App\Http\Controllers\DocumentController;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
@ -171,3 +172,15 @@ Route::get('contracts/{contract}/delete', [ContractController::class, 'destroy']
Route::get('contracts/{contract}/restore', [ContractController::class, 'restore'])
->name('contracts.restore')
->middleware(['auth:sanctum', 'verified']);
Route::post('documents/{contract}', [DocumentController::class, 'store'])
->name('documents.store')
->middleware(['auth:sanctum', 'verified']);
Route::get('documents/{document}/delete', [DocumentController::class, 'destroy'])
->name('documents.destroy')
->middleware(['auth:sanctum', 'verified']);
Route::get('documents/{document}', [DocumentController::class, 'show'])
->name('documents.show')
->middleware(['auth:sanctum', 'verified']);