Sfoglia il codice sorgente

Merge branch 'vue-spa-conversion' of chrisl/upccrt into master

Christopher Leggett 5 anni fa
parent
commit
4bf0f7b187
46 ha cambiato i file con 2801 aggiunte e 991 eliminazioni
  1. 8 0
      app/Asset.php
  2. 0 2
      app/Events/AssetUpdated.php
  3. 1 1
      app/Events/WorkOrderNoteAdded.php
  4. 40 0
      app/Events/WorkOrderNoteDeleted.php
  5. 39 0
      app/Events/WorkOrderNoteUpdated.php
  6. 6 1
      app/Http/Controllers/Api/AssetsController.php
  7. 70 0
      app/Http/Controllers/Api/UsersController.php
  8. 6 1
      app/Http/Controllers/Api/WorkOrderNotesController.php
  9. 7 10
      app/Http/Controllers/Api/WorkOrdersController.php
  10. 12 0
      app/Http/Controllers/SinglePageController.php
  11. 56 0
      app/Http/Controllers/UserController.php
  12. 8 0
      app/User.php
  13. 1 1
      composer.json
  14. 168 232
      composer.lock
  15. 2 2
      config/websockets.php
  16. 5 0
      package-lock.json
  17. 2 1
      package.json
  18. 1221 366
      public/js/app.js
  19. 41 29
      resources/js/app.js
  20. 0 1
      resources/js/bootstrap.js
  21. 27 0
      resources/js/components/AssetInfo.vue
  22. 91 0
      resources/js/components/AssetInfoEditModal.vue
  23. 0 0
      resources/js/components/AutocompleteCustomDropdown.vue
  24. 63 0
      resources/js/components/CircleSpinner.vue
  25. 30 4
      resources/js/components/Credential.vue
  26. 24 3
      resources/js/components/CredentialFormModal.vue
  27. 7 6
      resources/js/components/CredentialList.vue
  28. 0 0
      resources/js/components/ErrorList.vue
  29. 0 0
      resources/js/components/Modal.vue
  30. 54 0
      resources/js/components/NoteDeleteModal.vue
  31. 99 0
      resources/js/components/NoteFormModal.vue
  32. 26 26
      resources/js/components/Notes.vue
  33. 119 0
      resources/js/components/WoInfoEditModal.vue
  34. 22 20
      resources/js/components/WorkOrderInfo.vue
  35. 0 83
      resources/js/components/assetinfo.vue
  36. 0 56
      resources/js/components/note-form-modal.vue
  37. 0 63
      resources/js/components/woinfo-edit-modal.vue
  38. 85 0
      resources/js/views/App.vue
  39. 61 0
      resources/js/views/Dashboard.vue
  40. 79 0
      resources/js/views/Login.vue
  41. 54 0
      resources/js/views/Welcome.vue
  42. 250 0
      resources/js/views/WorkOrder.vue
  43. 5 0
      resources/views/landing.blade.php
  44. 1 65
      resources/views/layouts/app.blade.php
  45. 10 2
      routes/api.php
  46. 1 16
      routes/web.php

+ 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);
     }
 }

+ 40 - 0
app/Events/WorkOrderNoteDeleted.php

@@ -0,0 +1,40 @@
+<?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 WorkOrderNoteDeleted implements ShouldBroadcast
+{
+    use Dispatchable, InteractsWithSockets, SerializesModels;
+
+    public $note;
+
+    /**
+     * Create a new event instance.
+     *
+     * @return void
+     */
+    public function __construct($note)
+    {
+        $array = json_decode($note, true);
+        $this->note = $array;
+    }
+
+    /**
+     * Get the channels the event should broadcast on.
+     *
+     * @return \Illuminate\Broadcasting\Channel|array
+     */
+    public function broadcastOn()
+    {
+        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);

+ 70 - 0
app/Http/Controllers/Api/UsersController.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Http\Controllers\Controller;
+use App\User;
+use Illuminate\Http\Request;
+
+class UsersController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\User  $user
+     * @return \Illuminate\Http\Response
+     */
+    public function show(User $user)
+    {
+        //
+    }
+
+    public function workOrders(User $user) {
+        $workOrders = \App\WorkOrder::where('cibyuser', $user->username)->get();
+        return response()->json($workOrders->values(), 200);
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\User  $user
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, User $user)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\User  $user
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(User $user)
+    {
+        //
+    }
+}

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

@@ -57,7 +57,9 @@ class WorkOrderNotesController extends Controller
     public function update(Request $request, WorkOrderNote $workOrderNote)
     {
         $workOrderNote->thenote = $request->input('thenote');
+        $workOrderNote->notetype = $request->input('notetype');
         $workOrderNote->save();
+        event(new \App\Events\WorkOrderNoteUpdated($workOrderNote));
         return response()->json($workOrderNote, 200);
     }
 
@@ -69,6 +71,9 @@ class WorkOrderNotesController extends Controller
      */
     public function destroy(WorkOrderNote $workOrderNote)
     {
-        //
+        $deletedNote = $workOrderNote->toJson();
+        $workOrderNote->delete();
+        event(new \App\Events\WorkOrderNoteDeleted($deletedNote));
+        return response()->json("", 204);
     }
 }

+ 7 - 10
app/Http/Controllers/Api/WorkOrdersController.php

@@ -48,18 +48,15 @@ class WorkOrdersController extends Controller
      */
     public function show(WorkOrder $workOrder)
     {
-        return $workOrder;
+        return response()->json($workOrder);
     }
 
