1
0

10 Sitoutukset 591c7904e6 ... dfcefec10f

Tekijä SHA1 Viesti Päivämäärä
  Christopher Leggett dfcefec10f Implements circle-spinner for work order edit. 5 vuotta sitten
  Christopher Leggett 86eccf803d Readded Error handling to SPA. 5 vuotta sitten
  Christopher Leggett b172c8c191 Fixes live updating of work order notes. 5 vuotta sitten
  Christopher Leggett bd5cb7ddb2 Adapts Work Order Notes for SPA. 5 vuotta sitten
  Christopher Leggett f3d0308d7d Swaps out props arrays for props objects. 5 vuotta sitten
  Christopher Leggett b49bfa2ccb Adapts credentials for SPA. 5 vuotta sitten
  Christopher Leggett 341a7b8e4b Implements CircleSpinner while dashboard loads. 5 vuotta sitten
  Christopher Leggett 28f3e947c8 Adapts Asset Info for SPA and improves CircleSpinner. 5 vuotta sitten
  Christopher Leggett 17714ea340 Makes improvements to render of WOInfo component. 5 vuotta sitten
  Christopher Leggett fc4e12ceb6 Fixes navigation to dashboard after login. 5 vuotta sitten

+ 8 - 0
app/Asset.php

@@ -12,6 +12,14 @@ class Asset extends Model
     const CREATED_AT = 'created_date';
     const UPDATED_AT = 'modified_date';
 
+    public function getPcextraAttribute($value) {
+        return unserialize($value);
+    }
+
+    public function setPcextraAttribute($value) {
+        $this->attributes['pcextra'] = serialize($value);
+    }
+
     public function workOrders()
     {
         return $this->hasMany('App\WorkOrder');

+ 0 - 2
app/Events/AssetUpdated.php

@@ -16,7 +16,6 @@ class AssetUpdated implements ShouldBroadcast
     use Dispatchable, InteractsWithSockets, SerializesModels;
 
     public $data;
-    public $pcextra;
 
     /**
      * Create a new event instance.
@@ -26,7 +25,6 @@ class AssetUpdated implements ShouldBroadcast
     public function __construct($asset)
     {
         $this->data = $asset;
-        $this->pcextra = json_encode(unserialize($asset->pcextra));
     }
 
     /**

+ 1 - 1
app/Events/WorkOrderNoteAdded.php

@@ -34,6 +34,6 @@ class WorkOrderNoteAdded implements ShouldBroadcast
      */
     public function broadcastOn()
     {
-        return new Channel('wonotes.'.$this->note->notetype.'.'.$this->note->woid);
+        return new Channel('wonotes.'.$this->note->woid);
     }
 }

+ 39 - 0
app/Events/WorkOrderNoteUpdated.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Events;
+
+use Illuminate\Broadcasting\Channel;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Broadcasting\PresenceChannel;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+use App\WorkOrderNote;
+
+class WorkOrderNoteUpdated implements ShouldBroadcast
+{
+    use Dispatchable, InteractsWithSockets, SerializesModels;
+
+    public $note;
+
+    /**
+     * Create a new event instance.
+     *
+     * @return void
+     */
+    public function __construct(WorkOrderNote $workOrderNote)
+    {
+        $this->note = $workOrderNote;
+    }
+
+    /**
+     * Get the channels the event should broadcast on.
+     *
+     * @return \Illuminate\Broadcasting\Channel|array
+     */
+    public function broadcastOn()
+    {
+        return new Channel('wonotes.'.$this->note->woid);
+    }
+}

+ 6 - 1
app/Http/Controllers/Api/AssetsController.php

@@ -40,6 +40,11 @@ class AssetsController extends Controller
         //
     }
 
