Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

61 changed files with 7345 additions and 22566 deletions

1
.gitignore vendored
View file

@ -21,4 +21,3 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?

Binary file not shown.

View file

@ -1,17 +0,0 @@
#!/bin/bash
method="patch"
if [[ $1 != "" ]]; then
method=$1
fi
pnpm version $method
if [[ $? != 0 ]]; then
exit
fi
pnpm run build
if [[ $? == 0 ]]; then
tar czf cmc_fe.tar.gz dist
git add . cmc_fe.tar.gz
git commit -m "updated cmc_fe.tar.gz"
git push
fi

12709
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "cmc_fe",
"version": "0.1.10",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
@ -9,37 +9,35 @@
},
"dependencies": {
"@mdi/font": "5.9.55",
"@vuepic/vue-datepicker": "^3.6.8",
"animate.css": "^4.1.1",
"axios": "^1.3.4",
"core-js": "^3.30.0",
"html2pdf.js": "^0.10.1",
"@vuepic/vue-datepicker": "^3.6.4",
"axios": "^1.2.2",
"core-js": "^3.8.3",
"moment": "^2.29.4",
"roboto-fontface": "^0.10.0",
"sass": "^1.60.0",
"sass-loader": "^13.2.2",
"roboto-fontface": "*",
"sass": "^1.57.1",
"sass-loader": "^13.2.0",
"scss": "^0.2.4",
"vue": "^3.2.47",
"vue": "^3.2.13",
"vue-datepicker": "^1.3.0",
"vue-meta": "^2.4.0",
"vue-router": "^4.1.6",
"vue-splash": "^1.2.1",
"vue-virtual-scroller": "2.0.0-beta.8",
"vue-router": "^4.0.3",
"vue-virtual-scroller": "^2.0.0-beta.7",
"vue3-html2pdf": "^1.1.2",
"vue3-print-nb": "^0.1.4",
"vuetify": "^3.1.12",
"webfontloader": "^1.6.28"
"vuetify": "^3.0.0-beta.0",
"webfontloader": "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.21.4",
"@babel/eslint-parser": "^7.21.3",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-router": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.7.1",
"eslint-plugin-vue": "^8.0.3",
"vue-cli-plugin-vuetify": "~2.5.8",
"webpack-plugin-vuetify": "^2.0.1"
"webpack-plugin-vuetify": "^2.0.0-alpha.0"
},
"eslintConfig": {
"root": true,

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

View file

@ -4,12 +4,8 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>images/logo_fields.png">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="icon" href="<%= BASE_URL %>images/favicon32.png" sizes="32x32" />
<link rel="icon" href="<%= BASE_URL %>images/favicon192.png" sizes="192x192" />
<link rel="apple-touch-icon" href="<%= BASE_URL %>images/favicon180.png" />
<meta name="msapplication-TileImage" content="<%= BASE_URL %>images/favicon270.png" />
</head>
<body>
<noscript>

View file

@ -1,40 +1,12 @@
<template>
<CMCApp @ready="isReady" :class="{ fadein : !isLoading }" />
<LoadingScreen :isLoading="isLoading" />
<CMCApp />
</template>
<script>
import CMCApp from './CMCApp.vue'
import LoadingScreen from "./components/LoadingScreen.vue"
export default {
data(){
return {
isLoading: true
}
},
components: {
CMCApp,
LoadingScreen
},
methods: {
isReady() {
this.isLoading = false
}
},
CMCApp
}
}
</script>
<style>
.fadein {
animation: fadein 1s forwards;
}
@keyframes fadein {
from {
opacity:0;
visibility:hidden;
}
to {
opacity:1;
visibility:visible;
}
}
</style>

View file

@ -1,14 +1,12 @@
<template>
<v-app>
<MyNav :user="user" :site_info="site_info" v-if="!isLoading" />
<v-main class="ma-4">
<v-banner v-if="!site_info.backend_connected"
icon="mdi-exclamation"
color="error"
text="Cannot connect to the data service. Please contact support." >
</v-banner>
<MyNav :user="user" :site_info="site_info" />
<v-main class="ma-4">
<router-view :site_info="site_info" :user_info="user_info"></router-view>
</v-main>
</v-main>
<v-footer>
<sub>{{ site_info.name }} v{{ site_info.version }}</sub>
</v-footer>
</v-app>
</template>
@ -19,16 +17,13 @@ import axios from 'axios'
export default {
name: 'App',
components: {
MyNav
MyNav,
},
emits: ["ready"],
data() {
return {
isLoading: true,
site_info: {
name: "",
features: {},
backend_connected: false
name: "Loading...",
features: {}
},
user: {
first_name: "",
@ -49,14 +44,6 @@ export default {
}
},
methods: {
checkIfReady(){
if (this.site_info.name != "") {
setTimeout(() => {
this.isLoading = false
this.$emit("ready")
},1000)
}
},
checkLoginStatus() {
let url = this.$api_url + "/users/check_login"
console.log("Checking login status...")
@ -83,24 +70,10 @@ export default {
},
async getSiteInfo() {
console.log("Trying to get site Info...")
axios
.get(this.$api_url + "/info")
.then(response => {
this.site_info = response.data
console.log(this.site_info)
this.site_info.backend_connected = true
})
.catch(error => {
this.site_info.name = "Error getting data connection"
this.site_info.backend_connected = false
console.log(error)
setTimeout(() => {
this.getSiteInfo()
},5000)
}).finally(() => {
this.checkIfReady()
})
.then(response => {this.site_info = response.data})
.catch(error => (console.log(error)))
},
async getUserInfo() {

View file

@ -1,14 +1,6 @@
<script>
import moment from 'moment'
import axios from 'axios'
import Error from '@/types/ErrorType.vue'
export default {
data() {
return {
customer_list: [],
errors: {}
}
},
methods:{
formatDate(d,f) {
return moment(String(d)).format(f)
@ -34,35 +26,6 @@ export default {
minute: '2-digit',
second: '2-digit'})
},
async error(msg) {
let e = new Error()
e.msg = msg
e.id = crypto.randomUUID()
this.errors[e.id] = e
setTimeout(() => {
delete this.errors[e.id]
}, 10000)
},
async getCustomerList(name) {
console.log("searching customers...")
if (this.customer_list.length == 0) {
console.log("getting new customers...")
let url = this.$api_url + "/customers/full_list"
axios.get(url)
.then(resp => {
this.customer_list = resp.data
console.log(this.customer_list)
return this.customer_list.filter(x => {
x.name.contains(name) || x.acc_no.contains(name)
})
})
.catch(err => {
console.log(err)
})
} else {
return this.customer_list
}
}
}
}
</script>

View file

@ -67,10 +67,10 @@ table tr.at_risk:hover {
color: grey;
}
.scroller {
height: 600px;
border: 1px dotted #333;
}
.scroller.small {
height: 300px;
.scroller {
height: 600px;
}
.item {
@ -82,6 +82,3 @@ table tr.at_risk:hover {
align-items: center;
border-bottom: 1px solid #000;
}
.clickable {
cursor:pointer;
}

View file

@ -1,50 +0,0 @@
<template>
<v-container>
<v-card title="Add Comment">
<v-card-text>
<p>Add new comment for : {{ customer.acc_no }} - {{ customer.name }}</p>
<v-textarea v-model="comment" label="Comment"></v-textarea>
</v-card-text>
<v-card-actions>
<v-btn color="blue" :loading="saving" @click="saveComment">Save</v-btn>
<v-spacer></v-spacer>
<v-btn color="grey" @click="$emit('return','cancel')">Cancel</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script>
import axios from 'axios'
export default {
props: {
customer: null
},
data(){
return {
comment: "",
saving: false
}
},
methods: {
saveComment() {
this.saving = true
let url = this.$api_url + "/customers/comments"
axios.put(url, {
acc_no: this.customer.acc_no,
comment: this.comment
})
.then(resp => {
console.log(resp.data)
})
.catch(err => {
console.log(err)
})
.finally(() => {
this.saving = false
this.$emit('return',"saved")
})
}
}
}
</script>

View file

@ -1,156 +0,0 @@
<template>
<v-card :class="{ 'bg-red-lighten-5' : complaint.at_risk }">
<v-card-title>
Complaint Info : {{ complaint.id }}
<v-icon v-if="complaint.active" icon="mdi-play" title="Active"></v-icon>
<v-icon v-if="complaint.at_risk" color="red" icon="mdi-exclamation" title="At Risk"></v-icon>
</v-card-title>
<v-card-subtitle>
{{ complaint.customer.acc_no }} - {{ complaint.customer.name }}<br/>
{{ formatDate(complaint.complaint_date,"DD/MM/YYYY") }}
</v-card-subtitle>
<v-card-text>
Reason: {{ complaint.reason.reason }}<br/>
Driver: {{ complaint.driver.name }}<br/>
Delivery Date {{ formatDate(complaint.delivery_date,"DD/MM/YYYY") }}<br/>
Order : {{ complaint.sop.doc_no }}<br/>
</v-card-text>
<v-card-subtitle>
<v-progress-linear indeterminate :active="info_loading">
</v-progress-linear>
Comments
</v-card-subtitle>
<v-card-text>
{{ complaint.info.comments }}
</v-card-text>
<v-card-subtitle>
Info
</v-card-subtitle>
<v-card-text>
<v-row>
<v-col cols=4>
<v-row dense nogutters>
<v-btn-toggle dark multiple background-color="primary">
<v-btn :active="complaint.info.checks[1]"
:loading="loading[1]"
@click="clickComplaintCheckbox(1)">1</v-btn>
<v-btn :active="complaint.info.checks[2]"
:loading="loading[2]"
@click="clickComplaintCheckbox(2)">2</v-btn>
<v-btn :active="complaint.info.checks[3]"
:loading="loading[3]"
@click="clickComplaintCheckbox(3)">3</v-btn>
</v-btn-toggle>
</v-row>
<v-row dense nogutters>
<v-btn-toggle dark multiple background-color="primary">
<v-btn :active="complaint.at_risk"
:loading="loading[4]"
@click="clickAtRisk">At Risk</v-btn>
<v-btn :active="complaint.info.permanent"
:loading="loading[5]"
@click="clickPermanent">Permanent</v-btn>
</v-btn-toggle>
</v-row>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
</v-card-actions>
</v-card>
</template>
<script>
import Complaint from '@/types/ComplaintType.vue'
import methods from '@/CommonMethods.vue'
import axios from 'axios'
export default {
props: {
in_complaint: new Complaint()
},
watch: {
in_complaint(){
this.complaint = this.in_complaint
}
},
mixins: [methods],
emits: ["return"],
data() {
return {
complaint: this.in_complaint,
info_loading: true,
loading: [false, false, false, false, false, false]
}
},
computed: {
isThreeDone() {
let c = this.complaint.info.checks
if (c[1] && c[2] && c[3]) {
return true
}
return false
}
},
methods:{
getComplaintInfo() {
this.info_loading = true
let url = this.$api_url + "/customers/complaints/" + this.complaint.id + "/info"
console.log("Getting Complaint Info...")
axios.get(url)
.then(resp =>{
this.complaint.info = resp.data
this.info_loading = false
})
},
checkComplaintStatus(){
if (!this.complaint.info.permanent && this.complaint.at_risk && this.isThreeDone) {
console.log("Contract no longer at risk")
this.complaint.at_risk = false
}
},
clickAtRisk(){
this.loading[4] = true
this.complaint.at_risk = !this.complaint.at_risk
if (this.setComplaintStatus(4)) {
this.checkComplaintStatus()
}
},
clickComplaintCheckbox(box) {
this.loading[box] = true
this.complaint.info.checks[box] = !this.complaint.info.checks[box]
if (this.setComplaintStatus(box)) {
this.checkComplaintStatus()
}
},
clickPermanent(){
this.loading[5] = true
this.complaint.info.permanent = !this.complaint.info.permanent
if (this.setComplaintStatus(5)) {
this.checkComplaintStatus()
}
},
setComplaintStatus(idx) {
this.info_loading = true
let url = this.$api_url + "/customers/complaints/" + this.complaint.id + "/info/set"
console.log("Getting Complaint Info...")
axios.post(url, {
checks: JSON.stringify(this.complaint.info.checks),
at_risk: this.complaint.at_risk,
permanent: this.complaint.info.permanent
}).then(resp => {
let stat = resp.data
console.log(stat)
this.getComplaintInfo()
this.info_loading = false
}).catch(err => {
console.log(err)
}).finally(() => {
this.loading[idx] = false
})
return true
},
},
created() {
this.getComplaintInfo()
}
}
</script>

View file

@ -1,113 +0,0 @@
<template>
<v-card style="min-height:200px">
<v-card-title>
Comments
<v-btn v-if="customer.id != ''" color="warning" size="smaller" variant="text" icon="mdi-plus" @click="showAddNote"></v-btn>
<v-btn v-if="customer.id != ''" color="green" size="smaller" variant="text" icon="mdi-refresh" @click="getCustomerComments" :loading="comments_loading"></v-btn>
</v-card-title>
<v-card-text>
<v-progress-linear color="yellow" :active="comments_loading" indeterminate>
</v-progress-linear>
<v-list>
<RecycleScroller
class="scroller small"
item-size="50"
:items="comments"
v-slot="{ item }"
key-field="id">
<v-list-item :class="{ inactive : !item.active }">
{{ item.comment }}
<template v-slot:append>
<v-icon v-if="item.actioned" icon="mdi-tick" color="green" title="Actioned"></v-icon>
<v-btn v-if="item.active" variant="text" :loading="item.active_changing" @click="toggleActiveState(item)" size="small" icon="mdi-play" color="blue" title="Active"></v-btn>
<v-btn v-if="!item.active" variant="text" :loading="item.active_changing" @click="toggleActiveState(item)" size="small" icon="mdi-pause" title="Inactive"></v-btn>
</template>
</v-list-item>
</RecycleScroller>
</v-list>
</v-card-text>
</v-card>
<v-overlay v-model="dialog[0]" contained class="align-center justify-center">
<AddNote :customer="customer" @return="closeAddNote"></AddNote>
</v-overlay>
</template>
<script>
import Customer from '@/types/CustomerType.vue'
import AddNote from '@/components/AddNote.vue'
import axios from 'axios'
export default {
props: {
customer: new Customer()
},
components: {
AddNote
},
data(){
return {
comments_loading: null,
comments: [],
active_changing: false,
dialog: {}
}
},
watch: {
customer() {
this.getCustomerComments()
}
},
methods: {
showAddNote() {
this.dialog[0] = true
},
closeAddNote(e){
if (e == "saved"){
this.getCustomerComments()
}
this.dialog[0] = false
},
getCustomerComments(){
this.comments_loading = true
let url = this.$api_url + "/customers/comments"
axios.get(url,{
params: {
c_id: this.customer.id
}
})
.then(resp => {
this.comments = resp.data
})
.catch(error => (console.log(error)))
.finally(() => {
this.comments_loading = false
})
},
toggleActiveState(cmt){
cmt.active_changing = true
let url = this.$api_url + "/customers/comments/" + cmt.id + "/active"
axios.put(url,{
state: !cmt.active
})
.then(resp => {
console.log(resp.data)
this.getActiveState(cmt)
})
.catch(err => {
console.log(err)
})
},
getActiveState(cmt) {
let url = this.$api_url + "/customers/comments/" + cmt.id + "/active"
axios.get(url)
.then(resp => {
cmt.active = resp.data
})
.catch(err => {
console.log(err)
})
.finally(() => {
cmt.active_changing = false
})
},
}
}
</script>

View file

@ -1,66 +0,0 @@
<template>
<v-card title="Customer Search">
<v-card-text>
<v-text-field label="Search" v-model="customer_search" append-icon="mdi-magnify" @click:append="searchCustomers" @keyup.enter.prevent="searchCustomers"></v-text-field>
<v-progress-linear indeterminate :active="customers_loading">
</v-progress-linear>
<v-list>
<RecycleScroller class="scroller"
:items="filteredCustomers"
:item-size="50"
v-slot="{ item }"
key-field="acc_no"
>
<v-list-item @click="selectCustomer(item)">
{{ item.acc_no }} - {{ item.name }}
</v-list-item>
</RecycleScroller>
</v-list>
</v-card-text>
</v-card>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
customer_search: "",
customers_loading: null,
customers: []
}
},
computed: {
filteredCustomers(){
if (this.customer_search == null){
return []
}
let query = this.customer_search.toLowerCase()
let clist = this.customers.filter(q =>
q.name.toLowerCase().includes(query) ||
q.acc_no.includes(query)
)
return clist
},
},
emits: ['returnCustomer'],
methods: {
selectCustomer(cust){
this.$emit('returnCustomer',cust)
},
searchCustomers() {
this.customers_loading = true
let url = this.$api_url + "/customers/search/" + this.customer_search
axios.get(url)
.then(resp => {
this.customers = resp.data
})
.catch(err => {
console.log(err)
})
.finally(() => {
this.customers_loading = false
})
},
}
}
</script>

View file

@ -1,25 +0,0 @@
<template>
<v-row>
<v-col cols=12>
<v-expansion-panels v-model="debugPanel">
<v-expansion-panel title="Debug Info">
<v-expansion-panel-text>
{{ data }}
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
</template>
<script>
export default {
props: {
data: {}
},
data(){
return {
debugPanel: false
}
}
}
</script>

View file

@ -1,75 +0,0 @@
<template>
<v-card title="Driver Search">
<v-card-text>
<v-text-field label="Search" v-model="driver_search" append-icon="mdi-magnify"></v-text-field>
<v-progress-linear indeterminate :active="drivers_loading">
</v-progress-linear>
<v-list>
<RecycleScroller class="scroller"
:items="filteredDrivers"
:item-size="50"
v-slot="{ item }"
key-field="id"
>
<v-list-item @click="setDriver(item)">
{{ item.name }}
</v-list-item>
</RecycleScroller>
</v-list>
</v-card-text>
</v-card>
</template>
<script>
import axios from 'axios'
export default {
props:{
},
data() {
return {
driver_search: "",
drivers: [],
drivers_loading: null
}
},
emits: ['return'],
created() {
this.allDrivers()
},
computed: {
filteredDrivers() {
let q = this.driver_search.toLowerCase()
if (q == ""){ return this.drivers }
let ms = this.drivers.filter(m =>
m.name.toLowerCase().includes(q)
)
return ms
}
},
methods: {
allDrivers(){
this.drivers_loading = true
console.log("All Drivers...")
let url = this.$api_url + "/drivers"
axios.get(url)
.then(resp => {
this.drivers = resp.data
})
.catch(err => {
console.log(err)
})
.finally(() => {
this.drivers_loading = false
})
},
setDriver(p){
this.$emit('return',p)
}
}
}
</script>
<style scoped>
.scroller {
height:500px;
}
</style>

View file

@ -1,20 +0,0 @@
<template>
<v-row>
<v-list>
<v-list-item v-for="(e, index) in errors" :key="index">
<v-banner color="error" icon="$error">
<v-banner-text>
{{ e }}
</v-banner-text>
</v-banner>
</v-list-item>
</v-list>
</v-row>
</template>
<script>
export default {
props: {
errors: []
}
}
</script>

View file

@ -1,49 +0,0 @@
<template>
<div :class="{ loader: true, fadeout: !isLoading }">
<div class="animate__animated animate__rubberBand animate__infinite animate__slow">
<v-img style="border-radius:5%" height="128" src="/images/cmc-logo.png" /><br/>
</div>
<br/>
<v-progress-circular
color="deep-orange"
indeterminate
></v-progress-circular>
Starting ...
</div>
</template>
<script>
import 'animate.css';
export default {
name: "LoadingScreen",
props: ["isLoading"]
};
</script>
<style>
.loader {
background-color: lightyellow;
bottom: 0;
color: black;
display: block;
font-size: 16px;
left: 0;
overflow: hidden;
padding-top: 10vh;
position: fixed;
right: 0;
text-align: center;
top: 0;
}
.fadeout {
animation: fadeout 1s forwards;
}
@keyframes fadeout {
to {
opacity: 0;
visibility: hidden;
}
}
</style>

View file

@ -1,93 +0,0 @@
<template>
<v-card title="Med Search">
<v-card-text>
<v-text-field label="Search" v-model="med_search" append-icon="mdi-magnify" @click:append="searchMeds" @keyup.enter.prevent="searchMeds"></v-text-field>
<v-progress-linear indeterminate :active="meds_loading">
</v-progress-linear>
<v-list>
<RecycleScroller class="scroller"
:items="filteredMeds"
:item-size="50"
v-slot="{ item }"
key-field="id"
>
<v-list-item @click="setMed(item)">
{{ item.med_code }} : {{ item.name }}
{{ item }}
</v-list-item>
</RecycleScroller>
</v-list>
</v-card-text>
</v-card>
</template>
<script>
import axios from 'axios'
export default {
props:{
},
data() {
return {
med_search: "",
meds: [],
meds_loading: null
}
},
emits: ['returnMed'],
created() {
this.allMeds()
},
computed: {
filteredMeds() {
let q = this.med_search.toLowerCase()
if (q == ""){ return this.meds }
let ms = this.meds.filter(m =>
m.name.toLowerCase().includes(q) ||
m.med_code.toLowerCase().includes(q)
)
return ms
}
},
methods: {
allMeds(){
this.meds_loading = true
console.log("All Meds...")
let url = this.$api_url + "/meds/list"
axios.get(url)
.then(resp => {
this.meds = resp.data
})
.catch(err => {
console.log(err)
})
.finally(() => {
this.meds_loading = false
})
},
searchMeds() {
this.meds_loading = true
console.log("Searching for " & this.med_search)
let url = this.$api_url + "/meds/search/" + this.med_search
axios.get(url)
.then(resp => {
console.log(resp)
this.meds = resp.data
})
.catch(err => {
console.log(err)
})
.finally(() => {
this.meds_loading = false
})
},
setMed(p){
this.$emit('returnMed',p)
}
}
}
</script>
<style scoped>
.scroller {
height:500px;
}
</style>

View file

@ -4,25 +4,7 @@
<v-app-bar-nav-icon v-if="user.logged_in" variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title>{{ site_info.name }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
variant="text"
v-if="user.logged_in"
v-bind="props">
Hi {{ user.first_name }}
</v-btn>
</template>
<v-list theme="dark">
<v-list-item
v-for="(item, index) in user_items"
:key="index"
:title="item.title"
:to="item.value"
>
</v-list-item>
</v-list>
</v-menu>
<v-btn variant="text" v-if="user.logged_in">Hi {{ user.first_name }}</v-btn>
</v-app-bar>
<v-navigation-drawer
v-if="user.logged_in"
@ -46,22 +28,11 @@
</v-list-item>
</template>
</template>
<v-divider></v-divider>
<v-footer>
<v-banner>
<v-banner-text>
<sub>{{ site_info.name }}<br/>
BE v{{ site_info.version }}<br/>
FE v{{ appVersion }}
</sub>
</v-banner-text>
</v-banner>
</v-footer>
</v-list>
</v-navigation-drawer>
</template>
<script>
import { version } from "@/../package.json"
export default{
name: "MyNav",
props: {
@ -78,19 +49,12 @@ export default{
} else {
this.items = []
}
},
},
computed: {
user_items() {
return this.items.filter(x => x.isUserMgmt == true)
}
},
data(){
return {
appVersion: version,
items: [],
drawer: null,
drawer2: false
drawer: null
}
},
created() {
@ -101,6 +65,7 @@ export default{
methods:{
get_menu() {
let items = []
items.push({title: "Dashboard", value:"/about"})
items.push({title: "Customers",
value:"/customers",
children:[{title:"List",
@ -119,7 +84,7 @@ export default{
value:"/sop/printed"}]
})
items.push({title: "Logout", value:"/logout", isUserMgmt: true})
items.push({title: "Logout", value:"/logout"})
return items
}
}

View file

@ -1,19 +1,17 @@
<template>
<v-container class="button-container">
<v-btn :loading="generating" color="primary" @click="generatePdf()" class="mr-2">Create PDF</v-btn>
<v-btn color="grey" v-print="printObj">Print</v-btn>
<v-btn color="primary" @click="generatePdf()" class="mr-2">Create PDF</v-btn>
<v-btn color="grey" v-print="printObj">Print</v-btn>
</v-container>
</template>
<script>
import html2pdf from 'html2pdf.js';
export default {
props: {
scope: String,
filename: String
scope: String
},
data() {
return {
generating: false,
printObj: {
id: this.scope,
previewTitle: "Report Print",
@ -22,16 +20,8 @@ export default {
}
},
methods:{
generatePdf() {
this.generating = true
setTimeout(() => {
let el = document.getElementById(this.scope)
let opts = {
filename: (this.filename || 'file' ) + ".pdf"
}
html2pdf().set(opts).from(el).save()
this.generating = false
},500)
async generatePdf() {
html2pdf(document.getElementById(this.scope))
}
}
}

View file

@ -1,64 +0,0 @@
<template>
<v-card title="Product Search">
<v-card-text>
<v-text-field label="Search" v-model="product_search" append-icon="mdi-magnify" @click:append="searchProducts" @keyup.enter.prevent="searchProducts"></v-text-field>
<v-progress-linear indeterminate :active="products_loading">
</v-progress-linear>
<v-list>
<RecycleScroller class="scroller"
:items="products"
:item-size="50"
v-slot="{ item }"
key-field="code"
>
<v-list-item @click="setProduct(item)">
{{ item.code }} - {{ item.name }}
</v-list-item>
</RecycleScroller>
</v-list>
</v-card-text>
</v-card>
</template>
<script>
import axios from 'axios'
export default {
props:{
},
data() {
return {
product_search: "",
products: [],
products_loading: null
}
},
emits: ['returnProduct'],
methods: {
searchProducts() {
this.products_loading = true
console.log("Searching for " & this.product_search)
let url = this.$api_url + "/products/search/" + this.product_search
axios.get(url)
.then(resp => {
console.log(resp)
this.products = resp.data
})
.catch(err => {
console.log(err)
})
.finally(() => {
this.products_loading = false
})
},
setProduct(p){
this.$emit('returnProduct',p)
}
}
}
</script>
<style scoped>
.scroller {
height:500px;
}
</style>

View file

@ -1,152 +0,0 @@
<template>
<v-card>
<v-card-title>
{{ doc_types[doc_status] }} Orders - {{ customer.acc_no }} {{ customer.name }}
<v-btn v-if="customer.id != ''" size="smaller" icon="mdi-refresh" variant="text" color="green" @click="getCustomerRecentOrders" :loading="orders_loading"></v-btn>
</v-card-title>
<v-progress-linear color="blue" :active="orders_loading" indeterminate>
</v-progress-linear>
<v-container>
<v-row dense>
<v-col cols=12>
<RecycleScroller :items="orders"
class="scroller"
:class="{ small : size_small }"
:item-size="180"
key-field="doc_no"
v-slot="{ item }">
<v-card class="ma-2" density="compact" :class="{ 'bg-green-lighten-5' : item.doc_status == 'Live' }">
<v-row dense>
<v-col>
<v-card-title>
Order: {{ item.doc_no }}
</v-card-title>
<v-card-subtitle>
Order Date : {{ formatDate(item.doc_date,"DD/MM/YYYY") }}<br/>
Delivery Date : {{ formatDate(item.req_del_date,"DD/MM/YYYY") }}<br/>
<v-icon color="blue" v-if="item.doc_status == 'Completed'" icon="mdi-check"></v-icon>
<v-icon v-if="item.doc_status == 'Live'" icon="mdi-play"></v-icon>
{{ item.doc_status }}
<br/>
<v-icon color="blue-lighten-1" v-if="item.print_status == 'Printed'" icon="mdi-printer"></v-icon>
<v-icon v-if="item.print_status == 'Not printed'" icon="mdi-printer-off"></v-icon>
{{ item.print_status }}
</v-card-subtitle>
<v-card-text>
{{ item.customer.acc_no }} - {{ item.customer.name }}
</v-card-text >
</v-col>
<v-col>
<v-card-text >
<!--<h5>Address :</h5>-->
<template v-if="item.del_addr.id != 0 && item.del_addr.postal_name != ''">
<h5>Delivery Address :</h5>
<template v-for="(v, k , index) in item.del_addr" :key="index">
<span class="text-caption" v-if="k != 'id' && (v != '' || v != 0)">
{{ v }}<br/>
</span>
</template>
</template>
</v-card-text>
</v-col>
<v-col cols=5>
<v-card-text style="max-height:160px;overflow-y:scroll;">
<h5>Items :</h5>
<span class="text-caption" v-for="(i, index) in item.products" :key="index" style="border-bottom: 1px dotted #000;">
{{ i.code }} - {{ i.name }}
<span v-if="i.quantity != 0">x {{ i.quantity }}</span><br/>
</span>
</v-card-text>
</v-col>
<v-col cols=1>
<v-btn size="small" color="warning" title="Add Complaint">
<v-icon>mdi-plus</v-icon>
<v-icon>mdi-exclamation</v-icon>
</v-btn>
</v-col>
</v-row>
</v-card>
</RecycleScroller>
</v-col>
</v-row>
</v-container>
</v-card>
</template>
<script>
import axios from 'axios'
import Customer from '@/types/CustomerType.vue'
import methods from '@/CommonMethods.vue'
export default {
props:{
customer: new Customer(),
doc_status: Number,
limit: Number,
size_small: Boolean
},
watch: {
customer() {
this.getCustomerRecentOrders()
},
},
mixins: [methods],
data() {
return {
orders_loading: false,
orders: [],
doc_types: ["Live","","Completed"],
}
},
computed: {
prog_col(){
if (this.doc_status == 2){
return "green"
} else {
return "blue"
}
},
sortedOrders() {
let sorted = this.orders
sorted.sort((a, b) => {
if (a.doc_date < b.doc_date){
return 1
}
if (a.doc_date > b.doc_date){
return -1
}
return 0
})
return sorted
}
},
methods: {
getCustomerRecentOrders(){
this.orders_loading = true
let url = this.$api_url + "/customers/" + this.customer.id + "/orders/recent"
axios.get(url, {
params: {
doc_status: this.doc_status,
limit: this.limit || 6
}
})
.then(resp => {
this.orders = resp.data
})
.catch(err => {
console.log(err)
})
.finally(() => {
this.orders_loading = false
})
}
},
created() {
if (this.customer.id != "") {
this.getCustomerRecentOrders()
}
}
}
</script>

View file

@ -1,8 +1,8 @@
<template>
<PrintButtons :scope="scope" :filename="filename" />
<PrintButtons :scope="scope" />
<v-container>
<v-card class="a4 page">
<div :id="scope" class="pdf-scope" >
<div :id="scope" class="pdf-scope">
<slot></slot>
</div>
</v-card>
@ -10,11 +10,9 @@
</template>
<script>
import PrintButtons from './PrintButtons.vue'
import '@/assets/css/reports.css';
export default {
props: {
scope: String,
filename: String
scope: String
},
components: {
PrintButtons

View file

@ -1,73 +0,0 @@
<template>
<v-card title="Vet Search">
<v-card-text>
<v-text-field label="Search" v-model="vet_search" append-icon="mdi-magnify" @click:append="searchVets" @keyup.enter.prevent="searchVets"></v-text-field>
<v-progress-linear indeterminate :active="vets_loading">
</v-progress-linear>
<v-list>
<RecycleScroller class="scroller"
:items="vets"
:item-size="50"
v-slot="{ item }"
key-field="id"
>
<v-list-item @click="setVet(item)">
{{ item.practice }}
<v-tooltip activator="parent"
location="end">
{{ item.practice }}
<template v-for="(c,index) in item.contacts" :key="index">
<template v-if="c != ''">
{{ c }}<br/>
</template>
</template>
</v-tooltip>
</v-list-item>
</RecycleScroller>
</v-list>
</v-card-text>
</v-card>
</template>
<script>
import axios from 'axios'
export default {
props:{
},
data() {
return {
vet_search: "",
vets: [],
vets_loading: null
}
},
emits: ['returnVet'],
methods: {
searchVets() {
this.vets_loading = true
console.log("Searching for " & this.vet_search)
let url = this.$api_url + "/vets/search/" + this.vet_search
axios.get(url)
.then(resp => {
console.log(resp)
this.vets = resp.data
})
.catch(err => {
console.log(err)
})
.finally(() => {
this.vets_loading = false
})
},
setVet(p){
this.$emit('returnVet',p)
}
}
}
</script>
<style scoped>
.scroller {
height:500px;
}
</style>

View file

@ -10,8 +10,6 @@ import './assets/css/app.scss'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import axios from 'axios'
import VueVirtualScroller from 'vue-virtual-scroller'
import DebugPanel from '@/components/DebugPanel.vue'
import ErrorBanner from '@/components/ErrorBanner.vue'
axios.defaults.headers.common['X-Authentication'] = `Bearer ${localStorage.getItem('access_token')}`;
@ -21,8 +19,6 @@ const app = createApp(App).use(router)
.use(vuetify)
.use(print)
.use(VueVirtualScroller)
.use(DebugPanel)
.use(ErrorBanner)
.component('DatePicker', Datepicker)
var url = window.location.protocol + "//" + window.location.host + "/api/v1"

View file

@ -1,17 +0,0 @@
<script>
import axios from 'axios'
export default {
methods: {
customerSearch(query) {
let url = this.$api_url + "/customers/search/" + query
axios.get(url)
.then(resp => {
return resp.data
})
.catch(err => {
console.log(err)
})
}
}
}
</script>

View file

@ -1,4 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const AboutView = () => import('../views/AboutView.vue')
const LoginPage = () => import('../views/LoginPage.vue')
const LogOut = () => import('../components/LogOut.vue')
@ -6,14 +7,13 @@ const CustomerList = () => import('../views/customers/CustomerList.vue')
const ContractList = () => import('../views/contracts/ContractList.vue')
const ComplaintsList = () => import('../views/complaints/ComplaintsList.vue')
const MedFeedsList = () => import('../views/medfeeds/MedFeedsList.vue')
const OrderList = () => import('../views/salesorders/OrdersList.vue')
const SOPPrintedList = () => import('../views/salesorders/SOPPrinted.vue')
const routes = [
{
path: '/',
name: 'home',
component: CustomerList
component: HomeView
},
{
path: '/about',
@ -40,41 +40,16 @@ const routes = [
name: 'contractlist',
component: ContractList
},
{
path: '/customers/contracts/list/:id',
name: 'contractlistid',
component: ContractList
},
{
path: '/customers/medicated-feeds/list',
name: 'medfeedslist',
component: MedFeedsList
},
{
path: '/customers/medicated-feeds/list/:id',
name: 'medfeedslistid',
component: MedFeedsList
},
{
path: '/customers/complaints/list',
name: 'complaintslist',
component: ComplaintsList
},
{
path: '/customers/complaints/list/:id',
name: 'complaintslistid',
component: ComplaintsList
},
{
path: '/customers/orders/list',
name: 'orderslist',
component: OrderList
},
{
path: '/customers/orders/list/:id',
name: 'orderslistid',
component: OrderList
},
{
path: '/sop/printed',
name: 'sopprinted',

View file

@ -1,7 +0,0 @@
<script>
export default class ComplaintInfo {
checks = [false, false, false, false]
permanent = false
comments = ""
}
</script>

View file

@ -1,16 +0,0 @@
<script>
import Customer from '@/types/CustomerType.vue'
import ComplaintInfo from '@/types/ComplaintInfoType.vue'
export default class Complaint {
id = 0
complaint_date = new Date()
delivery_date = ""
reason = ""
sop = {}
at_risk = false
driver = ""
info = new ComplaintInfo()
customer = new Customer()
}
</script>

View file

@ -1,18 +0,0 @@
<script>
import Customer from '@/types/CustomerType.vue';
export default class Contract {
no = "";
customer = new Customer();
terms = "";
products = [];
start_date = new Date();
finish_date = new Date();
agree_date = "";
tonnage_per_month = 1;
total_tonnage = 1;
comments = "";
office_comments = "";
active = true;
isNew = true;
}
</script>

View file

@ -1,19 +0,0 @@
<script>
export default class CustomerAddress {
id = "";
description = "";
contract = "";
postal_name = "";
line_1 = "";
line_2 = "";
line_3 = "";
line_4 = "";
city = "";
county = "";
postcode = "";
country = "";
email = "";
faxno = "";
telephone = "";
}
</script>

View file

@ -1,13 +0,0 @@
<script>
import CustomerAddress from '@/types/CustomerAddressType.vue'
export default class Customer {
id = "";
acc_no = "";
name = "";
short_name = "";
full_title = "";
at_risk = false;
address = new CustomerAddress();
notes = {}
}
</script>

View file

@ -1,6 +0,0 @@
<script>
export default class Error {
id = "";
msg = "";
}
</script>

View file

@ -1,23 +0,0 @@
<script>
import Medication from '@/types/MedicationType.vue'
import Customer from '@/types/CustomerType.vue'
import CustomerAddress from '@/types/CustomerAddressType.vue'
import Product from '@/types/ProductType.vue'
import Vet from '@/types/VetType.vue'
export default class MedicatedFeed {
id = 0;
med_feed_id = 0;
medication = new Medication();
customer = new Customer();
product = new Product();
tonnage = 0;
vet = new Vet();
date_required = "";
delivery_address = new CustomerAddress();
alt_adds = [];
repeat = false;
repeat_message = "";
current = true;
isNew = true;
}
</script>

View file

@ -1,9 +0,0 @@
<script>
export default class Medication {
id = 0;
name = "";
info = [];
med_code = "";
inclusion_rate = "";
}
</script>

View file

@ -1,7 +0,0 @@
<script>
export default class Product {
code = "";
name = "";
price = "";
}
</script>

View file

@ -1,7 +0,0 @@
<script>
export default class Vet {
id = 0;
practice = "";
contacts = [];
}
</script>

View file

@ -1,23 +1,14 @@
<template>
<v-card width="500" min-height="350"
<v-card width="500" height="300"
class="mx-auto my-12"
id="login-box"
title="Welcome!"
subtitle="Please Log In">
<v-form :disabled="!my_site_info.backend_connected">
<v-form>
<v-responsive class="mx-auto" max-width="475">
<v-text-field label="Username"
@keyup.enter="submitLogin"
clearable
v-model="user.email"></v-text-field>
<v-text-field label="Password"
@keyup.enter="submitLogin"
v-model="user.password" clearable
type="password"></v-text-field>
<v-btn :disabled="!my_site_info.backend_connected" color="blue" :loading="logging_in" @click="submitLogin">Login</v-btn>
<v-banner v-if="show_error" color="error" icon="mdi-exclamation-thick" theme="dark">
<v-banner-text>{{ error_message }}</v-banner-text>
</v-banner>
<v-text-field label="Email" clearable v-model="user.email"></v-text-field>
<v-text-field label="Password" v-model="user.password" clearable
type="password"></v-text-field>
<v-btn color="blue" @click="submitLogin">Login</v-btn>
</v-responsive>
</v-form>
</v-card>
@ -27,35 +18,18 @@
import axios from 'axios'
export default {
props: {
site_info: {}
},
name: 'LoginPage',
data() {
return {
my_site_info: this.site_info,
user: {
email: "",
password: ""
},
show_error: false,
error_message: "",
error_count: 0,
logging_in: false
}
},
watch: {
'site_info.backend_connected'(new_val) {
console.log(new_val)
this.my_site_info.backend_connected = new_val
}
}
},
methods: {
submitLogin() {
this.error_message = ""
this.show_error = false
let url = this.$api_url + "/users/login"
this.logging_in = true
console.log("Logging in...")
axios
.post(url, {
@ -64,7 +38,6 @@ export default {
})
.then(resp => {
let data = resp.data
console.log(data)
if (data.logged_in) {
let token = data.token
localStorage.setItem('access_token', token.content)
@ -74,34 +47,10 @@ export default {
console.log("Logged in")
window.location.href = '/'
}
} else {
this.error_message = "Login failed. Invalid username or password."
this.error_count += 1
}
})
.catch(error => {
console.log(error)
this.error_message = "Login failed. Invalid username or password."
this.error_count += 1
})
.finally(() => {
setTimeout(() => {
if (this.error_message != ""){
this.show_error = true
}
this.logging_in = false
if (this.error_count > 2) {
let box = document.getElementById("login-box")
box.classList.add("animate__animated")
box.classList.add("animate__hinge")
setTimeout(() => {
box.classList.add("animate__fadeInUp")
box.classList.remove("animate__hinge")
},5000)
this.error_count = 0
}
},2000)
})
.catch(error => { console.log(error) })
}
}
}

View file

@ -1,175 +0,0 @@
<template>
<v-card :title="title">
<v-card-subtitle>
Complaint : {{ complaint.id }}
</v-card-subtitle>
<v-card-text>
<v-container>
<v-row>
<v-col cols=6>
<v-text-field readonly variant="outlined" prepend-inner-icon="mdi-magnify" @click="showCustomerSearch" label="Customer" :model-value="complaint.customer.acc_no + ' - ' + complaint.customer.name">
</v-text-field>
<label>
Complaint Date :
<DatePicker v-model="complaint.complaint_date" format="dd/MM/yyyy" />
</label>
</v-col>
<v-col cols=6>
<v-text-field label="Sales Order Number" v-model="complaint.sop.doc_no" variant="outlined"></v-text-field>
<v-select label="Reason" v-model="complaint.reason" :items="reasons" item-title="reason" item-value="id" return-object variant="outlined"></v-select>
<v-text-field readonly prepend-inner-icon="mdi-magnify" label="Driver" v-model="complaint.driver.name" variant="outlined" @click="showDriverSearch"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols=12>
<v-progress-linear :active="info_loading" indeterminate></v-progress-linear>
<v-textarea variant="outlined" label="Comments" v-model="complaint.info.comments">
</v-textarea>
</v-col>
</v-row>
</v-container>
</v-card-text>
<DebugPanel :data="complaint"></DebugPanel>
<ErrorBanner :errors="errors" />
<v-card-actions>
<v-btn v-if="!complaint.isNew" color="red-darken-1"
variant="text"
:loading="saving"
@click="saveComplaint(selected_complaint)">Save</v-btn>
<v-btn v-if="complaint.isNew" color="red-darken-1"
variant="text"
:loading="saving"
@click="saveComplaint(selected_complaint)">Add</v-btn>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1"
variant="text"
@click="close">Close</v-btn>
</v-card-actions>
</v-card>
<v-dialog v-model="search[0]" scrollable>
<CustomerSearch @returnCustomer="setCustomer"></CustomerSearch>
</v-dialog>
<v-dialog v-model="search[1]">
<DriverSearch @return="setDriver"></DriverSearch>
</v-dialog>
</template>
<script>
import axios from 'axios'
import methods from '@/CommonMethods.vue'
import DatePicker from '@vuepic/vue-datepicker'
import Complaint from '@/types/ComplaintType.vue'
import CustomerSearch from '@/components/CustomerSearch.vue'
import DriverSearch from '@/components/DriverSearch.vue'
export default {
props: {
setcomplaint: new Complaint()
},
components: {
CustomerSearch,
DriverSearch,
DatePicker
},
watch: {
setcomplaint(newval) {
this.complaint = newval
},
},
mixins: [methods],
data() {
return {
complaint: this.setcomplaint,
saving: false,
info_loading: false,
search: [],
customer_search: null,
customers_loading: false,
customers: [],
errors: [],
reasons: [],
debugPanel: true
}
},
computed: {
title() {
if ( this.complaint.isNew ) {
return "New Complaint"
} else {
return "Edit Complaint"
}
}
},
emits: ['closetab','complaintupdate'],
methods: {
close() {
this.$emit('closetab')
},
showCustomerSearch() {
this.search[0] = true
},
showDriverSearch() {
this.search[1] = true
},
async saveComplaint(){
this.errors = []
this.saving = true
let url = this.$api_url + "/customers/complaints/" + this.complaint.id + "/save"
if (this.complaint.isNew) {
url = this.$api_url + "/customers/complaints/add"
}
axios.post(url, {
complaint: this.complaint
}).then(resp => {
console.log("Saved Complaint : " + JSON.stringify(resp.data))
this.saving = false
let stat = resp.data
if (stat.status == true ) {
if (this.complaint.isNew) {
this.$emit('complaintupdate', resp.data)
} else {
this.$emit('complaintupdate', resp.data)
}
} else {
this.errors.push("Complaint not saved.")
console.log("Not Saved")
}
}).catch(err => {
console.log(err)
this.saving = false
})
},
setCustomer(c){
this.complaint.customer = c
this.search[0] = false
},
setDriver(d) {
this.complaint.driver = d
this.search[1] = false
},
getComplaintInfo(){
this.info_loading = true
let url = this.$api_url + "/customers/complaints/" + this.complaint.id + "/info"
console.log("Getting Complaint Info...")
axios.get(url)
.then(resp =>{
this.complaint.info = resp.data
}).finally(() => {
this.info_loading = false
})
},
getComplaintReasons() {
let url = this.$api_url + "/customers/complaints/reasons"
axios.get(url).
then(resp => {
this.reasons = resp.data
})
}
},
created() {
if (!this.complaint.isNew) {
this.getComplaintInfo()
}
this.getComplaintReasons()
}
}
</script>

View file

@ -1,88 +1,119 @@
<template>
<h3>Complaints</h3>
<v-tabs v-model="tab" >
<v-tab title="List" value="isList"></v-tab>
<v-tab title="Edit" value="edit" v-if="edit"></v-tab>
<v-tab title="Report" value="report" v-if="report" ></v-tab>
<v-tabs v-model="tab" fixed-tabs>
<v-tab title="List" v-model="list" />
<v-tab title="Edit" v-model="edit" v-if="edit" />
<v-tab title="Report" v-model="report" v-if="report" />
</v-tabs>
<v-window v-model="tab">
<v-window-item value="isList">
<v-col cols="12" xs="12" sm="12" md="12" lg=8>
<v-text-field label="Search"
variant="outlined"
v-model="searchQuery"
density="compact"
append-inner-icon="mdi-magnify"></v-text-field>
<v-row justify="space-between">
<v-col cols=4>
<v-btn v-if="site_info.features.addcomplaint" color="warning" prepend-icon="mdi-plus" variant="text" @click="showAddComplaint">Add</v-btn>
</v-col>
<v-col cols=3>
<v-btn align="end" variant="text" @click="showActive = !showActive">
Showing <span v-if="showActive">Active Only</span><span v-else>Inactive</span>
</v-btn>
</v-col>
</v-row>
<v-progress-linear indeterminate color="orange" :active="loading"></v-progress-linear>
<v-card variant="outlined">
<RecycleScroller class="scroller"
:items="filteredComplaints"
:item-size="100"
v-slot="{ item }"
key-field="id">
<v-row dense class="item" :class="{ 'bg-red-lighten-4' : item.at_risk }">
<v-col cols="4">
Complaint : {{ item.id }}<br/>
<span class="text-caption">
Complaint Date : {{ item.complaint_date }}<br/>
Sales Order: {{ item.sop.doc_no }}<br/>
</span>
</v-col>
<v-col cols="4" class="text-body-2">
{{ item.customer.acc_no }} - {{ item.customer.name }}<br/>
Reason : {{ item.reason.reason }}<br/>
Driver : {{ item.driver.name }}
</v-col>
<v-col cols=4>
<div class="d-flex justify-space-around align-center flex-column flex-sm-row fill-height">
<v-row>
<v-icon icon="mdi-exclamation" v-if="item.at_risk" color="red" title="At Risk"></v-icon>
<v-icon icon="mdi-play" v-if="item.active" title="Active"></v-icon>
</v-row>
<v-btn @click="showInfo(item)" color="blue-lighten-1">View</v-btn>
<v-btn v-if="site_info.features.editcomplaint" color="orange-lighten-2" @click="showEditComplaint(item)">Edit</v-btn>
</div>
</v-col>
</v-row>
</RecycleScroller>
</v-card>
</v-col>
</v-window-item>
<v-window-item value="edit">
<ComplaintEdit ref="edit" :setcomplaint="selected_complaint" @closetab="tab = 'isList'" @complaintupdate="complaintUpdated"></ComplaintEdit>
<v-window-item v-model="list">
<v-responsive
max-width="500"
>
<v-text-field
clearable
label="Search"
variant="outlined"
v-model="searchQuery"
density="compact"
append-inner-icon="mdi-magnify"></v-text-field>
<v-switch color="blue" label="Show Only Active" v-model="showActive"></v-switch>
<v-btn v-if="site_info.features.addcomplaint" color="warning">+ Add</v-btn>
</v-responsive>
<v-table>
<thead>
<tr>
<th>Comp No</th>
<th>Date</th>
<th>Order</th>
<th>Acc No</th>
<th>Name</th>
<th>Reason</th>
<th>Active</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan=7>
<img class="loading" src="/images/icons/loading.gif"/>
</td>
</tr>
<template v-for="complaint in filteredComplaints" :key="complaint.id">
<tr :class="{ at_risk : complaint.at_risk, cust_at_risk: complaint.customer.at_risk }">
<td>{{ complaint.id }}</td>
<td>{{ complaint.complaint_date }}</td>
<td>{{ complaint.sop.doc_no }}</td>
<td>{{ complaint.customer.acc_no }}</td>
<td>{{ complaint.customer.name }}
</td>
<td>{{ complaint.reason }}</td>
<td>
<img class="icon" v-if="complaint.active" v-bind:alt="complaint.active" v-bind:title="complaint.active" src="/images/icons/Live.png"/>
</td>
<td>
<v-btn @click="showInfo(complaint)">
<span v-if="complaint.info_shown">
Hide
</span>
<span v-else>
View
</span>
</v-btn>
</td>
</tr>
<tr v-if="complaint.info_shown">
<template v-if="complaint.info">
<template v-if="complaint.info_loaded">
<td colspan=4>
{{ complaint.info.comments }}
<br />
<sub>- {{ complaint.info.added_by }}</sub>
</td>
<td colspan=2>
<v-container>
<v-form :disabled="complaint.info.loading">
<v-row dense nogutters>
<v-checkbox label="1" type="checkbox" v-model="complaint.info.one" @change="changeComplaintCheckbox(complaint)" />
<v-checkbox label="2" v-model="complaint.info.two" @change="changeComplaintCheckbox(complaint)"/>
<v-checkbox label="3" v-model="complaint.info.three" @change="changeComplaintCheckbox(complaint)"/>
</v-row>
<v-row dense nogutters>
<v-checkbox label="At Risk" v-model="complaint.at_risk" @change="changeComplaintCheckbox(complaint)" />
<v-checkbox label="Permanent" v-model="complaint.info.permanent" @change="changeComplaintCheckbox(complaint)" />
</v-row>
</v-form>
</v-container>
</td>
<td>
<img v-if="complaint.info.loading" class="loading" src="/images/icons/loading.gif"/>
<template v-else>
<v-btn v-if="site_info.features.editcomplaint" color="warning">Edit</v-btn>
</template>
</td>
</template>
</template>
<template v-else>
<td colspan=6>
<img class="loading" src="/images/icons/loading.gif"/>
</td>
</template>
</tr>
</template>
</tbody>
</v-table>
</v-window-item>
</v-window>
<v-dialog v-model="showComplaintInfo">
<ComplaintInfo :in_complaint="selected_complaint" @return="doInfoReturn" />
</v-dialog>
</template>
<script>
import axios from 'axios'
import Complaint from '@/types/ComplaintType.vue'
import ComplaintInfo from '@/components/ComplaintInfo.vue'
import ComplaintEdit from '@/views/complaints/ComplaintEdit.vue'
export default {
props: {
site_info:{},
user_info:{}
},
components: {
ComplaintInfo,
ComplaintEdit
},
data() {
return {
tab: "isList",
tab: "list",
list: [],
listreceived: false,
showActive: true,
@ -90,9 +121,7 @@ export default {
limit: 300,
searchQuery: "",
edit: false,
report: false,
showComplaintInfo: false,
selected_complaint: {}
report: false
}
},
computed: {
@ -105,7 +134,7 @@ export default {
q.customer.name.toLowerCase().includes(query) ||
q.customer.acc_no.includes(query) ||
q.id == query ||
q.reason.reason.toLowerCase().includes(query)
q.reason.toLowerCase().includes(query)
)
if (this.showActive) {
clist = clist.filter(q =>
@ -116,65 +145,64 @@ export default {
}
},
methods: {
getComplaintsList(){
async getComplaintsList(){
this.loading = true
let url = this.$api_url + "/customers/complaints/list"
let c_id = this.$route.params.id || ""
console.log("Getting Contracts list..." + c_id)
console.log("Getting Complaint list...")
axios.get(url,{
params: {
limit: this.limit,
query: this.searchQuery,
c_id: c_id
query: this.searchQuery
}
}).then(resp => {
this.list = resp.data
this.listreceived = true
}).catch(err => {
console.log(err)
}).finally(() => {
this.loading = false
this.listreceived = true
})
},
showInfo(complaint) {
this.showComplaintInfo = true
this.selected_complaint = complaint
async getComplaintInfo(complaint) {
complaint.info.loading = true
let url = this.$api_url + "/customers/complaints/" + complaint.id + "/info"
console.log("Getting Complaint Info...")
axios.get(url)
.then(resp =>{
complaint.info = resp.data
complaint.info.loading = false
})
},
async showInfo(complaint) {
console.log(complaint.id)
complaint.info_loaded = false
complaint.info_shown = !complaint.info_shown
if (complaint.info_shown) {
complaint.info = await this.getComplaintInfo(complaint)
complaint.info_loaded = true
}
},
doInfoReturn(code) {
console.log(code)
async changeComplaintCheckbox(complaint) {
let set = await this.setComplaintStatus(complaint)
if (complaint.at_risk && set && complaint.info.one && complaint.info.two && complaint.info.three) {
console.log("Contract no longer at risk")
complaint.at_risk = false
}
},
showAddComplaint() {
this.selected_complaint = new Complaint()
this.selected_complaint.isNew = true
this.edit = true
this.tab = "edit"
async setComplaintStatus(complaint) {
complaint.info.loading = true
let url = this.$api_url + "/customers/complaints/" + complaint.id + "/info/set"
console.log("Getting Complaint Info...")
axios.post(url, {
one: complaint.info.one,
two: complaint.info.two,
three: complaint.info.three,
at_risk: complaint.at_risk,
permanent: complaint.info.permanent
}).then(resp => {
console.log(resp.data)
this.getComplaintInfo(complaint)
complaint.info_loaded = true
})
return true
},
showEditComplaint(cmp) {
this.selected_complaint = cmp
this.selected_complaint.isNew = false
this.edit = true
this.tab = "edit"
},
complaintUpdated(cmp) {
console.log(cmp)
this.tab = "isList"
}
}
}
</script>
<style>
.scroller {
height:600px;
}
.item {
height: 100px;
overflow-y:hidden;
padding: 0 1em;
margin-bottom:2px;
display: flex;
align-items: center;
border-bottom: 1px solid #000;
}
</style>

View file

@ -1,17 +1,16 @@
<template>
<v-card :title="title" :subtitle="'Contract : ' + contract.no">
<v-card title="Edit Contract" :subtitle="'Contract : ' + contract.no">
<v-card-text>
<v-container>
<v-row>
<v-col cols="6">
<v-text-field readonly variant="outlined" prepend-inner-icon="mdi-magnify" @click="showCustomerSearch" label="Customer" :model-value="contract.customer.acc_no + ' - ' + contract.customer.name">
</v-text-field>
<v-text-field type="number" variant="outlined" label="Tonnage Per Month" v-model="contract.tonnage_per_month"></v-text-field>
<v-text-field label="Customer" v-model="contract.customer.name" readonly></v-text-field>
<v-text-field type="number" label="Tonnage Per Month" v-model="contract.tonnage_per_month"></v-text-field>
</v-col>
<v-col cols="6">
<label>
Start Date
<DatePicker v-model="contract.start_date" format="dd/MM/yyyy" />
<DatePicker v-model="contract.start_date" format="dd/MM/yyyy" />
</label><br />
<label>
Finish Date
@ -19,36 +18,33 @@
</label><br />
</v-col>
</v-row>
<v-table density="compact">
<thead>
<tr>
<th style="width:65%;">
Product
</th>
<th style="width:20%">
Price
</th>
<th>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(p, index) in contract.products" :key="index">
<td>
<v-text-field readonly variant="outlined" prepend-inner-icon="mdi-magnify" @click="showProductSearch(index)" :model-value="p.code + ' - ' + p.name"></v-text-field>
</td>
<td>
<v-text-field prepend-icon="mdi-currency-gbp" type="number" density="compact" variant="outlined" v-model="p.price"></v-text-field>
</td>
<td>
<v-btn size="small" color="error" title="Remove" variant="plain" @click="removeProduct(p.code)" icon="mdi-minus"></v-btn>
</td>
</tr>
</tbody>
</v-table>
<template v-for="p in contract.products" :key="p.code">
<v-row>
<v-col cols="6">
<v-autocomplete v-model="p.code"
v-model:search="product_search"
:loading="products_loading"
:items="products"
cache-items
hide-no-data
hide-details
solo-inverted
label="Code"
no-data-text="No Products Found"
item-title="code"
item-value="code" ></v-autocomplete>
</v-col>
<v-col cols="4">
<v-text-field type="number" label="Price" v-model="p.price"></v-text-field>
</v-col>
<v-col cols="2">
<v-btn size="small" color="error" title="Remove" variant="plain" icon @click="contract.products.pop()"><v-icon>mdi-minus</v-icon></v-btn>
</v-col>
</v-row>
</template>
<v-row>
<v-col cols="4">
<v-btn @click="addProduct()" v-if="contract.products.length < 4">+ Add Product</v-btn>
<v-btn @click="contract.products.push({})" v-if="contract.products.length < 4">+ Add Product</v-btn>
</v-col>
</v-row>
<v-row>
@ -59,145 +55,107 @@
</label>
</v-col>
<v-col cols="12">
<v-textarea rows=3 label="Comments" variant="outlined" v-model="contract.comments"></v-textarea>
<v-textarea rows=3 label="Office Comments" variant="outlined" v-model="contract.office_comments"></v-textarea>
<v-textarea rows=3 label="Comments" v-model="contract.comments"></v-textarea>
<v-textarea rows=3 label="Office Comments" v-model="contract.office_comments"></v-textarea>
</v-col>
</v-row>
</v-container>
</v-card-text>
<DebugPanel :data="contract"></DebugPanel>
<ErrorBanner :errors="errors" />
<v-card-actions>
<v-btn v-if="!contract.isNew" color="red-darken-1"
variant="text"
:loading="saving"
@click="saveContract(selected_contract)">Save</v-btn>
<v-btn v-if="contract.isNew" color="red-darken-1"
variant="text"
:loading="saving"
@click="saveContract(selected_contract)">Add</v-btn>
<v-btn color="red-darken-1"
variant="text"
@click="saveContract(selected_contract)">Save</v-btn>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1"
variant="text"
@click="close">Close</v-btn>
</v-card-actions>
</v-card>
<v-dialog v-model="search[0]" scrollable>
<CustomerSearch @returnCustomer="setCustomer"></CustomerSearch>
</v-dialog>
<v-dialog v-model="search[1]">
<ProductSearch @returnProduct="setProduct"></ProductSearch>
</v-dialog>
</template>
<script>
import axios from 'axios'
import DatePicker from '@vuepic/vue-datepicker'
import ErrorBanner from '@/components/ErrorBanner.vue'
import methods from '@/CommonMethods.vue'
import Product from '@/types/ProductType.vue'
import ProductSearch from '@/components/ProductSearch.vue'
import CustomerSearch from '@/components/CustomerSearch.vue'
export default {
props: {
setcontract: {}
setcontract: {
no: Number,
customer: {
acc_no: String,
name: String,
at_risk: Boolean,
},
products: [{
code: String,
name: String,
price: Number,
}],
start_date: String,
finish_date: String,
agree_date: String,
tonnage_per_month: Number,
comments: String,
office_comments: String,
active: Boolean
}
},
components: {
DatePicker,
ErrorBanner,
ProductSearch,
CustomerSearch
DatePicker
},
watch: {
setcontract(newval) {
this.contract = newval
},
product_search(val) {
if (val && val.length > 1) {
this.searchProducts(val)
}
}
},
mixins: [methods],
data() {
return {
contract: this.setcontract,
dialog: this.opendialog,
saving: false,
search: [],
searchProdIndex: null,
product_search: null,
products_loading: false,
products: [],
customer_search: null,
customers_loading: false,
customers: [],
errors: []
}
},
computed: {
title() {
if ( this.contract.isNew ) {
return "New Contract"
} else {
return "Edit Contract"
}
}
},
emits: ['closetab','contractupdate'],
methods: {
close() {
this.$emit('closetab','list')
},
addProduct() {
this.contract.products.push(new Product())
},
removeProduct(code){
this.contract.products = this.contract.products.filter((c) => {
return c.code !== code
})
},
showCustomerSearch() {
this.search[0] = true
},
showProductSearch(num) {
this.search[1] = true
this.searchProdIndex = num
},
async saveContract(){
this.errors = []
this.saving = true
let url = this.$api_url + "/customers/contracts/" + this.contract.no + "/save"
if (this.contract.isNew) {
url = this.$api_url + "/customers/contracts/add"
}
console.log("Saving Contract : ", this.contract.no)
console.log(this.contract)
axios.post(url, {
contract: this.contract
}).then(resp => {
console.log("Saved Contract : " + JSON.stringify(resp.data))
this.saving = false
let stat = resp.data
if (stat.status == true ) {
if (this.contract.isNew) {
this.$emit('contractupdate', resp.data)
} else {
this.$emit('contractupdate', resp.data)
}
} else {
this.errors.push("Contract not saved.")
console.log("Not Saved")
}
this.$emit('contractupdate', resp.data)
}).catch(err => {
console.log(err)
this.saving = false
})
},
searchProducts(code) {
let url = this.$api_url + "/products/search/" + code
console.log(url)
axios.get(url)
.then(resp => {
console.log(resp)
this.products = resp.data
})
.catch(err => {
console.log(err)
this.products = [{code:"NoProductsFound", name:"No Products Found"}]
})
},
productCodeName(p) {
return p.code + ' - ' + p.name
},
setProduct(p){
let q = this.contract.products[this.searchProdIndex]
p.price = q.price
this.contract.products[this.searchProdIndex] = p
this.search[1] = false
},
setCustomer(c){
this.contract.customer = c
this.search[0] = false
}
}
}

View file

@ -7,114 +7,114 @@
</v-tabs>
<v-window v-model="tab" >
<v-window-item value="list">
<v-row>
<v-col cols="8" xs="12" sm="12" md="12" lg="8">
<v-responsive
max-width="500"
>
<v-text-field clearable
label="Search"
variant="outlined"
v-model="searchQuery"
density="compact"
append-inner-icon="mdi-magnify"></v-text-field>
<v-btn color="warning"
v-if="site_info.features.addcontract"
@click="showEditContract()"
prepend-icon="mdi-plus"
variant="text"
>Add</v-btn>
<v-progress-linear indeterminate color="blue" :active="loading"></v-progress-linear>
<v-card variant="outlined">
<RecycleScroller class="scroller"
:items="filteredContracts"
:item-size="130"
v-slot="{ item }"
key-field="no"
>
<v-row :class="[{inactive : !item.active}]" class="item">
<v-col cols="4" >
<h3>Contract : {{ item.no }}</h3>
{{ item.customer.acc_no }} - {{ item.customer.name }}
<v-switch v-model="item.active" color="green" @change="setContractInactive(item)" label="Active"></v-switch>
</v-col>
<v-col >
<template v-for="p in item.products" :key="p.code">
<span v-if="p.code != ''">
{{ p.code }} @ {{ p.price }}<br/>
</span>
</template>
</v-col>
<v-col >
<v-icon title="Start">mdi-play</v-icon>: {{ formatDate(item.start_date,"DD/MM/YYYY") }}<br/>
<v-icon title="Finish">mdi-flag-checkered</v-icon>: {{ formatDate(item.finish_date,"DD/MM/YYYY") }}<br/>
<v-icon title="Duration">mdi-clock</v-icon>: {{ item.duration }} months
</v-col>
<v-col>
{{ item.tonnage_per_month }} per month<br/>
= {{ formatNumber(item.tonnage_per_month * item.duration,2) }} total<br/>
= {{ formatNumber(item.tonnage_per_month * item.remaining_duration,2) }} remaining<br/>
</v-col>
<v-col>
<div class="d-flex justify-space-around align-center flex-column flex-sm-row fill-height">
<v-btn color="info">
More
<v-overlay activator="parent" class="align-center justify-center">
<v-card title="Info" width="600">
<v-card-subtitle>Comments</v-card-subtitle>
<v-card-text>{{ item.comments}} </v-card-text>
<v-card-subtitle>Office</v-card-subtitle>
<v-card-text>{{ item.office_comments}}</v-card-text>
<v-card-subtitle>Agreed</v-card-subtitle>
<v-card-text>{{ formatDate(item.agree_date,"DD/MM/YYYY") }}</v-card-text>
<v-card-subtitle>Products</v-card-subtitle>
<v-card-text>
<template v-for="p in item.products" :key="p.code">
<span v-if="p.code != ''">
{{ p.code }} - {{ p.name }} @ {{ p.price }}<br/>
</span>
</template>
</v-card-text>
<v-card-actions>
</v-responsive>
<v-dialog v-model="showDialog">
</v-dialog>
<v-row>
<v-col cols="8" xs="12" sm="12" md="8">
<v-progress-linear indeterminate color="blue" :active="loading"></v-progress-linear>
<RecycleScroller class="scroller"
:items="filteredContracts"
:item-size="130"
v-slot="{ item }"
key-field="no"
>
<v-row :class="[{inactive : !item.active}]" class="item">
<v-col cols="4" >
<h3>Contract : {{ item.no }}</h3>
{{ item.customer.acc_no }} - {{ item.customer.name }}
<v-switch v-model="item.active" color="green" @change="setContractInactive(item)" label="Active"></v-switch>
</v-col>
<v-col >
<template v-for="p in item.products" :key="p.code">
<span v-if="p.code != ''">
{{ p.code }} @ {{ p.price }}<br/>
</span>
</template>
</v-col>
<v-col >
<v-icon title="Start">mdi-play</v-icon>: {{ formatDate(item.start_date,"DD/MM/YYYY") }}<br/>
<v-icon title="Finish">mdi-flag-checkered</v-icon>: {{ formatDate(item.finish_date,"DD/MM/YYYY") }}<br/>
<v-icon title="Duration">mdi-clock</v-icon>: {{ item.duration }} months
</v-col>
<v-col>
{{ item.tonnage_per_month }} per month<br/>
= {{ formatNumber(item.tonnage_per_month * item.duration,2) }} total<br/>
= {{ formatNumber(item.tonnage_per_month * item.remaining_duration,2) }} remaining<br/>
</v-col>
<v-col>
<v-row>
<v-col cols="12">
<v-btn color="info">
More
<v-overlay activator="parent" class="align-center justify-center">
<v-card title="Info" width="600">
<v-card-subtitle>Comments</v-card-subtitle>
<v-card-text>{{ item.comments}} </v-card-text>
<v-card-subtitle>Office</v-card-subtitle>
<v-card-text>{{ item.office_comments}}</v-card-text>
<v-card-subtitle>Agreed</v-card-subtitle>
<v-card-text>{{ formatDate(item.agree_date,"DD/MM/YYYY") }}</v-card-text>
<v-card-subtitle>Products</v-card-subtitle>
<v-card-text>
<template v-for="p in item.products" :key="p.code">
<span v-if="p.code != ''">
{{ p.code }} - {{ p.name }} @ {{ p.price }}<br/>
</span>
</template>
</v-card-text>
<v-card-actions>
<v-btn color="info"
target="blank"
@click="getContractPrint(item.no)"
class="ma-2 pa-2"
:loading="item.multiloading"
>
Multi
</v-btn>
<v-btn color="info"
target="blank"
@click="getContractPrint(item.no)"
@click="getContractPrint(item.no,true)"
:loading="item.totalloading"
class="ma-2 pa-2"
:loading="multiloading"
>
Multi
Total
</v-btn>
<v-btn color="info"
target="blank"
@click="getContractPrint(item.no,true)"
:loading="totalloading"
class="ma-2 pa-2"
<v-btn color="warning"
v-if="site_info.features.editcontract"
@click="showEditContract(item)"
>
Total
Edit
</v-btn>
<v-btn color="warning"
v-if="site_info.features.editcontract"
@click="showEditContract(item)"
:loading="editloading"
>
Edit
</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
</v-btn>
<v-btn color="warning"
v-if="site_info.features.editcontract"
@click="showEditContract(item)"
:loading="editloading"
>
Edit
</v-btn>
</div>
</v-card-actions>
</v-card>
</v-overlay>
</v-btn>
</v-col>
<v-col cols="12">
<v-btn color="warning"
v-if="site_info.features.editcontract"
@click="showEditContract(item)"
>
Edit
</v-btn>
</v-col>
</v-row>
</RecycleScroller>
</v-card>
</v-col>
</v-row>
</v-col>
</v-row>
</RecycleScroller>
</v-col>
</v-row>
</v-window-item>
<v-window-item value="edit">
<ContractEdit ref="edit" :setcontract="selected_contract" @closetab="tab = 'list'" @contractupdate="contractUpdated"/>
@ -123,14 +123,11 @@
<ContractMulti :contract="selected_contract" :total="total" />
</v-window-item>
</v-window>
<v-dialog v-model="showDialog">
</v-dialog>
</template>
<script>
import ContractEdit from './ContractEdit.vue';
import ContractMulti from './ContractMulti.vue';
import methods from '@/CommonMethods.vue'
import ContractType from '@/types/ContractType.vue';
import axios from 'axios'
export default {
props: {
@ -182,37 +179,25 @@ export default {
edit: false,
report: false,
total: false,
content: "",
editloading: false,
multiloading: false,
totalloading: false
content: ""
}
},
mixins: [methods],
methods: {
async showEditContract(contract) {
this.editloading = true
setTimeout(() => {
if ( contract != undefined ) {
this.selected_contract = contract
} else {
this.selected_contract = new ContractType()
}
this.tab = "edit"
this.edit = true
this.editloading = false
},1000)
this.selected_contract = contract
//this.selected_contract = contract
this.tab = "edit"
this.edit = true
},
async getContractsList() {
this.loading = true
let url = this.$api_url + "/customers/contracts/list"
let c_id = this.$route.params.id || ""
console.log("Getting Contracts list..." + c_id)
console.log("Getting Contracts list...")
axios.get(url, {
params: {
limit: this.limit,
query: this.searchQuery,
c_id: c_id
query: this.searchQuery
}
})
.then(resp => {
@ -222,16 +207,12 @@ export default {
})
},
getContractPrint(contract, total = false) {
if (total){ this.totalloading = true } else { this.multiloading = true }
axios.get(this.$api_url + "/customers/contracts/" + contract + "/info")
.then(resp => {
this.selected_contract = resp.data
this.total = total
this.tab = "report"
this.report = true
}).finally(() => {
this.totalloading = false
this.multiloading = false
})
},
findContract(id) {
@ -268,7 +249,7 @@ export default {
</script>
<style scoped>
.scroller {
height: 70vw;
height: 600px;
}
.item {

View file

@ -1,5 +1,5 @@
<template>
<ReportLayout scope="contract" :filename="filename">
<ReportLayout scope="contract">
<div class="letter">
<p><span class="text-bold">Date: </span>{{ current_date }}</p>
<p class="text-bold">Customer's Address:</p>
@ -57,6 +57,7 @@
</template>
<script>
import '@/assets/css/reports.css';
import ReportLayout from '@/components/ReportLayout.vue'
import Common from '@/common.js';
import moment from 'moment';
@ -71,7 +72,6 @@ export default {
},
data(){
return {
filename: "Contract_" + this.contract.no,
current_date: Common.getDateNow(),
}
},

View file

@ -1,76 +1,33 @@
<template>
<v-text-field
label="Search"
variant="outlined"
v-model="searchQuery"
density="compact"
append-inner-icon="mdi-magnify"></v-text-field>
<v-row>
<v-col cols="12" sm=12 lg=6>
<v-card>
<v-card-title>
Customer List
</v-card-title>
<v-progress-linear color="blue" :active="customers_loading" indeterminate>
</v-progress-linear>
<v-list>
<RecycleScroller
class="scroller"
:items="filteredCustomers"
:item-size="60"
key-field="acc_no"
v-slot="{ item }"
>
<v-list-item >
<v-sheet class="clickable" @click="getCustomerInfo(item)" rounded :class="{ 'bg-error' : item.at_risk }">
<v-icon v-if="item.acc_no == selected_cust.acc_no">mdi-checkbox-marked-outline</v-icon>
<v-icon v-else>mdi-checkbox-blank-outline</v-icon>
{{ item.acc_no }} - {{ item.name }}:
<span class="text-caption">{{ item.address.line_1 }}, {{ item.address.line_2 }}, <br/>
{{ item.address.city }}, {{ item.address.county}}, {{ item.address.postcode }},
</span>
</v-sheet>
<template v-slot:append>
<v-btn-toggle>
<v-btn @click.prevent="viewOrders(item)" color="blue-lighten-1" title="View Orders">
<v-icon>mdi-cart-outline</v-icon>
</v-btn>
<v-btn @click.prevent="goToContracts(item)" color="blue-lighten-1" title="View Contracts">
<v-icon>mdi-file-edit-outline</v-icon>
<v-badge v-if="item.contract_count > 0" color="blue-lighten-4" floating :content="item.contract_count">
</v-badge>
</v-btn>
<v-btn @click.prevent="goToMedFeeds(item)" color="orange-lighten-1" title="View Med Feeds">
<v-icon>mdi-pill</v-icon>
<v-badge v-if="item.medfeed_count > 0" color="orange-lighten-4" floating :content="item.medfeed_count">
</v-badge>
</v-btn>
<v-btn @click.prevent="goToComplaints(item)" color="orange-lighten-1" title="View Complaints">
<v-icon>mdi-exclamation-thick</v-icon>
<v-badge v-if="item.complaint_count > 0" color="red-lighten-4" floating :content="item.complaint_count">
</v-badge>
</v-btn>
</v-btn-toggle>
</template>
</v-list-item>
</RecycleScroller>
</v-list>
</v-card>
<br/>
<RecentOrders :customer="selected_cust" size_small doc_status=0></RecentOrders>
</v-col>
<v-col cols="12" sm=12 lg=6>
<CustomerComments :customer="selected_cust"></CustomerComments>
<br/>
<RecentOrders :customer="selected_cust" doc_status=2></RecentOrders>
</v-col>
</v-row>
<h3>Customer List</h3>
<v-responsive
max-width="500"
>
<v-text-field
clearable
label="Search"
variant="outlined"
v-model="searchQuery"
density="compact"
append-inner-icon="mdi-magnify"></v-text-field>
</v-responsive>
<RecycleScroller
class="scroller"
:items="filteredCustomers"
:item-size="50"
key-field="acc_no"
v-slot="{ item }"
>
<v-row class="customer">
<v-col cols="6">
{{ item.acc_no }} - {{ item.name }}
</v-col>
</v-row>
<hr />
</RecycleScroller>
</template>
<script>
import axios from 'axios';
import RecentOrders from '@/components/RecentOrders.vue'
import CustomerComments from '@/components/CustomerComments.vue'
import Customer from '@/types/CustomerType.vue'
export default {
props: {
site_info: {},
@ -79,16 +36,15 @@ export default {
data() {
return {
searchQuery: "",
customer_list: [],
customers_loading: null,
selected_cust: new Customer(),
showSearch: true,
open: false,
list: [],
limit: 5000,
listreceived: false,
loading: true,
listreceived: false
}
},
components: {
RecentOrders,
CustomerComments
},
computed: {
filteredCustomers() {
@ -96,58 +52,38 @@ export default {
if (!this.listreceived){
this.getCustomerList()
}
let clist = this.customer_list.filter(q =>
let clist = this.list.filter(q =>
q.name.toLowerCase().includes(query) ||
q.acc_no.includes(query) ||
q.address.line_1.toLowerCase().includes(query) ||
q.address.line_2.toLowerCase().includes(query) ||
q.address.city.toLowerCase().includes(query) ||
q.address.postcode.toLowerCase().includes(query)
q.acc_no.includes(query)
)
return clist
},
},
methods: {
getCustomerInfo(c) {
this.selected_cust = c
},
goToContracts(c){
this.$router.push('/customers/contracts/list/' + c.id)
},
goToMedFeeds(c){
this.$router.push('/customers/medicated-feeds/list/' + c.id)
},
goToComplaints(c){
this.$router.push('/customers/complaints/list/' + c.id)
},
viewOrders(c){
this.$router.push('/customers/orders/list/' + c.id)
},
async getCustomerList() {
this.customers_loading = true
this.loading = true
let url = this.$api_url + "/customers/list"
axios
.get(url,{
params: {
limit: this.limit,
query: "" }})
params: { limit: this.limit, query: this.searchQuery }})
.then(resp => {
this.customer_list = resp.data
this.list = resp.data
this.listreceived = true
this.loading = false
})
.catch(error => (console.log(error)))
.finally(() => {
this.customers_loading = false
})
}
}
}
</script>
<style scoped>
.scroller {
height: 300px;
height: 500px;
}
.scroller.small {
height: 200px;
.customer {
height: 32%;
padding: 0 12px;
display: flex;
align-items: center;
}
</style>

View file

@ -1,191 +1,3 @@
<template>
<v-card :title="title" :subtitle="'Medicated Feed : ' + mf.id">
<v-card-text>
<v-container>
<v-row>
<v-col cols="6">
<v-text-field label="Customer" readonly variant="outlined" prepend-inner-icon="mdi-magnify" @click="showCustomerSearch" :model-value="mf.customer.acc_no + ' - ' + mf.customer.name">
</v-text-field>
<v-text-field label="Vet" readonly variant="outlined" prepend-inner-icon="mdi-magnify" @click="showVetSearch" :model-value="mf.vet.practice">
</v-text-field>
</v-col>
<v-col cols="6">
<v-card title="Medication :">
<v-card-text>
<v-text-field label="Medication" readonly variant="outlined" prepend-inner-icon="mdi-magnify" @click="showMedSearch" :model-value="mf.medication.name">
</v-text-field>
<template v-for="(i, idx) in mf.medication.info" :key="idx" >
<template v-if="i != ''">
{{ i }}<br/>
</template>
</template>
Med Code : {{ mf.medication.med_code }}<br/>
Inclusion Rate : {{ mf.medication.inclusion_rate }}
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-text-field label="Product" readonly variant="outlined" prepend-inner-icon="mdi-magnify" @click="showProductSearch" :model-value="mf.product.code + ' - ' + mf.product.name">
</v-text-field>
</v-col>
<v-col cols="6">
<v-text-field label="Tonnage" variant="outlined" type="number" v-model="mf.tonnage"></v-text-field>
</v-col>
<v-col cols="6">
<label>
Date Required :
<DatePicker v-model="mf.date_required" format="dd/MM/yyyy" />
</label>
</v-col>
</v-row>
<v-row>
<v-col cols="6">
<v-switch color="blue" label="Repeat prescription" v-model="mf.repeat"></v-switch>
</v-col>
<v-col cols="6">
<v-textarea :disabled="!mf.repeat" label="Repeat Message" v-model="mf.repeat_message" variant="outlined"></v-textarea>
</v-col>
</v-row>
</v-container>
</v-card-text>
<DebugPanel :data="mf"></DebugPanel>
<ErrorBanner :errors="errors" />
<v-card-actions>
<v-btn v-if="!mf.isNew" color="red-darken-1"
variant="text"
:loading="saving"
@click="saveMedFeed(mf)">Save</v-btn>
<v-btn v-if="mf.isNew" color="red-darken-1"
variant="text"
:loading="saving"
@click="saveMedFeed(mf)">Add</v-btn>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1"
variant="text"
@click="close">Close</v-btn>
</v-card-actions>
</v-card>
<v-dialog v-model="search[0]">
<CustomerSearch @returnCustomer="setCustomer"></CustomerSearch>
</v-dialog>
<v-dialog v-model="search[1]">
<ProductSearch @returnProduct="setProduct"></ProductSearch>
</v-dialog>
<v-dialog v-model="search[2]">
<VetSearch @returnVet="setVet"></VetSearch>
</v-dialog>
<v-dialog v-model="search[3]">
<MedSearch @returnMed="setMed"></MedSearch>
</v-dialog>
Not Implemented, yet :-)
</template>
<script>
import DatePicker from '@vuepic/vue-datepicker'
import axios from 'axios';
import CustomerSearch from '@/components/CustomerSearch.vue'
import ProductSearch from '@/components/ProductSearch.vue'
import VetSearch from '@/components/VetSearch.vue'
import MedSearch from '@/components/MedSearch.vue'
import ErrorBanner from '@/components/ErrorBanner.vue'
export default {
props:{
set_mf: {}
},
components: {
DatePicker,
CustomerSearch,
ProductSearch,
ErrorBanner,
VetSearch,
MedSearch
},
watch: {
set_mf(newval) {
this.mf = newval
},
},
data() {
return {
mf: this.set_mf,
vets: [],
medications: [],
products: [],
search: [],
searching: {},
errors: [],
saving: false
}
},
computed: {
title() {
if ( this.mf.isNew ) {
return "New Medicated Feed"
} else {
return "Edit Medicated Feed"
}
}
},
emits: ['closetab','medfeedupdated'],
methods: {
close() {
this.$emit('closetab','list')
},
saveMedFeed(medfeed) {
this.errors = []
this.saving = true
let url = this.$api_url + "/customers/medicated-feeds/" + this.mf.id + "/save"
if (this.mf.isNew) {
url = this.$api_url + "/customers/medicated-feeds/add"
}
console.log("Saving Med Feed...")
axios.post(url,{
medfeed: medfeed
})
.then(resp => {
let stat = resp.data
if (stat.status == true ) {
this.$emit('medfeedupdated')
} else {
this.errors.push("Error Saving... ")
}
})
.catch(err => {
console.log(err)
})
.finally(()=>{
this.saving = false
})
},
showCustomerSearch(){
this.search[0] = true
},
setCustomer(c){
this.mf.customer = c
this.search[0] = false
},
showProductSearch() {
this.search[1] = true
},
setProduct(p) {
this.mf.product = p
this.search[1] = false
},
showVetSearch() {
this.search[2] = true
},
setVet(v) {
this.mf.vet = v
this.search[2] = false
},
showMedSearch() {
this.search[3] = true
},
setMed(med) {
this.mf.medication = med
this.search[3] = false
},
},
}
</script>

View file

@ -10,68 +10,63 @@
<v-window-item value="list">
<v-row>
<v-col>
</v-col>
</v-row>
<v-row>
<v-col cols="12" xs="12" sm="12" md="12" lg=8>
<v-text-field clearable
label="Search"
variant="outlined"
v-model="searchQuery"
density="compact"
append-inner-icon="mdi-magnify"></v-text-field>
<v-btn v-if="site_info.features.addmedfeed" color="warning" @click="editMedFeed()" prepend-icon="mdi-plus" variant="text">Add</v-btn>
</v-col>
</v-row>
<v-btn v-if="site_info.features.addmedfeed" color="warning" @click="editMedFeed({})">+ Add</v-btn>
<v-row>
<v-col cols="8" xs="12" sm="12" md="8">
<v-progress-linear indeterminate color="blue" :active="loading"></v-progress-linear>
<v-card variant="outlined">
<RecycleScroller class="scroller"
:items="filteredMedFeeds"
:item-size="100"
v-slot="{ item }"
key-field="id">
<v-row dense class="item" :class="{ at_risk : item.customer.at_risk }">
<v-col cols="4">
Medicated Feed : {{ item.id }}<br/>
<span class="text-caption">
{{ item.customer.acc_no }} - {{ item.customer.name }}<br/>
Vet: {{ item.vet.practice }}</span>
</v-col>
<v-col class="text-caption">
{{ item.medication.name }} {{ item.medication.inclusion_rate }}<br />
{{ item.product.name }}
</v-col>
<v-col class="text-caption">
Required : {{ formatDate(item.date_required,"DD/MM/YYYY") }} <br/>
Repeat Prescription? : <v-icon v-if="item.repeat">mdi-refresh</v-icon>
</v-col>
<v-col>
<div class="d-flex justify-space-around align-center flex-column flex-sm-row fill-height">
<v-btn>
More
<v-overlay activator="parent" class="align-center justify-center">
<v-container>
<v-card width="600">
<v-card-title>Info</v-card-title>
<v-card-subtitle>Repeat Message</v-card-subtitle>
<v-card-text>{{ item.repeat_message }}</v-card-text>
<v-card-actions>
<v-btn class="mb-2 mr-2" color="blue" @click="reportScriptReq(item)">Script Request</v-btn>
<v-btn class="mb-2 mr-2" color="blue" @click="reportOrderForm(item)">Order Form</v-btn>
</v-card-actions>
</v-card>
</v-container>
</v-overlay>
</v-btn><br/>
<v-btn v-if="site_info.features.editmedfeed" color="warning" @click="editMedFeed(item)">Edit</v-btn>
</div>
</v-col>
</v-row>
</RecycleScroller>
</v-card>
<RecycleScroller class="scroller"
:items="filteredMedFeeds"
:item-size="130"
v-slot="{ item }"
key-field="id"
>
<v-row class="item" :class="{ at_risk : item.customer.at_risk }">
<v-col cols="4">
Medicated Feed : {{ item.id }},
{{ item.customer.acc_no }} - {{ item.customer.name }}
</v-col>
<v-col>
{{ item.medication.name }} {{ item.medication.inclusion_rate }}<br />
{{ item.product.name }}
</v-col>
<v-col>
Required : {{ formatDate(item.date_required,"DD/MM/YYYY") }} <br/>
Repeat Prescription? : <v-icon v-if="item.repeat">mdi-refresh</v-icon>
</v-col>
<v-col>
<v-btn>
More
<v-overlay activator="parent" class="align-center justify-center">
<v-container>
<v-card width="600">
<v-card-title>Info</v-card-title>
<v-card-subtitle>Repeat Message</v-card-subtitle>
<v-card-text>{{ item.repeat_message }}</v-card-text>
<v-card-actions>
<v-btn class="mb-2 mr-2" color="blue" @click="reportScriptReq(item)">Script Request</v-btn>
<v-btn class="mb-2 mr-2" color="blue" @click="reportOrderForm(item)">Order Form</v-btn>
</v-card-actions>
</v-card>
</v-container>
</v-overlay>
</v-btn>
<v-btn v-if="site_info.editmedfeed" >Edit</v-btn>
</v-col>
</v-row>
</RecycleScroller>
</v-col>
</v-row>
</v-window-item>
<v-window-item value="edit">
<MedFeedsEdit :set_mf="selected_mf" @closetab="tab = 'list'" @medfeedupdated="medfeedUpdated" />
<MedFeedsEdit />
</v-window-item>
<v-window-item value="scriptreq">
<ScriptReq :mf="selected_mf" :user="user_info"></ScriptReq>
@ -87,7 +82,6 @@ import MedFeedsEdit from './MedFeedsEdit.vue'
import ScriptReq from './MedFeedsScriptReq.vue'
import OrderForm from './MedFeedsOrderForm.vue'
import methods from '@/CommonMethods.vue'
import MedFeedType from '@/types/MedFeedType.vue';
export default {
props: {
site_info: {},
@ -120,8 +114,7 @@ export default {
}
let clist = this.list.filter(q =>
q.customer.name.toLowerCase().includes(query) ||
q.customer.acc_no.includes(query) ||
q.id == query
q.customer.acc_no.includes(query)
)
return clist
}
@ -131,13 +124,11 @@ export default {
async getMedFeedsList(){
this.loading = true
let url = this.$api_url + "/customers/medicated-feeds/list"
let c_id = this.$route.params.id || ""
console.log("Getting Medicated Feeds list..." + c_id)
console.log("Getting Medicated Feeds list...")
axios.get(url,{
params: {
limit: this.limit,
query: this.searchQuery,
c_id: c_id
query: this.searchQuery
}
}).then(resp => {
this.list = resp.data
@ -171,19 +162,9 @@ export default {
})
},
async editMedFeed(mf) {
console.log(mf)
if ( mf != undefined ) {
this.selected_mf = mf
} else {
this.selected_mf = new MedFeedType()
}
this.edit = true
this.tab = "edit"
},
medfeedUpdated(){
this.getMedFeedsList()
this.edit = false
this.tab = "list"
this.selected_mf = mf
}
}
}
@ -194,9 +175,9 @@ export default {
}
.item {
height: 100px;
height: 138px;
overflow-y:hidden;
padding: 0 1em;
padding: 0 12px;
margin-bottom:2px;
display: flex;
align-items: center;

View file

@ -74,7 +74,6 @@
size is 2 tonnes.</p>
<p>If you require any further information about the veterinary
medicinal products we stock, please get in touch.</p>
<p class="text-underline">Please ensure the prescription is filled out as per the Veterinary Medicines Regulations 2013.</p>
<h4>The customer requires this order on {{ formatDate(mf.date_required,"DD/MM/yyyy") }}</h4>
</main>
</div>

View file

@ -1,34 +0,0 @@
<template>
<h3>Orders List</h3>
<v-row>
<v-col cols="8" xs="12" sm="12" md="8">
<RecentOrders :customer="customer" limit=20 doc_status="0"></RecentOrders>
<br/>
<RecentOrders :customer="customer" limit=20 doc_status="2"></RecentOrders>
</v-col>
</v-row>
</template>
<script>
import Customer from '@/types/CustomerType.vue'
import RecentOrders from '@/components/RecentOrders.vue'
export default {
props: {
site_info: {},
user_info: {}
},
components:{
RecentOrders
},
data() {
return {
customer: new Customer()
}
},
created(){
let c_id = this.$route.params.id || 0
this.customer.id = c_id
}
}
</script>

View file

@ -1,135 +0,0 @@
<template>
<v-card>
<v-card-title>
{{ doc_types[doc_status] }} Orders - {{ customer.acc_no }} {{ customer.name }}
<v-btn v-if="customer.id != ''" size="smaller" icon="mdi-refresh" variant="text" color="green" @click="getCustomerRecentOrders" :loading="orders_loading"></v-btn>
</v-card-title>
<v-progress-linear color="blue" :active="orders_loading" indeterminate>
</v-progress-linear>
<v-container>
<v-row dense>
<v-col cols=12 v-for="item in sortedOrders" :key="item.doc_no">
<v-card density="compact" :class="{ 'bg-green-lighten-5' : item.doc_status == 'Live' }">
<v-row dense>
<v-col>
<v-card-title>
Order: {{ item.doc_no }}
</v-card-title>
<v-card-subtitle>
{{ formatDate(item.doc_date,"DD/MM/YYYY") }}
<v-icon v-if="item.doc_status == 'Completed'" icon="mdi-check"></v-icon>
<v-icon v-if="item.doc_status == 'Live'" icon="mdi-play"></v-icon>
{{ item.doc_status }}
</v-card-subtitle>
<v-card-text>
{{ item.customer.acc_no }} - {{ item.customer.name }}
</v-card-text>
</v-col>
<v-col>
<v-card-text>
<!--<h5>Address :</h5>-->
<template v-if="item.del_addr.id != 0 && item.del_addr.postal_name != ''">
<h5>Delivery Address :</h5>
<template v-for="(v, k , index) in item.del_addr" :key="index">
<span class="text-caption" v-if="k != 'id' && (v != '' || v != 0)">
{{ v }}<br/>
</span>
</template>
</template>
</v-card-text>
</v-col>
<v-col cols=5>
<v-card-text>
<h5>Items :</h5>
<span class="text-caption" v-for="(i, index) in item.products" :key="index" style="border-bottom: 1px dotted #000;">
{{ i.code }} - {{ i.name }}
<span v-if="i.quantity != 0">x {{ i.quantity }}</span><br/>
</span>
</v-card-text>
</v-col>
<v-col cols=1>
<v-btn variant="text" icon="mdi-exclamation" color="red"></v-btn>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
</v-container>
</v-card>
</template>
<script>
import axios from 'axios'
import Customer from '@/types/CustomerType.vue'
import methods from '@/CommonMethods.vue'
export default {
props:{
customer: new Customer(),
doc_status: Number,
limit: Number
},
watch: {
customer() {
this.getCustomerRecentOrders()
},
},
mixins: [methods],
data() {
return {
orders_loading: false,
orders: [],
doc_types: ["Live","","Completed"],
}
},
computed: {
prog_col(){
if (this.doc_status == 2){
return "green"
} else {
return "blue"
}
},
sortedOrders() {
let sorted = this.orders
sorted.sort((a, b) => {
if (a.doc_date < b.doc_date){
return 1
}
if (a.doc_date > b.doc_date){
return -1
}
return 0
})
return sorted
}
},
methods: {
getCustomerRecentOrders(){
this.orders_loading = true
let url = this.$api_url + "/customers/" + this.customer.id + "/orders/recent"
axios.get(url, {
params: {
doc_status: this.doc_status,
limit: this.limit || 6
}
})
.then(resp => {
this.orders = resp.data
})
.catch(err => {
console.log(err)
})
.finally(() => {
this.orders_loading = false
})
}
},
created() {
if (this.customer.id != "") {
this.getCustomerRecentOrders()
}
}
}
</script>

34
todo.md
View file

@ -1,34 +0,0 @@
### Basic
- [ ] login screen animated logo a la calckey
- [ ] log out button under "Hi {USER]" area
- [ ] store customer, products, complaints, contracts, etc. in var and repeatedly check for updates
### Contracts
- [x] contracts list
- [x] print contract
- [x] new contract
- [x] edit contract?
### Complaints
- [x] complaints list
- [x] view complaint info (comments, 1, 2, 3, at risk, permanent, added by)
- [ ] make list recycle box
- [ ] add driver to complaint info
- [ ] edit complaint text
- [ ] add complaint
- [ ] edit complaint
### Medicated Feeds
- [x] Medicated Feeds List
- [x] more info on list?
- [x] Medicated feeds report 1
- [x] medicated feeds report 2
- [ ] add medicated feeds
- [ ] edit medicated feeds
- [ ] add/edit medicines
### Farmers Cheques
- [ ] Farmers Cheques
### Poultry Letters (?)
- [ ] Find out whatever these actually are

6861
yarn.lock Normal file

File diff suppressed because it is too large Load diff