-    /**
-     * Show the form for editing the specified resource.
-     *
-     * @param  \App\WorkOrder  $workOrder
-     * @return \Illuminate\Http\Response
-     */
-    public function edit(WorkOrder $workOrder)
-    {
-        //
+    public function asset(WorkOrder $workOrder) {
+        return response()->json($workOrder->asset);
+    }
+
+    public function notes(WorkOrder $workOrder) {
+        return response()->json($workOrder->notes->values(), 200);
     }
 
     /**

+ 12 - 0
app/Http/Controllers/SinglePageController.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+class SinglePageController extends Controller
+{
+    public function index() {
+        return view("landing");
+    }
+}

+ 56 - 0
app/Http/Controllers/UserController.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\User;
+use Validator;
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use Illuminate\Support\Facades\Auth;
+
+class UserController extends Controller
+{
+    public function login()
+    {
+        $credentials = [
+            'username' => request('username'), 
+            'password' => request('password')
+        ];
+
+        if (Auth::attempt($credentials)) {
+            $success['token'] = Auth::user()->createToken('MyApp')->accessToken;
+            $success['username'] = Auth::user()->username;
+
+            return response()->json(['success' => $success]);
+        }
+
+        return response()->json(['error' => 'Unauthorised'], 401);
+    }
+
+    public function register(Request $request)
+    {
+        $validator = Validator::make($request->all(), [
+            'name' => 'required',
+            'email' => 'required|email',
+            'password' => 'required',
+        ]);
+
+        if ($validator->fails()) {
+            return response()->json(['error' => $validator->errors()], 401);
+        }
+
+        $input = $request->all();
+        $input['password'] = bcrypt($input['password']);
+
+        $user = User::create($input);
+        $success['token'] = $user->createToken('MyApp')->accessToken;
+        $success['name'] = $user->name;
+
+        return response()->json(['success' => $success]);
+    }
+
+    public function getDetails()
+    {
+        return response()->json(['success' => Auth::user()]);
+    }
+}

+ 8 - 0
app/User.php

@@ -44,4 +44,12 @@ class User extends Authenticatable
     {
         return $this->userpass;
     }
+
+    public function workOrders() {
+        return $this->hasMany('App\WorkOrder', 'cibyuser', 'username');
+    }
+
+    public function getRouteKeyName() {
+        return 'username';
+    }
 }

+ 1 - 1
composer.json

@@ -14,7 +14,7 @@
         "laravel/framework": "^6.2",
         "laravel/passport": "^8.4",
         "laravel/tinker": "^2.0",
-        "pusher/pusher-php-server": "~4.0"
+        "pusher/pusher-php-server": "~3.0"
     },
     "require-dev": {
         "facade/ignition": "^1.4",

File diff suppressed because it is too large
+ 168 - 232
composer.lock


+ 2 - 2
config/websockets.php

@@ -68,10 +68,10 @@ return [
      * the chance to add your own middleware to this list or change any of
      * the existing middleware. Or, you can simply stick with this list.
      */
-    'middleware' => [
+    /* 'middleware' => [
         'web',
         Authorize::class,
-    ],
+    ], */
 
     'statistics' => [
         /*

+ 5 - 0
package-lock.json

@@ -9446,6 +9446,11 @@
                 "vue-style-loader": "^4.1.0"
             }
         },
+        "vue-router": {
+            "version": "3.1.6",
+            "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.6.tgz",
+            "integrity": "sha512-GYhn2ynaZlysZMkFE5oCHRUTqE8BWs/a9YbKpNLi0i7xD6KG1EzDqpHQmv1F5gXjr8kL5iIVS8EOtRaVUEXTqA=="
+        },
         "vue-style-loader": {
             "version": "4.1.2",
             "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",

+ 2 - 1
package.json

@@ -27,6 +27,7 @@
         "@fortawesome/fontawesome-free": "^5.12.1",
         "clipboard": "^2.0.4",
         "laravel-echo": "^1.6.1",
-        "pusher-js": "^5.0.3"
+        "pusher-js": "^5.0.3",
+        "vue-router": "^3.1.6"
     }
 }

File diff suppressed because it is too large
+ 1221 - 366
public/js/app.js


+ 41 - 29
resources/js/app.js

@@ -9,41 +9,53 @@ require('./bootstrap');
 window.Clipboard = require('clipboard');
 require('./notify.min.js');
 
-window.Vue = require('vue');
+import Vue from 'vue'
+import VueRouter from 'vue-router'
 
-/**
- * The following block of code may be used to automatically register your
- * Vue components. It will recursively scan this directory for the Vue
- * components and automatically register them with their "basename".
- *
- * Eg. ./components/ExampleComponent.vue -> <example-component></example-component>
- */
+Vue.use(VueRouter)
 
-// const files = require.context('./', true, /\.vue$/i)
-// files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default))
-
-Vue.component('example-component', require('./components/ExampleComponent.vue').default);
-Vue.component('woinfo', require('./components/woinfo.vue').default);
-Vue.component('assetinfo', require('./components/assetinfo.vue').default);
-Vue.component('credential', require('./components/credential.vue').default);
-Vue.component('notes', require('./components/notes.vue').default);
-Vue.component('modal', require('./components/modal.vue').default);
-Vue.component('woinfo-edit-modal', require('./components/woinfo-edit-modal.vue').default);
-Vue.component('errorlist', require('./components/errorlist.vue').default);
-Vue.component('credential-form-modal', require('./components/credential-form-modal.vue').default);
-Vue.component('autocomplete-custom-dropdown', require('./components/autocomplete-custom-dropdown.vue').default);
-Vue.component('credential-list', require('./components/credential-list.vue').default);
-Vue.component('note-form-modal', require('./components/note-form-modal.vue').default);
+import App from './views/App'
+import Dashboard from './views/Dashboard'
+import WorkOrder from './views/WorkOrder'
+import Login from './views/Login'
+import Home from './views/Welcome'
 
-/**
- * Next, we will create a fresh Vue application instance and attach it to
- * the page. Then, you may begin adding components to this application
- * or customize the JavaScript scaffolding to fit your unique needs.
- */
+const router = new VueRouter({
+    mode: 'history',
+    routes: [
+        {
+            path: '/',
+            name: 'home',
+            component: Home
+        },
+        {
+            path: '/login',
+            name: 'login',
+            component: Login,
+        },
+        {
+            path: '/dashboard',
+            name: 'dashboard',
+            component: Dashboard,
+        },
+        {
+            path: '/workorders/:id',
+            name: 'workorders',
+            component: WorkOrder,
+            props: getIdAsString,
+        }
+    ],
+})
+
+function getIdAsString ( route ) {
+    return { id: route.params.id.toString() }
+}
 
 const app = new Vue({
     el: '#app',
-});
+    components: { App },
+    router,
+})
 
 $(function () {
     $('[data-toggle="tooltip"]').tooltip()

+ 0 - 1
resources/js/bootstrap.js

@@ -36,7 +36,6 @@ window.Pusher = require('pusher-js');
 window.Echo = new Echo({
     broadcaster: 'pusher',
     key: process.env.MIX_PUSHER_APP_KEY,
-    authEndpoint: '/broadcasting/auth',
     wsHost: window.location.hostname,
     wsPort: 6001,
     disableStats: true,

+ 27 - 0
resources/js/components/AssetInfo.vue

@@ -0,0 +1,27 @@
+<template>
+    <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>
+
+        <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 {
+    components: {
+        AssetInfoEditModal,
+    },
+    props: {
+        asset: {
+            type: Object,
+            required: true
+        },
+    },
+}
+</script>

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

+ 0 - 0
resources/js/components/autocomplete-custom-dropdown.vue → resources/js/components/AutocompleteCustomDropdown.vue


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

+ 30 - 4
resources/js/components/credential.vue → resources/js/components/Credential.vue

@@ -10,7 +10,10 @@
         </div>
         <div slot="footer">
             <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
-            <button type="button" class="btn btn-danger" @click=deleteCredential()>Confirm</button>
+            <button type="button" class="btn btn-danger" @click=deleteCredential()>
+                <div v-if="!credentialDeleting">Confirm</div>
+                <circle-spinner v-else :size="2"></circle-spinner>
+            </button>
         </div>
     </modal>
     <div class="row no-gutters">
@@ -42,12 +45,30 @@
 </div>
 </template>
 <script>
+import CredentialFormModal from '../components/CredentialFormModal.vue'
+import Modal from '../components/Modal.vue'
+import CircleSpinner from '../components/CircleSpinner.vue'
 export default {
-    props: ['credential', 'descriptions'],
+    components: {
+        CredentialFormModal,
+        Modal,
+        CircleSpinner,
+    },
+    props: {
+        credential: {
+            type: Object,
+            required: true,
+        },
+        descriptions: {
+            type: Object,
+            required: true,
+        }
+    },
     data() {
         return {
             data: this.credential,
-            creddescList: this.descriptions
+            creddescList: this.descriptions,
+            credentialDeleting: false,
         }
     },
     mounted() {
@@ -58,10 +79,15 @@ export default {
     },
     methods: {
         deleteCredential() {
+            this.credentialDeleting = true
             axios.delete('/api/credentials/' + this.data.credid, this.data)
                 .then(response => {
+                    this.credentialDeleting = false
                     $('#credential'+this.data.credid+'deleteModal').modal('hide');
-                }).catch(error => { console.error(error) })
+                }).catch(error => { 
+                    this.credentialDeleting = false
+                    console.error(error)
+                })
         }
     }
 }

+ 24 - 3
resources/js/components/credential-form-modal.vue → resources/js/components/CredentialFormModal.vue

@@ -20,12 +20,23 @@
         </div>
         <div slot="footer">
             <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
-            <button type="button" class="btn btn-primary" @click="updateCredential()">Save</button>
+            <button type="button" class="btn btn-primary" @click="updateCredential()">
+                <div v-if="!credentialSaving">Save</div>
+                <circle-spinner v-else :size="2"></circle-spinner>
+            </button>
         </div>
     </modal>
 </template>
 <script>
+import Modal from '../components/Modal.vue'
+import AutocompleteCustomDropdown from '../components/AutocompleteCustomDropdown.vue'
+import CircleSpinner from '../components/CircleSpinner.vue'
 export default {
+    components: {
+        AutocompleteCustomDropdown,
+        Modal,
+        CircleSpinner,
+    },
     props: {
         populateWith: {
             type: Object,
@@ -63,7 +74,8 @@ export default {
             data: JSON.parse(JSON.stringify(this.populateWith)),
             creddesc: {},
             id: this.modalId,
-            errors: []
+            errors: [],
+            credentialSaving: false,
         }
     },
     mounted () {
@@ -86,6 +98,7 @@ export default {
             this.data.credpass = this.rndStr(16);
         },
         updateCredential() {
+            this.credentialSaving = true
             if (this.creddesc.id === "custom") {
                 this.data.credtype = 1
                 this.data.creddesc = this.creddesc.name
@@ -104,13 +117,21 @@ export default {
                 }
                 axios.post('/api/credentials/', this.data)
                     .then(response => {
+                        this.credentialSaving = false
                         $('#'+this.id).modal('hide');
+                    }).catch(error => {
+                        this.credentialSaving = false
+                        console.error(error)
                     })
             } else {
                 axios.put('/api/credentials/' + this.data.credid, this.data)
                     .then(response => {
+                        this.credentialSaving = false
                         $('#'+this.id).modal('hide');
-                    }).catch(error =>{console.error(error)});
+                    }).catch(error => { 
+                        this.credentialSaving = false
+                        console.error(error)
+                    });
             }
         }
     }

+ 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 = {}

+ 0 - 0
resources/js/components/errorlist.vue → resources/js/components/ErrorList.vue


+ 0 - 0
resources/js/components/modal.vue → resources/js/components/Modal.vue


+ 54 - 0
resources/js/components/NoteDeleteModal.vue

@@ -0,0 +1,54 @@
+<template>
+     <modal :id="modalId" tabindex="-1" role="dialog" :aria-labelledby="modalId+'Label'">
+        <h5 slot="header" class="modal-title" :id="modalId+'Label'">
+            Delete Note
+        </h5>
+        <div slot="body">
+            Are you sure?
+        </div>
+        <div slot="footer">
+            <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+            <button type="button" class="btn btn-danger" @click="deleteNote(note)">
+                <div v-if="!noteDeleting">Confirm</div>
+                <circle-spinner v-else :size="2"></circle-spinner>
+            </button>
+        </div>
+    </modal>
+</template>
+<script>
+import Modal from './Modal.vue'
+import CircleSpinner from './CircleSpinner.vue'
+export default {
+    components: {
+        Modal,
+        CircleSpinner,
+    },
+    props: {
+        modalId: {
+            type: String,
+            require: true,
+        },
+        note: {
+            type: Object,
+            required: true,
+        },
+    },
+    data () {
+        return {
+            noteDeleting: false
+        }
+    },
+    methods: {
+        deleteNote: function(note) {
+            this.noteDeleting = true
+            axios.delete(`/api/workorders/notes/${this.note.noteid}`).then( response => {
+                this.noteDeleting = false
+                $(`#${this.modalId}`).modal('hide')
+            }).catch( error => {
+                this.noteDeleting = false
+                console.log(error)
+            })
+        }
+    }
+}
+</script>

+ 99 - 0
resources/js/components/NoteFormModal.vue

@@ -0,0 +1,99 @@
+<template>
+    <div>
+        <modal v-if="changeType" :id="modalId" tabindex="-1" role="dialog" :aria-labelledby="modalId+'Label'">
+            <!-- We want the original notetype, if we used note.notetype the text would change as soon as we hit the Confirm button.
+            This way the change in text occurs when we receive the updated info from the backend, so we can make sure there are no
+            errors preventing the data from actually changing first -->
+            <h5 v-if="populateWith.notetype === 0 " slot="header" class="modal-title" :id="modalId+'Label'">
+                Change to Private Note
+            </h5>
+            <h5 v-else slot="header" class="modal-title" :id="modalId+'Label'">
+                Change to Public Note
+            </h5>
+            <div slot="body">
+                Are you sure?
+            </div>
+            <div slot="footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+                <button type="button" class="btn btn-primary" @click="updateNote()">
+                    <div v-if="!noteSaving">Confirm</div>
+                    <circle-spinner v-else :size="2"></circle-spinner>
+                </button>
+            </div>
+        </modal>
+        <modal v-else :id="modalId" tabindex="-1" role="dialog" :aria-labelledby="modalId+'Label'">
+            <h5 slot="header" class="modal-title" :id="modalId+'Label'">
+                Edit Note
+            </h5>
+            <div slot="body">
+                <div class="form-group">
+                    <label for="content">Content</label>
+                    <textarea :name="'content'+note.noteid" :id="'content'+note.noteid" class="form-control" v-model="note.thenote"></textarea>
+                </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="updateNote()">
+                    <div v-if="!noteSaving">Save</div>
+                    <circle-spinner v-else :size="2"></circle-spinner>
+                </button>
+            </div>
+        </modal>
+    </div>
+</template>
+<script>
+import Modal from '../components/Modal.vue'
+import CircleSpinner from '../components/CircleSpinner.vue'
+export default {
+    components: {
+        Modal,
+        CircleSpinner,
+    },
+    props: {
+        populateWith: {
+            type: Object,
+            require: true
+        },
+        modalId: {
+            type: String,
+            require: true
+        },
+        noteUser: {
+            type: String
+        },
+        woid: {
+            type: Number
+        },
+        // If true, user is intending to switch the note
+        // from public to private or vice/versa. Modal
+        // Will be displayed differently, but the update
+        // logic is only different for one line of code.
+        changeType: {
+            type: Boolean,
+            default: false,
+        }
+    },
+    data () {
+        return {
+            note: JSON.parse(JSON.stringify(this.populateWith)),
+            noteSaving: false,
+        }
+    },
+    methods: {
+        updateNote () {
+            this.noteSaving = true
+            if (this.changeType) {
+                this.note.notetype = (this.note.notetype === 1 ? 0 : 1)
+            }
+            axios.put('/api/workorders/notes/' + this.note.noteid, this.note)
+                .then((response) => {
+                    this.noteSaving = false
+                    this.hideModal()
+                })
+        },
+        hideModal () {
+            $('#'+this.modalId).modal('hide')
+        }
+    }
+}
+</script>

+ 26 - 26
resources/js/components/notes.vue → resources/js/components/Notes.vue

@@ -8,21 +8,26 @@
                     <div class="collapse" :id="'note'+noteType+'add'">
                         <div class="d-flex flex-wrap flex-md-nowrap justify-content-center">
                             <textarea :name="'newnote'+noteType" :id="'newnote'+noteType" class="form-control" v-model="newNote.thenote"></textarea>
-                            <button type="button" class="btn btn-secondary m-2" @click="createNote()">Save</button>
+                            <button type="button" class="btn btn-secondary m-2" @click="createNote()">
+                                <div v-if="!noteSaving">Save</div>
+                                <circle-spinner v-else :size="2"></circle-spinner>
+                            </button>
                         </div>
                     </div>
                 </div>
             </div>
-        <li class="row no-gutters mb-2" v-bind:key="index" v-for="(note, index) in this.notes">
+        <li class="row no-gutters mb-2" v-bind:key="note.noteid" v-for="(note, index) in this.notes">
             <note-form-modal :modal-id="'note'+note.noteid+'editModal'" :populate-with="note"></note-form-modal>
+            <note-delete-modal :modal-id="`note${note.noteid}deleteModal`" :note="note"></note-delete-modal>
+            <note-form-modal :modal-id="`note${note.noteid}switchModal`" :populate-with="note" change-type></note-form-modal>
             <div class="col-md-1 d-flex flex-column mx-md-3" :class="noteOrders[index]">
                 <div class="text-center p-0 m-0">{{note.noteuser}}</div>
                 <div class="text-muted text-small text-center p-0 m-0">{{getHRDate(note.notetime)}}</div>
                 <div class="btn-group justify-content-center p-0 m-0">
                     <template v-if="authusername === note.noteuser || authusername === 'admin'">
                         <button class="btn btn-sm btn-primary m-1" data-toggle="modal" :data-target="'#note'+note.noteid+'editModal'"><i class="fas fa-fw fa-edit"></i></button>
-                        <button class="btn btn-sm btn-danger m-1"><i class="fas fa-fw fa-trash-alt"></i></button>
-                        <button class="btn btn-sm btn-primary m-1"><i class="fa fa-fw fa-random"></i></button>
+                        <button class="btn btn-sm btn-danger m-1" data-toggle="modal" :data-target="`#note${note.noteid}deleteModal`"><i class="fas fa-fw fa-trash-alt"></i></button>
+                        <button class="btn btn-sm btn-primary m-1" data-toggle="modal" :data-target="`#note${note.noteid}switchModal`"><i class="fa fa-fw fa-random"></i></button>
                     </template>
                 </div>
             </div>
@@ -38,10 +43,18 @@
 </template>
 <script>
 import dateMixin from '../mixins/dateMixin'
+import NoteFormModal from './NoteFormModal.vue'
+import CircleSpinner from './CircleSpinner.vue'
+import NoteDeleteModal from './NoteDeleteModal.vue'
 export default {
+    components: {
+        NoteFormModal,
+        CircleSpinner,
+        NoteDeleteModal
+    },
     mixins:[dateMixin],
     props: {
-        initialnotes: {
+        notes: {
             type: Array,
             default: [],
         },
@@ -54,19 +67,19 @@ export default {
             required: true,
         },
         woid: {
-            type: Number,
+            type: String,
             required: true,
         }
     },
     data () {
         return {
-            notes: this.initialnotes,
             newNote: {
                 notetype: this.noteType,
                 thenote: '',
                 noteuser: this.authusername,
-                woid: this.woid
+                woid: this.woid,
             },
+            noteSaving: false
         }
     },
     computed: {
@@ -74,30 +87,17 @@ 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 () {
+            this.noteSaving = true
             axios.post('/api/workorders/notes', this.newNote)
                     .then((response) => {
+                        this.noteSaving = false
                         $('#note'+this.noteType+'add').collapse('hide')
                         this.newNote.thenote = ''
+                    }).catch(error => {
+                        this.noteSaving = false
+                        console.log(error)
                     })
         },
         getNoteOrders(notes) {

+ 119 - 0
resources/js/components/WoInfoEditModal.vue

@@ -0,0 +1,119 @@
+<template>
+<modal :id="id" tabindex="-1" role="dialog" :aria-labelledby="id+'Label'">
+    <h5 slot="header" class="modal-title" :id="id+'Label'">
+        Edit Work Order Information
+    </h5>
+    <div slot="body">
+        <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>
+            <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>
+            <error-list :errors="errors.suggested"></error-list>
+        </div>
+        <div class="form-group">
+            <label for="storelist">Store</label>
+            <autocomplete-custom-dropdown :options="storeList" :value="this.store" v-model="store"></autocomplete-custom-dropdown>
+            <!-- <select name="storelist" id="storelist" class="form-control" v-if="storeList" v-model="data.storeid">
+                <option v-for="store in storeList" v-bind:key="store.storeid" v-bind:value="store.storeid">
+                    {{ store.storesname }}
+                </option>
+            </select> -->
+        </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="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.vue'
+import AutocompleteCustomDropdown from '../components/AutocompleteCustomDropdown.vue'
+import ErrorList from '../components/ErrorList.vue'
+import CircleSpinner from '../components/CircleSpinner.vue'
+export default {
+    components: {
+        Modal,
+        AutocompleteCustomDropdown,
+        ErrorList,
+        CircleSpinner,
+    },
+    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: JSON.parse(JSON.stringify(this.populateWith)),
+            id: this.modalId,
+            store: {},
+            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) => {
+                    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]
+        }
+    },
+    // 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))
+            this.store = {
+                'id': this.data.storeid,
+                'name': this.storeList[this.data.storeid]
+            }
+        },
+        storeList: function (value) {
+            this.store = {
+                'id': this.data.storeid,
+                'name': this.storeList[this.data.storeid]
+            }
+        }
+    }
+}
+</script>

+ 22 - 20
resources/js/components/woinfo.vue → resources/js/components/WorkOrderInfo.vue

@@ -1,18 +1,18 @@
 <template>
     <div>
-        <woinfo-edit-modal modal-id="workordereditModal" :populate-with="this.data" :store-list="this.storeList"></woinfo-edit-modal>
-        <p><i class="fas fa-fw fa-info-circle"></i> <span v-text="this.data.probdesc"></span></p>
-        <p><i class="far fa-fw fa-lightbulb"></i> <span v-text="this.data.suggested"></span></p>
-        <p><i class="fas fa-fw fa-paste"></i> <span v-text="this.data.woid"></span></p>
-        <p><i class="fas fa-fw fa-building"></i> <span>{{ storeList[data.storeid] }}</span></p>
+        <wo-info-edit-modal modal-id="workordereditModal" :populate-with="workOrder" :store-list="this.storeList"></wo-info-edit-modal>
+        <p><i class="fas fa-fw fa-info-circle"></i> <span v-text="workOrder.probdesc"></span></p>
+        <p><i class="far fa-fw fa-lightbulb"></i> <span v-text="workOrder.suggested"></span></p>
+        <p><i class="fas fa-fw fa-paste"></i> <span v-text="workOrder.woid"></span></p>
+        <p><i class="fas fa-fw fa-building"></i> <span>{{ storeList[workOrder.storeid] }}</span></p>
         <p><i class="fas fa-fw fa-sign-in-alt"></i> 
-            <span class="dashed-underline" data-toggle="tooltip" data-placement="bottom" v-bind:title=this.getHRDate(this.data.dropdate)>
-                {{ Math.floor(this.daysSinceToday(this.data.dropdate)) }} days ago
+            <span class="dashed-underline" data-toggle="tooltip" data-placement="bottom" v-bind:title=this.getHRDate(workOrder.dropdate)>
+                {{ daysOrDate(workOrder.dropdate) }}
             </span>
         </p>
         <p><i class="fas fa-sign-out-alt"></i> 
-            <span class="dashed-underline" data-toggle="tooltip" data-placement="bottom" v-if="!this.isZero(this.data.pickupdate)" v-bind:title=this.getHRDate(this.data.pickupdate)>
-                {{ Math.floor(this.daysSinceToday(this.data.pickupdate)) }} days ago
+            <span class="dashed-underline" data-toggle="tooltip" data-placement="bottom" v-if="!this.isZero(workOrder.pickupdate)" v-bind:title=this.getHRDate(workOrder.pickupdate)>
+                {{ daysOrDate(workOrder.pickupdate) }}
             </span>
         </p>
         <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#workordereditModal">Edit</button>
@@ -20,28 +20,30 @@
 </template>
 <script>
     import dateMixin from '../mixins/dateMixin'
+    import WoInfoEditModal from '../components/WoInfoEditModal'
     export default {
+        components: {
+            WoInfoEditModal
+        },
         mixins: [dateMixin],
         props: ['workOrder', 'stores'],
-        data() {
-            return {
-                data: JSON.parse(this.workOrder),
-            }
-        },
         computed: {
             storeList: function () {
                 let list = {}
-                JSON.parse(this.stores).map(val => {
+                Object.values(this.stores).map(val => {
                     list[val.storeid] = val.storesname
                 })
                 return list
             }
         },
-        mounted() {
-            Echo.channel('work-order.'+this.data.woid)
-                .listen('WorkOrderUpdated', (e) => {
-                    this.data = e.data;
-                });
+        methods: {
+            daysOrDate: function(date) {
+                if (this.daysSinceToday(date) <= 30) {
+                    return Math.floor(this.daysSinceToday(date)) + "days ago"
+                } else {
+                    return this.getHRDate(date)
+                }
+            }
         }
     }
 </script>

+ 0 - 83
resources/js/components/assetinfo.vue

@@ -1,83 +0,0 @@
-<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>
-
-            <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>
-    </div>
-</template>
-<script>
-export default {
-    props: ['asset', 'pcextraStart'],
-    data() {
-        return {
-            data: JSON.parse(this.asset),
-            pcextra: JSON.parse(this.pcextraStart),
-        }
-    },
-    mounted() {
-        Echo.channel('asset.'+this.data.pcid)
-                .listen('AssetUpdated', (e) => {
-                this.data = e.data;
-                this.pcextra = JSON.parse(e.pcextra);
-            });
-    },
-    methods: {
-        updateAsset() {
-            this.data.pcextra = this.pcextra;
-            axios.put('/api/assets/' + this.data.pcid, this.data)
-                    .then((response) => {})
-                    .catch((error) => {});
-                $('#asseteditModal').modal('hide');
-        }
-    }
-}
-</script>

+ 0 - 56
resources/js/components/note-form-modal.vue

@@ -1,56 +0,0 @@
-<template>
-    <modal :id="modalId" tabindex="-1" role="dialog" :aria-labelledby="modalId+'Label'">
-                <h5 slot="header" class="modal-title" :id="modalId+'Label'">
-                    Edit Note
-                </h5>
-                <div slot="body">
-                    <div class="form-group">
-                        <label for="content">Content</label>
-                        <textarea :name="'content'+note.noteid" :id="'content'+note.noteid" class="form-control" v-model="note.thenote"></textarea>
-                    </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="updateNote(note)">Save</button>
-                </div>
-            </modal>
-</template>
-<script>
-export default {
-    props: {
-        populateWith: {
-            type: Object,
-            require: true
-        },
-        modalId: {
-            type: String,
-            require: true
-        },
-        noteType: {
-            type: Number
-        },
-        noteUser: {
-            type: String
-        },
-        woid: {
-            type: Number
-        }
-    },
-    data () {
-        return {
-            note: JSON.parse(JSON.stringify(this.populateWith))
-        }
-    },
-    methods: {
-        updateNote (note) {
-            axios.put('/api/workorders/notes/' + note.noteid, note)
-                .then((response) => {
-                    hideModal()
-                })
-        },
-        hideModal () {
-            $('#'+this.modalId).modal('hide')
-        }
-    }
-}
-</script>

+ 0 - 63
resources/js/components/woinfo-edit-modal.vue

@@ -1,63 +0,0 @@
-<template>
-<modal :id="id" tabindex="-1" role="dialog" :aria-labelledby="id+'Label'">
-    <h5 slot="header" class="modal-title" :id="id+'Label'">
-        Edit Work Order Information
-    </h5>
-    <div slot="body">
-        <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>
-        </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>
-        </div>
-        <div class="form-group">
-            <label for="storelist">Store</label>
-            <autocomplete-custom-dropdown :options="storeList" :value="this.store" v-model="store"></autocomplete-custom-dropdown>
-            <!-- <select name="storelist" id="storelist" class="form-control" v-if="storeList" v-model="data.storeid">
-                <option v-for="store in storeList" v-bind:key="store.storeid" v-bind:value="store.storeid">
-                    {{ store.storesname }}
-                </option>
-            </select> -->
-        </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="updateWorkOrder()">Save</button>
-    </div>
-</modal>
-</template>
-<script>
-export default {
-    props:['populateWith', 'storeList', 'modalId'],
-    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: JSON.parse(JSON.stringify(this.populateWith)),
-            id: this.modalId,
-            store: {},
-            errors: []
-        }
-    },
-    mounted () {
-        this.store = {
-            'id': this.data.storeid,
-            'name': this.storeList[this.data.storeid]
-        }
-    },
-    methods: {
-        updateWorkOrder() {
-            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; });
-        }
-    }
-}
-</script>

+ 85 - 0
resources/js/views/App.vue

@@ -0,0 +1,85 @@
+<template>
+    <div>
+        <nav class="navbar navbar-expand navbar-dark bg-primary text-light shadow-sm">
+            <div class="container-fluid">
+                <router-link :to="{name: 'home'}" class="navbar-brand">
+                    UPCCRT
+                </router-link>
+                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false">
+                    <span class="navbar-toggler-icon"></span>
+                </button>
+
+                <div class="collapse navbar-collapse" id="navbarSupportedContent">
+                    <!-- Left Side Of Navbar -->
+                    <ul class="navbar-nav mr-auto text-center">
+                        <li class="nav-item">
+                            <a href="#" class="nav-link"><i class="fas fa-sign-in-alt"></i> <span class="d-none d-sm-block d-md-inline">Check-In</span></a>
+                        </li>
+                        <li class="nav-item">
+                            <a href="#" class="nav-link"><i class="fas fa-sign-out-alt"></i> <span class="d-none d-sm-block d-md-inline">Check-Out</span></a>
+                        </li>
+                        <li class="nav-item">
+                            <router-link :to="{ name: 'dashboard' }" class="nav-link"><i class="fas fa-tachometer-alt"></i> <span class="d-none d-sm-block d-md-inline">Dashboard</span></router-link>
+                        </li>
+                        <li class="nav-item">
+                            <a href="#" class="nav-link"><i class="fas fa-chart-bar"></i> <span class="d-none d-sm-block d-md-inline">Reports</span></a>
+                        </li>
+                    </ul>
+
+                    <!-- Right Side Of Navbar -->
+                    <ul class="navbar-nav ml-auto">
+                        <!-- Authentication Links -->
+                        <router-link :to="{ name: 'login' }" class="nav-link" v-if="!isLoggedIn">Login</router-link>
+                        <li class="nav-link" v-if="isLoggedIn">{{username}}</li>
+                        <router-link :to="{ name: 'home'}" class="nav-link" v-if="isLoggedIn">Home</router-link>
+                        <!-- @guest
+                            <li class="nav-item">
+                                <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
+                            </li>
+                            @if (Route::has('register'))
+                                <li class="nav-item">
+                                    <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
+                                </li>
+                            @endif
+                        @else
+                            <li class="nav-item dropdown">
+                                <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
+                                    {{ Auth::user()->username }} <span class="caret"></span>
+                                </a>
+
+                                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
+                                    <a class="dropdown-item" href="{{ route('logout') }}"
+                                       onclick="event.preventDefault();
+                                                     document.getElementById('logout-form').submit();">
+                                        {{ __('Logout') }}
+                                    </a>
+
+                                    <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
+                                        @csrf
+                                    </form>
+                                </div>
+                            </li>
+                        @endguest -->
+                    </ul>
+                </div>
+            </div>
+        </nav>
+        <main class="py-4">
+            <router-view></router-view>
+        </main>
+    </div>
+</template>
+<script>
+export default {
+    data () {
+        return {
+            isLoggedIn : null,
+            username : null
+        }
+    },
+    mounted () {
+        this.isLoggedIn = localStorage.getItem('jwt')
+        this.username = localStorage.getItem('user')
+    }
+}
+</script>

+ 61 - 0
resources/js/views/Dashboard.vue

@@ -0,0 +1,61 @@
+<template>
+    <div class="container">
+        <div class="row justify-content-center">
+            <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">
+                            <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>
+                                {{ workOrder.probdesc }}
+                            </div>
+                            <div>
+                                {{ workOrder.suggested }}
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div v-else class="col-md-12">
+                <circle-spinner color="#663399"></circle-spinner>
+            </div>
+        </div>
+    </div>
+</template>
+<script>    
+import CircleSpinner from '../components/CircleSpinner.vue'
+export default {
+    components: {
+        CircleSpinner
+    },
+    data () {
+        return {
+            workOrders: [],
+            isLoading: true,
+        }
+    },
+    mounted () {
+        let token = localStorage.getItem('jwt')
+        let user = localStorage.getItem('user')
+
+        axios.defaults.headers.common['Content-Type'] = 'application/json'
+        axios.defaults.headers.common['Authorization'] = 'Bearer ' + token
+
+        axios.get('api/users/'+user+'/workorders').then(response => {
+            this.workOrders = response.data
+            this.isLoading = false
+        }).catch(error => {
+            console.log(error)
+        })
+    },
+    beforeRouteEnter (to, from, next) {
+        if ( ! localStorage.getItem('jwt')) {
+            return next('login')
+        }
+
+        next()
+    }
+}
+</script>

+ 79 - 0
resources/js/views/Login.vue

@@ -0,0 +1,79 @@
+<template>
+    <div class="container">
+        <div class="row justify-content-center">
+            <div class="col-md-8">
+                <div class="card card-default">
+                    <div class="card-header">Login</div>
+
+                    <div class="card-body">
+                        <form method="POST" action="/login">
+                            <div class="form-group row">
+                                <label for="username" class="col-sm-4 col-form-label text-md-right">Username</label>
+
+                                <div class="col-md-6">
+                                    <input id="username" type="username" class="form-control" v-model="username" required autofocus>
+                                </div>
+                            </div>
+
+                            <div class="form-group row">
+                                <label for="password" class="col-md-4 col-form-label text-md-right">Password</label>
+
+                                <div class="col-md-6">
+                                    <input id="password" type="password" class="form-control" v-model="password" required>
+                                </div>
+                            </div>
+
+                            <div class="form-group row mb-0">
+                                <div class="col-md-8 offset-md-4">
+                                    <button type="submit" class="btn btn-primary" @click="handleSubmit">
+                                        Login
+                                    </button>
+                                </div>
+                            </div>
+                        </form>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+    export default {
+        data(){
+            return {
+                username : "",
+                password : ""
+            }
+        },
+        methods : {
+            handleSubmit(e){
+                e.preventDefault()
+
+                if (this.password.length > 0) {
+                    axios.post('api/login', {
+                        username: this.username,
+                        password: this.password
+                        })
+                        .then(response => {
+                        localStorage.setItem('user',response.data.success.username)
+                        localStorage.setItem('jwt',response.data.success.token)
+
+                        if (localStorage.getItem('jwt') != null){
+                            this.$router.go('/dashboard')
+                        }
+                        })
+                        .catch(function (error) {
+                        console.error(error);
+                        });
+                }
+            }
+        },
+        beforeRouteEnter (to, from, next) { 
+            if (localStorage.getItem('jwt')) {
+                return next('dashboard');
+            }
+
+            next();
+        }
+    }
+</script>

+ 54 - 0
resources/js/views/Welcome.vue

@@ -0,0 +1,54 @@
+<template>
+    <div class="flex-center position-ref full-height">
+        <div class="content">
+            <div  class="m-b-md">
+                <h2 class="title m-b-md">
+                    UPCCRT
+                </h2>
+                <h3>
+                    University PC Care Repair Tracker
+                </h3>
+            </div>
+        </div>
+    </div>
+</template>
+<style scoped>
+    .full-height {
+        height: 100vh;
+    }
+    .flex-center {
+        align-items: center;
+        display: flex;
+        justify-content: center;
+    }
+    .position-ref {
+        position: relative;
+    }
+    .top-right {
+        position: absolute;
+        right: 10px;
+        top: 18px;
+    }
+    .content {
+        text-align: center;
+    }
+    .title {
+        font-size: 60px;
+    }
+    .links > a {
+        color: #636b6f;
+        padding: 0 25px;
+        font-size: 12px;
+        font-weight: 600;
+        letter-spacing: .1rem;
+        text-decoration: none;
+        text-transform: uppercase;
+    }
+    .m-b-md {
+        margin-bottom: 30px;
+        color: #000000;
+    }
+</style>
+<script>
+export default {}
+</script>

+ 250 - 0
resources/js/views/WorkOrder.vue

@@ -0,0 +1,250 @@
+<template>
+    <div class="container-fluid">
+        <div class="row my-3">
+            <div class="col-lg-6">
+                <div class="card h-100">
+                    <div class="card-header text-right">
+                        <button type="button" class="btn btn-default xs-toggle" data-toggle="collapse" data-target="#assetTab">
+                            <span class="sr-only">Toggle Navigation</span>
+                            <span class="icon-bar"></span>
+                            <span class="icon-bar"></span>
+                            <span class="icon-bar"></span>
+                        </button>
+                        <ul class="nav nav-pills card-header-pills nav-justified xs-collapse collapse" id="assetTab" role="tablist">
+                            <li class="nav-item">
+                                <a class="nav-link active" id="assetinfo-tab" data-toggle="pill" href="#assetinfo" role="tab" aria-controls="assetinfo" aria-selected="true">Hardware</a>
+                            </li>
+                            <!-- @if($workOrder->asset->group !== null)
+                            <li class="nav-item">
+                                <a class="nav-link" id="group-tab" data-toggle="pill" href="#group" role="tab" aria-controls="group" aria-selected="false">Group</a>
+                            </li>
+                            @endif -->
+                            <li class="nav-item">
+                                <a class="nav-link" id="credentials-tab" data-toggle="pill" href="#credentials" role="tab" aria-controls="credentials" aria-selected="false">Credentials</a>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="card-body">
+                        <div class="tab-content">
+                            <div class="tab-pane active" id="assetinfo" role="tabpanel" aria-labelledby="assetinfo-tab">
+                                <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 -->
+                            <div class="tab-pane" id="credentials" role="tabpanel" aria-labelledby="credentials-tab">
+                                <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>
+            </div>
+            <div class="col-lg-6">
+                <div class="card h-100">
+                    <div class="card-header text-right">
+                        <button type="button" class="btn btn-default xs-toggle" data-toggle="collapse" data-target="#workorderTab">
+                            <span class="sr-only">Toggle Navigation</span>
+                            <span class="icon-bar"></span>
+                            <span class="icon-bar"></span>
+                            <span class="icon-bar"></span>
+                        </button>
+                        <ul class="nav nav-pills card-header-pills nav-justified xs-collapse collapse" id="workorderTab" role="tablist">
+                            <li class="nav-item">
+                                <a class="nav-link active" id="workordersumm-tab" data-toggle="pill" href="#workordersumm" role="tab" aria-controls="workordersumm" aria-selected="true">Summary</a>
+                            </li>
+                            <li class="nav-item">
+                                <a class="nav-link" id="attachments-tab" data-toggle="pill" href="#attachments" role="tab" aria-controls="attachments" aria-selected="false">Attachments</a>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class='card-body'>
+                        <div class='tab-content'>
+                            <div class="tab-pane active" id="workordersumm" role="tabpanel" aria-labelledby="workordersumm-tab">
+                                <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
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <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 v-if="!workOrderNotesLoading" :notes="publicNotes" :authusername="authUser" :note-type="0" :woid="id"></notes>
+                        <circle-spinner v-else color="#663399"></circle-spinner>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="row my-3 no-gutters">
+            <div class="col-12">
+                <div class="card">
+                    <div class="card-body">
+                        <h5 class="card-title">Private/Billing Notes</h5>
+                        <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>
+</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,
+        }
+    },
+    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
+
+            // Establish listener for liveupdate broadcasts.
+            // Must be done here or we can't guarantee this.asset
+            // exists by the time we execute this code.
+             Echo.channel('asset.'+this.asset.pcid)
+            .listen('AssetUpdated', (e) => {
+                this.asset = e.data;
+            });
+
+            // 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;
+            });
+
+        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
+                })
+                // Sometimes when switching or deleting items from the end of a list,
+                // They go away before the modal can be hidden, this will hide the
+                // modal in case the component can't hide it itself. There may be 
+                // a better way to do this.
+                $(`#note${e.note.noteid}switchModal`).modal('hide')
+                // 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.workOrderNotes.findIndex((note) => {
+                    return note.noteid === e.note['noteid']
+                })
+                $(`#note${e.note['noteid']}deleteModal`).modal('hide')
+                this.workOrderNotes.splice(index, 1)
+            })
+    },
+}
+</script>

+ 5 - 0
resources/views/landing.blade.php

@@ -0,0 +1,5 @@
+@extends('layouts.app')
+
+@section('content')
+    <app></app>
+@endsection

+ 1 - 65
resources/views/layouts/app.blade.php

@@ -21,71 +21,7 @@
 </head>
 <body>
     <div id="app">
-        <nav class="navbar navbar-expand navbar-dark bg-primary text-light shadow-sm">
-            <div class="container-fluid">
-                <a class="navbar-brand" href="{{ url('/') }}">
-                    {{ config('app.name', 'Laravel') }}
-                </a>
-                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
-                    <span class="navbar-toggler-icon"></span>
-                </button>
-
-                <div class="collapse navbar-collapse" id="navbarSupportedContent">
-                    <!-- Left Side Of Navbar -->
-                    <ul class="navbar-nav mr-auto text-center">
-                        <li class="nav-item">
-                            <a href="#" class="nav-link"><i class="fas fa-sign-in-alt"></i> <span class="d-none d-sm-block d-md-inline">Check-In</span></a>
-                        </li>
-                        <li class="nav-item">
-                            <a href="#" class="nav-link"><i class="fas fa-sign-out-alt"></i> <span class="d-none d-sm-block d-md-inline">Check-Out</span></a>
-                        </li>
-                        <li class="nav-item">
-                            <a href="#" class="nav-link"><i class="fas fa-tachometer-alt"></i> <span class="d-none d-sm-block d-md-inline">Dashboard</span></a>
-                        </li>
-                        <li class="nav-item">
-                            <a href="#" class="nav-link"><i class="fas fa-chart-bar"></i> <span class="d-none d-sm-block d-md-inline">Reports</span></a>
-                        </li>
-                    </ul>
-
-                    <!-- Right Side Of Navbar -->
-                    <ul class="navbar-nav ml-auto">
-                        <!-- Authentication Links -->
-                        @guest
-                            <li class="nav-item">
-                                <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
-                            </li>
-                            @if (Route::has('register'))
-                                <li class="nav-item">
-                                    <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
-                                </li>
-                            @endif
-                        @else
-                            <li class="nav-item dropdown">
-                                <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
-                                    {{ Auth::user()->username }} <span class="caret"></span>
-                                </a>
-
-                                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
-                                    <a class="dropdown-item" href="{{ route('logout') }}"
-                                       onclick="event.preventDefault();
-                                                     document.getElementById('logout-form').submit();">
-                                        {{ __('Logout') }}
-                                    </a>
-
-                                    <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
-                                        @csrf
-                                    </form>
-                                </div>
-                            </li>
-                        @endguest
-                    </ul>
-                </div>
-            </div>
-        </nav>
-
-        <main class="py-4">
-            @yield('content')
-        </main>
+        @yield('content')
     </div>
 </body>
 </html>

+ 10 - 2
routes/api.php

@@ -13,17 +13,26 @@ use Illuminate\Http\Request;
 |
 */
 
+Route::post('login', 'UserController@login');
+Route::post('register', 'UserController@register');
+
 Route::middleware('auth:api')->get('/user', function (Request $request) {
     return $request->user();
 });
 
 Route::middleware('auth:api')->group( function() {
+    Route::get('/users/{user}/workorders', 'Api\UsersController@workOrders');
+
+    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::delete('/workorders/notes/{workOrderNote}', 'Api\WorkOrderNotesController@destroy');
 
+    Route::get('/assets/{asset}/credentials', 'Api\AssetsController@credentials');
     Route::put('/assets/{asset}', 'Api\AssetsController@update');
 
     Route::get('/stores', 'Api\StoresController@index');
@@ -32,7 +41,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');
 
 });

+ 1 - 16
routes/web.php

@@ -11,19 +11,4 @@
 |
 */
 
-Route::get('/', function () {
-    return view('welcome');
-});
-
-Auth::routes();
-
-Route::get('/home', 'HomeController@index')->name('home');
-
-Route::get('/workorders/{workOrder}', 'WorkOrdersController@show');
-
-Route::get('/workorders/{workOrder}/edit', 'WorkOrdersController@edit')->name('editwo');
-
-Route::put('/workorders/{workOrder}', 'WorkOrdersController@update')->name('updatewo');
-
-Route::get('/assets/{asset}/edit', 'AssetsController@edit')->name('editasset');
-Route::put('assets/{asset}', 'AssetsController@update')->name('updateasset');
+Route::get('/{any}', 'SinglePageController@index')->where('any', '.*');

Some files were not shown because too many files changed in this diff