+    public function credentials(Asset $asset) {
+        $credentials = $asset->credentials->sortByDesc('creddate')->values();
+        return response()->json($credentials, 200);
+    }
+
     /**
      * Update the specified resource in storage.
      *
@@ -52,7 +57,7 @@ class AssetsController extends Controller
         $asset->pcmanu = $request->input('pcmanu');
         $asset->pcmake = $request->input('pcmake');
         $asset->pcnickname = $request->input('pcnickname');
-        $asset->pcextra = serialize($request->input('pcextra'));
+        $asset->pcextra = $request->input('pcextra');
         $asset->save();
         event(new \App\Events\AssetUpdated($asset));
         return response()->json($asset, 200);

+ 1 - 0
app/Http/Controllers/Api/WorkOrderNotesController.php

@@ -58,6 +58,7 @@ class WorkOrderNotesController extends Controller
     {
         $workOrderNote->thenote = $request->input('thenote');
         $workOrderNote->save();
+        event(new \App\Events\WorkOrderNoteUpdated($workOrderNote));
         return response()->json($workOrderNote, 200);
     }
 

+ 4 - 0
app/Http/Controllers/Api/WorkOrdersController.php

@@ -55,6 +55,10 @@ class WorkOrdersController extends Controller
         return response()->json($workOrder->asset);
     }
 
+    public function notes(WorkOrder $workOrder) {
+        return response()->json($workOrder->notes->values(), 200);
+    }
+
     /**
      * Update the specified resource in storage.
      *

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 649 - 215
public/js/app.js


+ 91 - 0
resources/js/components/AssetInfoEditModal.vue

@@ -0,0 +1,91 @@
+<template>
+    <modal :id="modalId" tabindex="-1" role="dialog" :aria-labelledby="`${modalId}Label`">
+        <h5 slot="header" class="modal-title" :id="`${modalId}Label`">
+            Edit Work Order Information
+        </h5>
+        <div slot="body">
+            <div class="form-group">
+                <label for="manufacturer">Manufacturer</label>
+                <input type="text" name="manufacturer" id="manufacturer" class="form-control" v-model="asset.pcmanu">
+            </div>
+            <div class="form-group">
+                <label for="make">Make</label>
+                <input type="text" name="make" id="make" class="form-control" v-model="asset.pcmake">
+            </div>
+            <div class="form-group">
+                <label for="nickname">Nickname</label>
+                <input type="text" name="nickname" id="nickname" class="form-control" v-model="asset.pcnickname">
+            </div>
+            <div class="form-group">
+                <label for="os">OS</label>
+                <input type="text" name="os" id="os" class="form-control" v-model="asset.pcextra[2]">
+            </div>
+            <div class="form-group">
+                <label for="serial">Serial Number</label>
+                <input type="text" id="serial" name="serial" class="form-control" v-model="asset.pcextra[104]">
+            </div>
+            <div class="form-group">
+                <label for="cpu">CPU</label>
+                <input type="text" id="cpu" name="cpu" class="form-control" v-model="asset.pcextra[101]">
+            </div>
+            <div class="form-group">
+                <label for="ram">RAM</label>
+                <input type="text" name="ram" id="ram" class="form-control" v-model="asset.pcextra[100]">
+            </div>
+            <div class="form-group">
+                <label for="graphics">Graphics</label>
+                <input type="text" name="graphics" id="grapics" class="form-control" v-model="asset.pcextra[4]">
+            </div>
+        </div>
+
+        <div slot="footer">
+            <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+            <button type="button" class="btn btn-primary" @click="updateAsset()">
+                <div v-if="!assetSaving">Save</div>
+                <circle-spinner v-else :size="2"></circle-spinner>
+            </button>
+        </div>
+    </modal>
+</template>
+<script>
+import Modal from '../components/Modal.vue'
+import CircleSpinner from '../components/CircleSpinner.vue'
+export default {
+    components: {
+        Modal,
+        CircleSpinner,
+    },
+    props: {
+        populateWith: {
+            type: Object,
+            required: true,
+        },
+        modalId: {
+            type: String,
+            required: true,
+        },
+    },
+    data () {
+        return {
+            // Essentially makes a copy of the object. Otherwise
+            // We would have a reference to the exising object causing
+            // unwanted mutations in the parent component.
+            asset: JSON.parse(JSON.stringify(this.populateWith)),
+            assetSaving: false,
+        }
+    },
+    methods: {
+        updateAsset() {
+            this.assetSaving = true
+            axios.put(`/api/assets/${this.asset.pcid}`, this.asset)
+                    .then((response) => {
+                        this.assetSaving = false
+                        $(`#${this.modalId}`).modal('hide');
+                    })
+                    .catch((error) => {
+                        this.assetSaving = false
+                    });
+        }
+    }
+}
+</script>

+ 63 - 0
resources/js/components/CircleSpinner.vue

@@ -0,0 +1,63 @@
+<template>
+  <div class="loader" :style="{fontSize: size + 'px', borderLeft: '1.1em solid ' + color}">Loading...</div>
+</template>
+
+<script>
+  export default {
+    name: 'IntersectingCirclesSpinner',
+
+    props: {
+      color: {
+        type: String,
+        default: '#fff'
+      },
+      size: {
+        type: Number,
+        default: 10
+      }
+    },
+  }
+</script>
+
+<style scoped>
+  .loader,
+.loader:after {
+  border-radius: 50%;
+  width: 10em;
+  height: 10em;
+}
+.loader {
+  margin: 0px auto;
+  position: relative;
+  text-indent: -9999em;
+  border-top: 1.1em solid rgba(255, 255, 255, 0.2);
+  border-right: 1.1em solid rgba(255, 255, 255, 0.2);
+  border-bottom: 1.1em solid rgba(255, 255, 255, 0.2);
+  -webkit-transform: translateZ(0);
+  -ms-transform: translateZ(0);
+  transform: translateZ(0);
+  -webkit-animation: load8 1.1s infinite linear;
+  animation: load8 1.1s infinite linear;
+  overflow: hidden;
+}
+@-webkit-keyframes load8 {
+  0% {
+    -webkit-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@keyframes load8 {
+  0% {
+    -webkit-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+</style>

+ 6 - 0
resources/js/components/credential-form-modal.vue → resources/js/components/CredentialFormModal.vue

@@ -25,7 +25,13 @@
     </modal>
 </template>
 <script>
+import Modal from '../components/Modal.vue'
+import AutocompleteCustomDropdown from '../components/AutocompleteCustomDropdown.vue'
 export default {
+    components: {
+        AutocompleteCustomDropdown,
+        Modal,
+    },
     props: {
         populateWith: {
             type: Object,

+ 7 - 6
resources/js/components/credential-list.vue → resources/js/components/CredentialList.vue

@@ -8,9 +8,15 @@
     </div>
 </template>
 <script>
+import Credential from '../components/Credential.vue'
+import CredentialFormModal from '../components/CredentialFormModal.vue'
 export default {
+    components: {
+        Credential,
+        CredentialFormModal,
+    },
     props: {
-        credentialList: {
+        credentials: {
             type: Array,
             required: true
         },
@@ -25,11 +31,6 @@ export default {
             type: Number
         }
     },
-    data () {
-        return {
-            credentials: this.credentialList
-        }
-    },
     computed : {
         creddescList: function () {
             let list = {}

+ 5 - 1
resources/js/components/note-form-modal.vue → resources/js/components/NoteFormModal.vue

@@ -16,7 +16,11 @@
             </modal>
 </template>
 <script>
+import Modal from '../components/Modal.vue'
 export default {
+    components: {
+        Modal,
+    },
     props: {
         populateWith: {
             type: Object,
@@ -45,7 +49,7 @@ export default {
         updateNote (note) {
             axios.put('/api/workorders/notes/' + note.noteid, note)
                 .then((response) => {
-                    hideModal()
+                    this.hideModal()
                 })
         },
         hideModal () {

+ 53 - 20
resources/js/components/WoInfoEditModal.vue

@@ -7,12 +7,12 @@
         <div class="form-group">
             <label for="probdesc">Problem Description</label>
             <textarea id="probdesc" name="probdesc" :class="{'form-control': true, 'is-invalid': errors.probdesc}" v-model="data.probdesc"></textarea>
-            <errorlist :errors="errors.probdesc"></errorlist>
+            <error-list :errors="errors.probdesc"></error-list>
         </div>
         <div class="form-group">
             <label for="suggested">Suggested Solution</label>
             <textarea id="suggested" name="suggested" :class="{'form-control': true, 'is-invalid': errors.suggested}" v-model="data.suggested"></textarea>
-            <errorlist :errors="errors.suggested"></errorlist>
+            <error-list :errors="errors.suggested"></error-list>
         </div>
         <div class="form-group">
             <label for="storelist">Store</label>
@@ -27,46 +27,79 @@
 
     <div slot="footer">
         <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
-        <button type="button" class="btn btn-primary" @click="updateWorkOrder()">Save</button>
+        <button type="button" class="btn btn-primary" @click="updateWorkOrder()">
+            <div v-if="!woInfoSaving">Save</div>
+            <circle-spinner v-else :size="2"></circle-spinner>
+        </button>
     </div>
 </modal>
 </template>
 <script>
-import Modal from '../components/Modal'
-import AutocompleteCustomDropdown from '../components/AutocompleteCustomDropdown'
+import Modal from '../components/Modal.vue'
+import AutocompleteCustomDropdown from '../components/AutocompleteCustomDropdown.vue'
+import ErrorList from '../components/ErrorList.vue'
+import CircleSpinner from '../components/CircleSpinner.vue'
 export default {
     components: {
         Modal,
-        AutocompleteCustomDropdown
+        AutocompleteCustomDropdown,
+        ErrorList,
+        CircleSpinner,
     },
-    props:['populateWith', 'storeList', 'modalId'],
-    data() {
+    props: {
+        populateWith: {
+            type: Object,
+            required: true,
+        },
+        storeList: {
+            type: Object,
+            required: true,
+        },
+        modalId: {
+            type: String,
+            required: true,
+        },
+    },
+    data () {
         return {
             // Copies object from prop, so it doesn't mutate the object
             // from the parent component. Parent component will be updated via
             // Websocket if updateWorkOrder is successful.
-            data: {},
+            data: JSON.parse(JSON.stringify(this.populateWith)),
             id: this.modalId,
             store: {},
-            errors: []
+            errors: [],
+            woInfoSaving: false
         }
     },
     methods: {
         updateWorkOrder() {
+            this.woInfoSaving = true
             this.data.storeid = this.store.id
             axios.put('/api/workorders/' + this.data.woid, this.data)
-                .then((response) => { $('#workordereditModal').modal('hide'); })
-                .catch((error) => { this.errors = JSON.parse(error.response.request.response).errors; });
+                .then((response) => {
+                    this.woInfoSaving = false
+                    $('#workordereditModal').modal('hide') 
+                })
+                .catch((error) => {
+                    this.woInfoSaving = false
+                    this.errors = JSON.parse(error.response.request.response).errors
+                });
+        }
+    },
+    mounted () {
+        this.store = {
+            'id': this.data.storeid,
+            'name': this.storeList[this.data.storeid]
         }
     },
-    // The props are obtained in parent components via axios,
-    // so the values may change after the initial render, these
-    // Watchers fill in our initial values when the values of the
-    // props change. As a side effect if they are updated while we
-    // are editing they will change right underneath us, not sure
-    // probably will implement a notice when they change during edit
-    // with the option to view changes or override them or something
-    // that.
+    // This was originally here because the populateWith info would originally
+    // not necessarily be loaded when this component was mounted. That is no
+    // longer the case as I now don't render the parent component until the
+    // ajax request is finised. The watch statements are still here to cover
+    // the case where someone else updates the populateWith values while
+    // someone is editing, although it probably should be edited so it doesn't
+    // immediately overwrite the unsaved edits of the current user.
     watch: {
         populateWith: function (value) {
             this.data = JSON.parse(JSON.stringify(this.populateWith))

+ 19 - 69
resources/js/components/assetinfo.vue

@@ -1,83 +1,33 @@
 <template>
     <div>
-        <modal id="asseteditModal" tabindex="-1" role="dialog" aria-labelledby="asseteditModalLabel">
-            <h5 slot="header" class="modal-title" id="asseteditModalLabel">
-                Edit Work Order Information
-            </h5>
-            <div slot="body">
-                <div class="form-group">
-                    <label for="manufacturer">Manufacturer</label>
-                    <input type="text" name="manufacturer" id="manufacturer" class="form-control" v-model.lazy="data.pcmanu">
-                </div>
-                <div class="form-group">
-                    <label for="make">Make</label>
-                    <input type="text" name="make" id="make" class="form-control" v-model.lazy="data.pcmake">
-                </div>
-                <div class="form-group">
-                    <label for="nickname">Nickname</label>
-                    <input type="text" name="nickname" id="nickname" class="form-control" v-model.lazy="data.pcnickname">
-                </div>
-                <div class="form-group">
-                    <label for="os">OS</label>
-                    <input type="text" name="os" id="os" class="form-control" v-model.lazy="pcextra[2]">
-                </div>
-                <div class="form-group">
-                    <label for="serial">Serial Number</label>
-                    <input type="text" id="serial" name="serial" class="form-control" v-model.lazy="pcextra[104]">
-                </div>
-                <div class="form-group">
-                    <label for="cpu">CPU</label>
-                    <input type="text" id="cpu" name="cpu" class="form-control" v-model.lazy="pcextra[101]">
-                </div>
-                <div class="form-group">
-                    <label for="ram">RAM</label>
-                    <input type="text" name="ram" id="ram" class="form-control" v-model.lazy="pcextra[100]">
-                </div>
-                <div class="form-group">
-                    <label for="graphics">Graphics</label>
-                    <input type="text" name="graphics" id="grapics" class="form-control" v-model.lazy="pcextra[4]">
-                </div>
-            </div>
+        <asset-info-edit-modal :modal-id="`asset${asset.pcid}editModal`" :populate-with="asset"></asset-info-edit-modal>
+        <h4 class="text-center">{{ this.asset.pcmanu }} {{ this.asset.pcmake }} - <small class="text-muted">{{this.asset.pcnickname}}</small></h4>
+        <p>OS: {{this.asset.pcextra[2]}}</p>
+        <p>S/N: {{ this.asset.pcextra[104] }}</p>
+        <p>CPU: {{ this.asset.pcextra[101]}}</p>
+        <p>RAM: {{ this.asset.pcextra[100] }}</p>
+        <p>Graphics: {{this.asset.pcextra[4]}}</p>
 
-            <div slot="footer">
-                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
-                <button type="button" class="btn btn-primary" @click="updateAsset()">Save</button>
-            </div>
-        </modal>
-        <h4 class="text-center">{{ this.data.pcmanu }} {{ this.data.pcmake }} - <small class="text-muted">{{this.data.pcnickname}}</small></h4>
-        <p>OS: {{this.pcextra[2]}}</p>
-        <p>S/N: {{ this.pcextra[104] }}</p>
-        <p>CPU: {{ this.pcextra[101]}}</p>
-        <p>RAM: {{ this.pcextra[100] }}</p>
-        <p>Graphics: {{this.pcextra[4]}}</p>
-
-        <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#asseteditModal">Edit</button>
+        <button type="button" class="btn btn-primary" data-toggle="modal" :data-target="`#asset${asset.pcid}editModal`">Edit</button>
     </div>
 </template>
 <script>
+import AssetInfoEditModal from '../components/AssetInfoEditModal.vue'
 export default {
-    props: ['asset', 'pcextraStart'],
-    data() {
-        return {
-            data: JSON.parse(this.asset),
-            pcextra: JSON.parse(this.pcextraStart),
-        }
+    components: {
+        AssetInfoEditModal,
+    },
+    props: {
+        asset: {
+            type: Object,
+            required: true
+        },
     },
     mounted() {
-        Echo.channel('asset.'+this.data.pcid)
+        Echo.channel('asset.'+this.asset.pcid)
                 .listen('AssetUpdated', (e) => {
-                this.data = e.data;
-                this.pcextra = JSON.parse(e.pcextra);
+                this.asset = e.data;
             });
     },
-    methods: {
-        updateAsset() {
-            this.data.pcextra = this.pcextra;
-            axios.put('/api/assets/' + this.data.pcid, this.data)
-                    .then((response) => {})
-                    .catch((error) => {});
-                $('#asseteditModal').modal('hide');
-        }
-    }
 }
 </script>

+ 16 - 1
resources/js/components/credential.vue

@@ -42,8 +42,23 @@
 </div>
 </template>
 <script>
+import CredentialFormModal from '../components/CredentialFormModal.vue'
+import Modal from '../components/Modal.vue'
 export default {
-    props: ['credential', 'descriptions'],
+    components: {
+        CredentialFormModal,
+        Modal,
+    },
+    props: {
+        credential: {
+            type: Object,
+            required: true,
+        },
+        descriptions: {
+            type: Object,
+            required: true,
+        }
+    },
     data() {
         return {
             data: this.credential,

+ 6 - 21
resources/js/components/notes.vue

@@ -38,10 +38,14 @@
 </template>
 <script>
 import dateMixin from '../mixins/dateMixin'
+import NoteFormModal from '../components/NoteFormModal.vue'
 export default {
+    components: {
+        NoteFormModal,
+    },
     mixins:[dateMixin],
     props: {
-        initialnotes: {
+        notes: {
             type: Array,
             default: [],
         },
@@ -54,13 +58,12 @@ export default {
             required: true,
         },
         woid: {
-            type: Number,
+            type: String,
             required: true,
         }
     },
     data () {
         return {
-            notes: this.initialnotes,
             newNote: {
                 notetype: this.noteType,
                 thenote: '',
@@ -74,24 +77,6 @@ export default {
             return this.getNoteOrders(this.notes)
         }
     },
-    mounted () {
-        Echo.channel('wonotes.'+this.noteType+'.'+this.woid)
-            .listen('WorkOrderNoteAdded', (e) => {
-                this.notes.push(e.note)
-            })
-            .listen('WorkOrderNoteEdited', (e) => {
-                let index = this.notes.findIndex((note) => {
-                    return note.noteid === e.note.noteid
-                })
-                this.notes[index] = e.note
-            })
-            .listen('WorkOrderNoteDeleted', (e) => {
-                let index = this.notes.findIndex((note) => {
-                    return note.noteid === e.noteid
-                })
-                this.notes.splice(index, 1)
-            })
-    },
     methods: {
         createNote () {
             axios.post('/api/workorders/notes', this.newNote)

+ 13 - 4
resources/js/views/Dashboard.vue

@@ -1,11 +1,11 @@
 <template>
     <div class="container">
         <div class="row justify-content-center">
-            <div class="col-md-12">
+            <div v-if="!isLoading" class="col-md-12">
                 <div class="col-md-4" v-for="(workOrder, index) in workOrders" :key="workOrder.woid">
                     <div class="card">
                         <div class="card-header">
-                            <h4 class="card-title">{{workOrder.woid}}</h4>
+                            <router-link :to="{ name: 'workorders', params: { id: workOrder.woid } }" class="h4 card-title">{{workOrder.woid}}</router-link>
                         </div>
                         <div class="card-body card-body-dark">
                             <div>
@@ -18,14 +18,22 @@
                     </div>
                 </div>
             </div>
+            <div v-else class="col-md-12">
+                <circle-spinner color="#663399"></circle-spinner>
+            </div>
         </div>
     </div>
 </template>
-<script>
+<script>    
+import CircleSpinner from '../components/CircleSpinner.vue'
 export default {
+    components: {
+        CircleSpinner
+    },
     data () {
         return {
-            workOrders: []
+            workOrders: [],
+            isLoading: true,
         }
     },
     mounted () {
@@ -37,6 +45,7 @@ export default {
 
         axios.get('api/users/'+user+'/workorders').then(response => {
             this.workOrders = response.data
+            this.isLoading = false
         }).catch(error => {
             console.log(error)
         })

+ 1 - 1
resources/js/views/Login.vue

@@ -70,7 +70,7 @@
         },
         beforeRouteEnter (to, from, next) { 
             if (localStorage.getItem('jwt')) {
-                return next('board');
+                return next('dashboard');
             }
 
             next();

+ 114 - 13
resources/js/views/WorkOrder.vue

@@ -27,16 +27,18 @@
                     <div class="card-body">
                         <div class="tab-content">
                             <div class="tab-pane active" id="assetinfo" role="tabpanel" aria-labelledby="assetinfo-tab">
-                                <!-- <assetinfo asset="{{$asset}}"></assetinfo> -->
+                                <asset-info v-if="!assetLoading" :asset="asset"></asset-info>
+                                <circle-spinner v-else :color="'#663399'"></circle-spinner>
                             </div>
                             <!-- @if($workOrder->asset->group !== null)
                             <div class="tab-pane" id="group" role="tabpanel" aria-labelledby="group-tab">
                                 Name: {{$workOrder->asset->group->pcgroupname}}
                             </div>
-                            @endif
+                            @endif -->
                             <div class="tab-pane" id="credentials" role="tabpanel" aria-labelledby="credentials-tab">
-                                <credential-list :credential-list="{{$workOrder->asset->credentials->sortByDesc('creddate')->values()}}" :descriptions="{{App\CredDesc::all()}}" :pcid="{{$workOrder->asset->pcid}}"></credential-list>
-                            </div> -->
+                                <credential-list v-if="!credentialsLoading && !credentialDescriptionsLoading" :credentials="credentials" :descriptions="credentialDescriptions" :pcid="asset.pcid"></credential-list>
+                                <circle-spinner v-else color="#663399"></circle-spinner>
+                            </div>
                         </div>
                     </div>
                 </div>
@@ -62,7 +64,8 @@
                     <div class='card-body'>
                         <div class='tab-content'>
                             <div class="tab-pane active" id="workordersumm" role="tabpanel" aria-labelledby="workordersumm-tab">
-                                <work-order-info :work-order="workOrder" :stores="stores"></work-order-info>
+                                <work-order-info v-if="!woLoading && !storesLoading" :work-order="workOrder" :stores="stores"></work-order-info>
+                                <circle-spinner v-else :color="'#663399'"></circle-spinner>
                             </div>
                             <div class="tab-pane" id="attachments" role="tabpanel" aria-labelledby="attachments-tab">
                                 TODO 2
@@ -72,12 +75,13 @@
                 </div>
             </div>
         </div>
-        <!-- <div class="row my-3 no-gutters">
+        <div class="row my-3 no-gutters">
             <div class="col-12">
                 <div class="card">
                     <div class="card-body">
                         <h5 class="card-title">Customer Notes</h5>
-                        <notes :initialnotes='{{ $workOrder->notes->where('notetype', '0')->values() }}' authusername="{{Auth::user()->username}}" :note-type="0" :woid="{{$workOrder->woid}}"></notes>
+                        <notes v-if="!workOrderNotesLoading" :notes="publicNotes" :authusername="authUser" :note-type="0" :woid="id"></notes>
+                        <circle-spinner v-else color="#663399"></circle-spinner>
                     </div>
                 </div>
             </div>
@@ -87,49 +91,146 @@
                 <div class="card">
                     <div class="card-body">
                         <h5 class="card-title">Private/Billing Notes</h5>
-                        <notes :initialnotes='{{ $workOrder->notes->where('notetype', '1')->values() }}' authusername="{{Auth::user()->username}}" :note-type="1" :woid="{{$workOrder->woid}}"></notes>
+                        <notes v-if="!workOrderNotesLoading" :notes="privateNotes" :authusername="authUser" :note-type="1" :woid="id"></notes>
+                        <circle-spinner v-else color="#663399"></circle-spinner>
                     </div>
                 </div>
             </div>
-        </div> -->
+        </div>
     </div>
 </template>
 <script>
 import WorkOrderInfo from '../components/WorkOrderInfo.vue'
+import AssetInfo from '../components/AssetInfo.vue'
+import CircleSpinner from '../components/CircleSpinner.vue'
+import CredentialList from '../components/CredentialList.vue'
+import Notes from '../components/Notes.vue'
 export default {
     components: {
         WorkOrderInfo,
+        AssetInfo,
+        CircleSpinner,
+        CredentialList,
+        Notes,
+    },
+    props: {
+        id: {
+            type: String,
+            required: true,
+        }
     },
-    props: ['id'],
     data () {
         return {
             workOrder: {},
+            asset: {},
             stores: {},
+            credentials: {},
+            credentialDescriptions: {},
+            workOrderNotes: [],
+            woLoading: true,
+            assetLoading: true,
+            storesLoading: true,
+            credentialsLoading: true,
+            credentialDescriptionsLoading: true,
+            workOrderNotesLoading: true,
+            authUser: localStorage.getItem('user'),
+        }
+    },
+    computed: {
+        publicNotes: function() {
+            let notes = []
+            notes = this.workOrderNotes.filter(note => note.notetype === 0)
+            return notes
+        },
+        privateNotes: function() {
+            let notes = []
+            notes = this.workOrderNotes.filter(note => note.notetype === 1)
+            return notes
         }
     },
     mounted () {
+        // Get authentication info from current user from local storage
         let token = localStorage.getItem('jwt')
         let user = localStorage.getItem('user')
 
+        // Set some axios config options for Content-Type and Authentication.
         axios.defaults.headers.common['Content-Type'] = 'application/json'
         axios.defaults.headers.common['Authorization'] = 'Bearer ' + token
 
+        // Get WorkOrder from API
         axios.get('/api/workorders/'+this.id).then(response => {
             this.workOrder = response.data
+            this.woLoading = false
         }).catch(error => {
             console.log(error)
         })
 
+        // Get list of stores from API
         axios.get('/api/stores/').then(response => {
             this.stores = response.data
+            this.storesLoading = false
+        }).catch(error => {
+            console.log(error)
+        })
+
+        // Get Asset from API (will most likely be replaced with list of assets
+        // in the future. Current each WO can only have one but that is likely
+        // to change)
+        axios.get(`/api/workorders/${this.id}/asset`).then(response => {
+            this.asset = response.data
+            this.assetLoading = false
+
+            // Get Credentials for this asset. May be relocated to be part of asset
+            // Component when WOs can have more than one asset.
+            axios.get(`/api/assets/${this.asset.pcid}/credentials`).then (response => {
+                this.credentials = response.data
+                this.credentialsLoading = false
+            }).catch(error => {
+                console.log(error)
+            })
+
+            // Get default list of Credential Descriptions. May be relocated along with
+            // the above.
+            axios.get('/api/credentials/descriptions').then(response => {
+                this.credentialDescriptions = response.data
+                this.credentialDescriptionsLoading = false
+            }).catch(error => {
+                console.log(error)
+            })
+        }).catch(error => {
+            console.log(error)
+        })
+
+        axios.get(`/api/workorders/${this.id}/notes`).then(response => {
+            this.workOrderNotes = response.data
+            this.workOrderNotesLoading = false
         }).catch(error => {
             console.log(error)
         })
         
         Echo.channel('work-order.'+this.id)
-                .listen('WorkOrderUpdated', (e) => {
-                    this.workOrder = e.data;
-                });
+            .listen('WorkOrderUpdated', (e) => {
+                this.workOrder = e.data;
+            });
+
+        Echo.channel('wonotes.'+this.id)
+            .listen('WorkOrderNoteAdded', (e) => {
+                this.workOrderNotes.push(e.note)
+            })
+            .listen('WorkOrderNoteUpdated', (e) => {
+                let index = this.workOrderNotes.findIndex((note) => {
+                    return note.noteid === e.note.noteid
+                })
+                // Note has to be edited this way, or else Vue cannot
+                // see it to recompute the computed values.
+                this.workOrderNotes.splice(index, 1, e.note)
+            })
+            .listen('WorkOrderNoteDeleted', (e) => {
+                let index = this.notes.findIndex((note) => {
+                    return note.noteid === e.noteid
+                })
+                this.notes.splice(index, 1)
+            })
     },
 }
 </script>

+ 4 - 3
routes/api.php

@@ -23,13 +23,15 @@ Route::middleware('auth:api')->get('/user', function (Request $request) {
 Route::middleware('auth:api')->group( function() {
     Route::get('/users/{user}/workorders', 'Api\UsersController@workOrders');
 
-    Route::get('/workorders/{workOrder}/assets');
+    Route::get('/workorders/{workOrder}/asset', 'Api\WorkOrdersController@asset');
+    Route::get('/workorders/{workOrder}/notes', 'Api\WorkOrdersController@notes');
     Route::get('/workorders/{workOrder}', 'Api\WorkOrdersController@show');
     Route::put('/workorders/{workOrder}', 'Api\WorkOrdersController@update');
 
     Route::post('/workorders/notes', 'Api\WorkOrderNotesController@store');
     Route::put('/workorders/notes/{workOrderNote}', 'Api\WorkOrderNotesController@update');
 
+    Route::get('/assets/{asset}/credentials', 'Api\AssetsController@credentials');
     Route::put('/assets/{asset}', 'Api\AssetsController@update');
 
     Route::get('/stores', 'Api\StoresController@index');
@@ -38,7 +40,6 @@ Route::middleware('auth:api')->group( function() {
     Route::post('/credentials', 'Api\CredentialsController@store');
     Route::put('/credentials/{credential}', 'Api\CredentialsController@update');
     Route::delete('/credentials/{credential}', 'Api\CredentialsController@destroy');
-
-    Route::get('credtypes', 'Api\CredDescController@index');
+    Route::get('credentials/descriptions', 'Api\CredDescController@index');
 
 });

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä