Compare commits

..

115 commits
v1.0.0 ... main

Author SHA1 Message Date
5817f648b6 updated cmc_fe.tar.gz 2023-04-20 09:52:08 +01:00
8a594cc653 0.1.10 2023-04-20 09:51:14 +01:00
a032ff6eea Merge branch 'main' of https://forge.notnull.click/psw/cmc_fe 2023-04-20 09:50:31 +01:00
5827002154 fixed reasons in complaints 2023-04-20 09:50:25 +01:00
499c92448a updated cmc_fe.tar.gz 2023-04-20 09:44:42 +01:00
b7437298d1 0.1.9 2023-04-20 09:43:46 +01:00
33176f1756 updated packages 2023-04-04 09:32:23 +01:00
0ab97f2afc updated cmc_fe.tar.gz 2023-04-03 14:49:46 +01:00
d22bb18898 0.1.8 2023-04-03 14:49:12 +01:00
c741f58f01 switch to pnpm 2023-04-03 14:49:10 +01:00
88976c0c7b switch to pnpm 2023-04-03 14:48:40 +01:00
e5ca7ae946 globalised ErrorBanner and DebugPanel 2023-03-27 16:48:18 +01:00
fe7daccadd added main markup for complaint add/edit 2023-03-27 16:42:02 +01:00
e2cff40cd7 orders now in recycle containers 2023-03-27 14:48:22 +01:00
5fed496a36 order list 2023-03-27 12:35:36 +01:00
a1426eec60 added customer complaints list 2023-03-27 11:14:16 +01:00
c5805185c7 Merge branch 'main' of https://forge.notnull.click/psw/cmc_fe 2023-03-27 09:51:00 +01:00
86378a58c3 better looking complaint ticks 2023-03-27 09:50:39 +01:00
c0d1df0944 working on switching to overlays instead of dialogs? 2023-03-24 17:27:18 +00:00
d357201b06 switch to npm 2023-03-24 17:15:55 +00:00
218baab58a updated cmc_fe.tar.gz 2023-03-24 17:11:03 +00:00
fe136d9fd9 0.1.7 2023-03-24 17:10:19 +00:00
b7d6eb1987 changed build scripts 2023-03-24 17:10:13 +00:00
d0a6e6fa5b changed build scripts 2023-03-24 17:09:06 +00:00
59f77df2a2 1.1.1 2023-03-24 17:06:24 +00:00
16ae90d0c5 1.1.0 2023-03-24 17:06:03 +00:00
1e203291f1 1.0.0 2023-03-24 17:05:54 +00:00
1e0f505c67 0.1.6 2023-03-24 17:04:52 +00:00
a96164236d switched to npm 2023-03-24 17:04:35 +00:00
0567bd9465 check in 2023-03-24 16:53:49 +00:00
ded985353a complaints now in new format 2023-03-24 16:07:47 +00:00
28e97f1c7f styling changes on contracts and meds 2023-03-24 14:35:22 +00:00
eef3f9816c better loading check 2023-03-24 13:42:33 +00:00
1e946827c1 better loading check 2023-03-24 13:42:22 +00:00
0a3497a192 updated cmc_fe.tar.gz 2023-03-24 12:56:20 +00:00
1ab4e9ecae v0.1.5 2023-03-24 12:55:35 +00:00
d565b4ab6a some styling 2023-03-24 12:55:23 +00:00
2acb73802a better meds search 2023-03-24 12:05:37 +00:00
9e77272535 add/edit med feeds works 2023-03-24 11:47:02 +00:00
68c2109438 better del addr and comments displays 2023-03-24 11:35:15 +00:00
2c5457673c coloured live orders and rearranged customer list 2023-03-24 10:56:50 +00:00
7b61ccf99b editing med feeds works, working on adding 2023-03-23 21:18:01 +00:00
64fcfff373 better saving/loading in contracts and med feeds, handlers for adding med feed 2023-03-23 20:46:24 +00:00
43ab44794c better error banner 2023-03-23 20:36:07 +00:00
8195e81657 upgrade to odbcn v6 now working vetter 2023-03-23 19:43:01 +00:00
d4f7fa7337 working on editing med feeds again 2023-03-23 18:18:58 +00:00
342bbfc9bc added vet and med search 2023-03-23 17:05:16 +00:00
8799da8440 removed superfluous customer ID in med feeds and contracts list 2023-03-23 13:19:47 +00:00
a3419e99a3 updated cmc_fe.tar.gz 2023-03-23 13:02:26 +00:00
ed5010637f v0.1.4-b 2023-03-23 13:01:35 +00:00
1e69f92af6 squircled favicon 2023-03-23 13:01:11 +00:00
d0f521adac updated cmc_fe.tar.gz 2023-03-23 12:54:10 +00:00
193ed07ca4 v0.1.4a 2023-03-23 12:53:26 +00:00
3a22597410 added favicon 2023-03-23 12:53:15 +00:00
696756aa63 updated cmc_fe.tar.gz 2023-03-23 12:33:07 +00:00
a23708c992 updated cmc_fe.tar.gz 2023-03-23 12:05:55 +00:00
ab2f730086 v0.1.4 2023-03-23 12:05:14 +00:00
16f636a387 fixed id bug on med feeds and contracts lists (if 1) 2023-03-23 12:05:03 +00:00
42a3324d04 use cust id instead of acc_no for queries 2023-03-22 14:46:07 +00:00
56f9be0a21 updated cmc_fe.tar.gz 2023-03-22 13:14:53 +00:00
676e44c794 v0.1.3 2023-03-22 13:13:45 +00:00
d86fde9d19 better recent orders layout 2023-03-22 13:13:29 +00:00
31bc1e4eae updated cmc_fe.tar.gz 2023-03-21 16:58:36 +00:00
0bf0c8c8ab v0.1.2 2023-03-21 16:57:57 +00:00
9f073ddba3 better customer list/dashboard 2023-03-21 16:57:39 +00:00
a46eebe5d3 contracts and med feeds by acc_no 2023-03-21 15:06:42 +00:00
623b107f65 work towards comments 2023-03-21 13:02:06 +00:00
5f021f8d5c brought customer and product search into med feeds 2023-03-20 22:20:33 +00:00
0caeb1d859 created customer search component 2023-03-20 22:04:16 +00:00
814fe233e4 created product search component 2023-03-20 21:55:08 +00:00
bd99364728 better customer screen WIP 2023-03-20 18:39:43 +00:00
c5fd049d07 better customer screen WIP 2023-03-20 18:13:10 +00:00
b78da566d9 v0.1.1 2023-03-20 17:03:30 +00:00
35c7f00ba3 updated cmc_fe.tar.gz 2023-03-20 15:49:57 +00:00
834ac4201f better search on contract edit 2023-03-20 15:48:33 +00:00
b6dfa61f04 better search on contract edit 2023-03-20 15:27:10 +00:00
b19a24842d medfeed edit processing 2023-03-17 16:48:21 +00:00
92fc782ca5 better buttons on contracts 2023-03-17 16:12:47 +00:00
6991d1adfd better buttons on med feeds 2023-03-17 16:10:37 +00:00
28fc9e9629 better list scaling 2023-03-17 16:04:58 +00:00
710eadc49b add data connection error 2023-03-17 16:02:55 +00:00
b80bba3d3d added hinge animation when login fails 2023-03-17 15:29:39 +00:00
a5b3970d7f added hinge animation when login fails 2023-03-17 15:09:09 +00:00
bdf42122f7 slightly better edit med feeds layout 2023-03-03 11:06:42 +00:00
c4d3ccf66a page for med feeds add/edit done - need to do searches for vets and meds 2023-02-15 16:30:07 +00:00
207035bd8d updated cmc_fe.tar.gz 2023-02-14 16:30:07 +00:00
9ab4f2a6db added progress bar for logon and nicer logout menu 2023-02-14 16:29:15 +00:00
6f82189a42 added animated logo for starting up 2023-02-14 15:50:44 +00:00
351e4e3e14 updated cmc_fe.tar.gz 2023-02-14 15:21:36 +00:00
c9829b5d76 fixed product search in contracts edit 2023-02-14 15:20:54 +00:00
c50feca092 updated cmc_fe.tar.gz 2023-02-14 14:32:27 +00:00
81431f1896 working on displaying errors 2023-02-14 14:28:57 +00:00
bb0f26c1fc save 2023-02-13 20:03:41 +00:00
d941ff9663 removed autocomplete from product list 2023-02-13 13:46:50 +00:00
c10d69a54d added todo 2023-02-11 12:04:54 +00:00
b3338fb1a5 added todo 2023-02-11 12:04:01 +00:00
ed1a240777 base work for edit mf done 2023-02-11 12:02:39 +00:00
22f368bf22 better type system 2023-02-11 11:59:21 +00:00
4104b28f6f framework for edit med feeds 2023-02-11 11:55:26 +00:00
0f1b107904 updated cmc_fe.tar.gz 2023-02-10 16:58:45 +00:00
26ae32aa52 fixed issue where pdf-scope css won't apply to med feeds reports 2023-02-10 16:58:21 +00:00
7efba6ab7c updated cmc_fe.tar.gz 2023-02-10 16:49:22 +00:00
5cf3f65fb8 updated cmc_fe.tar.gz 2023-02-10 16:48:26 +00:00
a62d4ce25f updated check for add contract feature 2023-02-10 12:37:49 +00:00
9f35e4f3a1 updated cmc_fe.tar.gz 2023-02-10 12:27:41 +00:00
784ab71557 working on add contract 2023-01-30 15:15:29 +00:00
2dfd7007c9 neary finished on add contract 2023-01-30 12:48:31 +00:00
494c25e8ec updated cmc_fe.tar.gz 2023-01-19 13:30:04 +00:00
527d3590b3 updated cmc_fe.tar.gz 2023-01-19 13:27:30 +00:00
0a761950b1 updated cmc_fe.tar.gz 2023-01-19 13:26:18 +00:00
ce965bb01f updated cmc_fe.tar.gz 2023-01-19 13:19:05 +00:00
92b3901eb7 updated cmc_fe.tar.gz 2023-01-19 13:11:42 +00:00
ea87461d73 updated cmc_fe.tar.gz 2023-01-19 13:06:28 +00:00
281dec65fe added script to create release and tar.gz file 2023-01-19 12:57:05 +00:00
7c2046f945 added script to create release 2023-01-19 12:55:12 +00:00
61 changed files with 22566 additions and 7345 deletions

1
.gitignore vendored
View file

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

BIN
cmc_fe.tar.gz Normal file

Binary file not shown.

17
create-release.sh Executable file
View file

@ -0,0 +1,17 @@
#!/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 Normal file

File diff suppressed because it is too large Load diff

View file

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

7366
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

BIN
public/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/images/favicon32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
public/images/favicon64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
public/images/favicon96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View file

@ -4,8 +4,12 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>images/logo_fields.png">
<title><%= htmlWebpackPlugin.options.title %></title> <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> </head>
<body> <body>
<noscript> <noscript>

View file

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

View file

@ -1,12 +1,14 @@
<template> <template>
<v-app> <v-app>
<MyNav :user="user" :site_info="site_info" /> <MyNav :user="user" :site_info="site_info" v-if="!isLoading" />
<v-main class="ma-4"> <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>
<router-view :site_info="site_info" :user_info="user_info"></router-view> <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> </v-app>
</template> </template>
@ -17,13 +19,16 @@ import axios from 'axios'
export default { export default {
name: 'App', name: 'App',
components: { components: {
MyNav, MyNav
}, },
emits: ["ready"],
data() { data() {
return { return {
isLoading: true,
site_info: { site_info: {
name: "Loading...", name: "",
features: {} features: {},
backend_connected: false
}, },
user: { user: {
first_name: "", first_name: "",
@ -44,6 +49,14 @@ export default {
} }
}, },
methods: { methods: {
checkIfReady(){
if (this.site_info.name != "") {
setTimeout(() => {
this.isLoading = false
this.$emit("ready")
},1000)
}
},
checkLoginStatus() { checkLoginStatus() {
let url = this.$api_url + "/users/check_login" let url = this.$api_url + "/users/check_login"
console.log("Checking login status...") console.log("Checking login status...")
@ -70,10 +83,24 @@ export default {
}, },
async getSiteInfo() { async getSiteInfo() {
console.log("Trying to get site Info...")
axios axios
.get(this.$api_url + "/info") .get(this.$api_url + "/info")
.then(response => {this.site_info = response.data}) .then(response => {
.catch(error => (console.log(error))) 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()
})
}, },
async getUserInfo() { async getUserInfo() {

View file

@ -1,6 +1,14 @@
<script> <script>
import moment from 'moment' import moment from 'moment'
import axios from 'axios'
import Error from '@/types/ErrorType.vue'
export default { export default {
data() {
return {
customer_list: [],
errors: {}
}
},
methods:{ methods:{
formatDate(d,f) { formatDate(d,f) {
return moment(String(d)).format(f) return moment(String(d)).format(f)
@ -26,6 +34,35 @@ export default {
minute: '2-digit', minute: '2-digit',
second: '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> </script>

View file

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

View file

@ -0,0 +1,50 @@
<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

@ -0,0 +1,156 @@
<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

@ -0,0 +1,113 @@
<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

@ -0,0 +1,66 @@
<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

@ -0,0 +1,25 @@
<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

@ -0,0 +1,75 @@
<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

@ -0,0 +1,20 @@
<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

@ -0,0 +1,49 @@
<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

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

View file

@ -1,6 +1,6 @@
<template> <template>
<v-container class="button-container"> <v-container class="button-container">
<v-btn color="primary" @click="generatePdf()" class="mr-2">Create PDF</v-btn> <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="grey" v-print="printObj">Print</v-btn>
</v-container> </v-container>
</template> </template>
@ -8,10 +8,12 @@
import html2pdf from 'html2pdf.js'; import html2pdf from 'html2pdf.js';
export default { export default {
props: { props: {
scope: String scope: String,
filename: String
}, },
data() { data() {
return { return {
generating: false,
printObj: { printObj: {
id: this.scope, id: this.scope,
previewTitle: "Report Print", previewTitle: "Report Print",
@ -20,8 +22,16 @@ export default {
} }
}, },
methods:{ methods:{
async generatePdf() { generatePdf() {
html2pdf(document.getElementById(this.scope)) 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)
} }
} }
} }

View file

@ -0,0 +1,64 @@
<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

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,16 @@
<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

@ -0,0 +1,18 @@
<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

@ -0,0 +1,19 @@
<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

@ -0,0 +1,13 @@
<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>

6
src/types/ErrorType.vue Normal file
View file

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

23
src/types/MedFeedType.vue Normal file
View file

@ -0,0 +1,23 @@
<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

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

View file

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

7
src/types/VetType.vue Normal file
View file

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

View file

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

View file

@ -0,0 +1,175 @@
<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,119 +1,88 @@
<template> <template>
<h3>Complaints</h3> <h3>Complaints</h3>
<v-tabs v-model="tab" fixed-tabs> <v-tabs v-model="tab" >
<v-tab title="List" v-model="list" /> <v-tab title="List" value="isList"></v-tab>
<v-tab title="Edit" v-model="edit" v-if="edit" /> <v-tab title="Edit" value="edit" v-if="edit"></v-tab>
<v-tab title="Report" v-model="report" v-if="report" /> <v-tab title="Report" value="report" v-if="report" ></v-tab>
</v-tabs> </v-tabs>
<v-window v-model="tab"> <v-window v-model="tab">
<v-window-item v-model="list"> <v-window-item value="isList">
<v-responsive <v-col cols="12" xs="12" sm="12" md="12" lg=8>
max-width="500" <v-text-field label="Search"
>
<v-text-field
clearable
label="Search"
variant="outlined" variant="outlined"
v-model="searchQuery" v-model="searchQuery"
density="compact" density="compact"
append-inner-icon="mdi-magnify"></v-text-field> append-inner-icon="mdi-magnify"></v-text-field>
<v-switch color="blue" label="Show Only Active" v-model="showActive"></v-switch> <v-row justify="space-between">
<v-btn v-if="site_info.features.addcomplaint" color="warning">+ Add</v-btn> <v-col cols=4>
</v-responsive> <v-btn v-if="site_info.features.addcomplaint" color="warning" prepend-icon="mdi-plus" variant="text" @click="showAddComplaint">Add</v-btn>
<v-table> </v-col>
<thead> <v-col cols=3>
<tr> <v-btn align="end" variant="text" @click="showActive = !showActive">
<th>Comp No</th> Showing <span v-if="showActive">Active Only</span><span v-else>Inactive</span>
<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> </v-btn>
</td> </v-col>
</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>
<v-row dense nogutters> <v-progress-linear indeterminate color="orange" :active="loading"></v-progress-linear>
<v-checkbox label="At Risk" v-model="complaint.at_risk" @change="changeComplaintCheckbox(complaint)" /> <v-card variant="outlined">
<v-checkbox label="Permanent" v-model="complaint.info.permanent" @change="changeComplaintCheckbox(complaint)" /> <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-row>
</v-form> <v-btn @click="showInfo(item)" color="blue-lighten-1">View</v-btn>
</v-container> <v-btn v-if="site_info.features.editcomplaint" color="orange-lighten-2" @click="showEditComplaint(item)">Edit</v-btn>
</td> </div>
<td> </v-col>
<img v-if="complaint.info.loading" class="loading" src="/images/icons/loading.gif"/> </v-row>
<template v-else> </RecycleScroller>
<v-btn v-if="site_info.features.editcomplaint" color="warning">Edit</v-btn> </v-card>
</template> </v-col>
</td> </v-window-item>
</template> <v-window-item value="edit">
</template> <ComplaintEdit ref="edit" :setcomplaint="selected_complaint" @closetab="tab = 'isList'" @complaintupdate="complaintUpdated"></ComplaintEdit>
<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-item>
</v-window> </v-window>
<v-dialog v-model="showComplaintInfo">
<ComplaintInfo :in_complaint="selected_complaint" @return="doInfoReturn" />
</v-dialog>
</template> </template>
<script> <script>
import axios from 'axios' 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 { export default {
props: { props: {
site_info:{}, site_info:{},
user_info:{}
},
components: {
ComplaintInfo,
ComplaintEdit
}, },
data() { data() {
return { return {
tab: "list", tab: "isList",
list: [], list: [],
listreceived: false, listreceived: false,
showActive: true, showActive: true,
@ -121,7 +90,9 @@ export default {
limit: 300, limit: 300,
searchQuery: "", searchQuery: "",
edit: false, edit: false,
report: false report: false,
showComplaintInfo: false,
selected_complaint: {}
} }
}, },
computed: { computed: {
@ -134,7 +105,7 @@ export default {
q.customer.name.toLowerCase().includes(query) || q.customer.name.toLowerCase().includes(query) ||
q.customer.acc_no.includes(query) || q.customer.acc_no.includes(query) ||
q.id == query || q.id == query ||
q.reason.toLowerCase().includes(query) q.reason.reason.toLowerCase().includes(query)
) )
if (this.showActive) { if (this.showActive) {
clist = clist.filter(q => clist = clist.filter(q =>
@ -145,64 +116,65 @@ export default {
} }
}, },
methods: { methods: {
async getComplaintsList(){ getComplaintsList(){
this.loading = true this.loading = true
let url = this.$api_url + "/customers/complaints/list" let url = this.$api_url + "/customers/complaints/list"
console.log("Getting Complaint list...") let c_id = this.$route.params.id || ""
console.log("Getting Contracts list..." + c_id)
axios.get(url,{ axios.get(url,{
params: { params: {
limit: this.limit, limit: this.limit,
query: this.searchQuery query: this.searchQuery,
c_id: c_id
} }
}).then(resp => { }).then(resp => {
this.list = resp.data this.list = resp.data
this.loading = false
this.listreceived = true this.listreceived = true
}).catch(err => {
console.log(err)
}).finally(() => {
this.loading = false
}) })
}, },
async getComplaintInfo(complaint) { showInfo(complaint) {
complaint.info.loading = true this.showComplaintInfo = true
let url = this.$api_url + "/customers/complaints/" + complaint.id + "/info" this.selected_complaint = complaint
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_loaded = false
complaint.info_shown = !complaint.info_shown },
if (complaint.info_shown) { doInfoReturn(code) {
complaint.info = await this.getComplaintInfo(complaint) console.log(code)
complaint.info_loaded = true },
showAddComplaint() {
this.selected_complaint = new Complaint()
this.selected_complaint.isNew = true
this.edit = true
this.tab = "edit"
},
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"
} }
},
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
}
},
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
},
} }
} }
</script> </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,11 +1,12 @@
<template> <template>
<v-card title="Edit Contract" :subtitle="'Contract : ' + contract.no"> <v-card :title="title" :subtitle="'Contract : ' + contract.no">
<v-card-text> <v-card-text>
<v-container> <v-container>
<v-row> <v-row>
<v-col cols="6"> <v-col cols="6">
<v-text-field label="Customer" v-model="contract.customer.name" readonly></v-text-field> <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 type="number" label="Tonnage Per Month" v-model="contract.tonnage_per_month"></v-text-field> </v-text-field>
<v-text-field type="number" variant="outlined" label="Tonnage Per Month" v-model="contract.tonnage_per_month"></v-text-field>
</v-col> </v-col>
<v-col cols="6"> <v-col cols="6">
<label> <label>
@ -18,33 +19,36 @@
</label><br /> </label><br />
</v-col> </v-col>
</v-row> </v-row>
<template v-for="p in contract.products" :key="p.code"> <v-table density="compact">
<v-row> <thead>
<v-col cols="6"> <tr>
<v-autocomplete v-model="p.code" <th style="width:65%;">
v-model:search="product_search" Product
:loading="products_loading" </th>
:items="products" <th style="width:20%">
cache-items Price
hide-no-data </th>
hide-details <th>
solo-inverted </th>
label="Code" </tr>
no-data-text="No Products Found" </thead>
item-title="code" <tbody>
item-value="code" ></v-autocomplete> <tr v-for="(p, index) in contract.products" :key="index">
</v-col> <td>
<v-col cols="4"> <v-text-field readonly variant="outlined" prepend-inner-icon="mdi-magnify" @click="showProductSearch(index)" :model-value="p.code + ' - ' + p.name"></v-text-field>
<v-text-field type="number" label="Price" v-model="p.price"></v-text-field> </td>
</v-col> <td>
<v-col cols="2"> <v-text-field prepend-icon="mdi-currency-gbp" type="number" density="compact" variant="outlined" v-model="p.price"></v-text-field>
<v-btn size="small" color="error" title="Remove" variant="plain" icon @click="contract.products.pop()"><v-icon>mdi-minus</v-icon></v-btn> </td>
</v-col> <td>
</v-row> <v-btn size="small" color="error" title="Remove" variant="plain" @click="removeProduct(p.code)" icon="mdi-minus"></v-btn>
</template> </td>
</tr>
</tbody>
</v-table>
<v-row> <v-row>
<v-col cols="4"> <v-col cols="4">
<v-btn @click="contract.products.push({})" v-if="contract.products.length < 4">+ Add Product</v-btn> <v-btn @click="addProduct()" v-if="contract.products.length < 4">+ Add Product</v-btn>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
@ -55,107 +59,145 @@
</label> </label>
</v-col> </v-col>
<v-col cols="12"> <v-col cols="12">
<v-textarea rows=3 label="Comments" v-model="contract.comments"></v-textarea> <v-textarea rows=3 label="Comments" variant="outlined" v-model="contract.comments"></v-textarea>
<v-textarea rows=3 label="Office Comments" v-model="contract.office_comments"></v-textarea> <v-textarea rows=3 label="Office Comments" variant="outlined" v-model="contract.office_comments"></v-textarea>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
</v-card-text> </v-card-text>
<DebugPanel :data="contract"></DebugPanel>
<ErrorBanner :errors="errors" />
<v-card-actions> <v-card-actions>
<v-btn color="red-darken-1" <v-btn v-if="!contract.isNew" color="red-darken-1"
variant="text" variant="text"
:loading="saving"
@click="saveContract(selected_contract)">Save</v-btn> @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-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="blue-darken-1" <v-btn color="blue-darken-1"
variant="text" variant="text"
@click="close">Close</v-btn> @click="close">Close</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </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> </template>
<script> <script>
import axios from 'axios' import axios from 'axios'
import DatePicker from '@vuepic/vue-datepicker' 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 { export default {
props: { 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: { components: {
DatePicker DatePicker,
ErrorBanner,
ProductSearch,
CustomerSearch
}, },
watch: { watch: {
setcontract(newval) { setcontract(newval) {
this.contract = newval this.contract = newval
}, },
product_search(val) {
if (val && val.length > 1) {
this.searchProducts(val)
}
}
}, },
mixins: [methods],
data() { data() {
return { return {
contract: this.setcontract, contract: this.setcontract,
dialog: this.opendialog, dialog: this.opendialog,
saving: false, saving: false,
search: [],
searchProdIndex: null,
product_search: null, product_search: null,
products_loading: false, products_loading: false,
products: [], 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: { methods: {
close() { close() {
this.$emit('closetab','list') 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(){ async saveContract(){
this.errors = []
this.saving = true this.saving = true
let url = this.$api_url + "/customers/contracts/" + this.contract.no + "/save" let url = this.$api_url + "/customers/contracts/" + this.contract.no + "/save"
console.log("Saving Contract : ", this.contract.no) if (this.contract.isNew) {
console.log(this.contract) url = this.$api_url + "/customers/contracts/add"
}
axios.post(url, { axios.post(url, {
contract: this.contract contract: this.contract
}).then(resp => { }).then(resp => {
console.log("Saved Contract : " + JSON.stringify(resp.data)) console.log("Saved Contract : " + JSON.stringify(resp.data))
this.saving = false this.saving = false
let stat = resp.data
if (stat.status == true ) {
if (this.contract.isNew) {
this.$emit('contractupdate', resp.data) this.$emit('contractupdate', resp.data)
} else {
this.$emit('contractupdate', resp.data)
}
} else {
this.errors.push("Contract not saved.")
console.log("Not Saved")
}
}).catch(err => { }).catch(err => {
console.log(err) console.log(err)
this.saving = false 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) { productCodeName(p) {
return p.code + ' - ' + p.name 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,21 +7,22 @@
</v-tabs> </v-tabs>
<v-window v-model="tab" > <v-window v-model="tab" >
<v-window-item value="list"> <v-window-item value="list">
<v-responsive <v-row>
max-width="500" <v-col cols="8" xs="12" sm="12" md="12" lg="8">
>
<v-text-field clearable <v-text-field clearable
label="Search" label="Search"
variant="outlined" variant="outlined"
v-model="searchQuery" v-model="searchQuery"
density="compact" density="compact"
append-inner-icon="mdi-magnify"></v-text-field> append-inner-icon="mdi-magnify"></v-text-field>
</v-responsive> <v-btn color="warning"
<v-dialog v-model="showDialog"> v-if="site_info.features.addcontract"
</v-dialog> @click="showEditContract()"
<v-row> prepend-icon="mdi-plus"
<v-col cols="8" xs="12" sm="12" md="8"> variant="text"
>Add</v-btn>
<v-progress-linear indeterminate color="blue" :active="loading"></v-progress-linear> <v-progress-linear indeterminate color="blue" :active="loading"></v-progress-linear>
<v-card variant="outlined">
<RecycleScroller class="scroller" <RecycleScroller class="scroller"
:items="filteredContracts" :items="filteredContracts"
:item-size="130" :item-size="130"
@ -52,8 +53,7 @@
= {{ formatNumber(item.tonnage_per_month * item.remaining_duration,2) }} remaining<br/> = {{ formatNumber(item.tonnage_per_month * item.remaining_duration,2) }} remaining<br/>
</v-col> </v-col>
<v-col> <v-col>
<v-row> <div class="d-flex justify-space-around align-center flex-column flex-sm-row fill-height">
<v-col cols="12">
<v-btn color="info"> <v-btn color="info">
More More
<v-overlay activator="parent" class="align-center justify-center"> <v-overlay activator="parent" class="align-center justify-center">
@ -77,7 +77,7 @@
target="blank" target="blank"
@click="getContractPrint(item.no)" @click="getContractPrint(item.no)"
class="ma-2 pa-2" class="ma-2 pa-2"
:loading="item.multiloading" :loading="multiloading"
> >
Multi Multi
</v-btn> </v-btn>
@ -85,7 +85,7 @@
<v-btn color="info" <v-btn color="info"
target="blank" target="blank"
@click="getContractPrint(item.no,true)" @click="getContractPrint(item.no,true)"
:loading="item.totalloading" :loading="totalloading"
class="ma-2 pa-2" class="ma-2 pa-2"
> >
Total Total
@ -93,6 +93,7 @@
<v-btn color="warning" <v-btn color="warning"
v-if="site_info.features.editcontract" v-if="site_info.features.editcontract"
@click="showEditContract(item)" @click="showEditContract(item)"
:loading="editloading"
> >
Edit Edit
</v-btn> </v-btn>
@ -100,19 +101,18 @@
</v-card> </v-card>
</v-overlay> </v-overlay>
</v-btn> </v-btn>
</v-col>
<v-col cols="12">
<v-btn color="warning" <v-btn color="warning"
v-if="site_info.features.editcontract" v-if="site_info.features.editcontract"
@click="showEditContract(item)" @click="showEditContract(item)"
:loading="editloading"
> >
Edit Edit
</v-btn> </v-btn>
</v-col> </div>
</v-row>
</v-col> </v-col>
</v-row> </v-row>
</RecycleScroller> </RecycleScroller>
</v-card>
</v-col> </v-col>
</v-row> </v-row>
</v-window-item> </v-window-item>
@ -123,11 +123,14 @@
<ContractMulti :contract="selected_contract" :total="total" /> <ContractMulti :contract="selected_contract" :total="total" />
</v-window-item> </v-window-item>
</v-window> </v-window>
<v-dialog v-model="showDialog">
</v-dialog>
</template> </template>
<script> <script>
import ContractEdit from './ContractEdit.vue'; import ContractEdit from './ContractEdit.vue';
import ContractMulti from './ContractMulti.vue'; import ContractMulti from './ContractMulti.vue';
import methods from '@/CommonMethods.vue' import methods from '@/CommonMethods.vue'
import ContractType from '@/types/ContractType.vue';
import axios from 'axios' import axios from 'axios'
export default { export default {
props: { props: {
@ -179,25 +182,37 @@ export default {
edit: false, edit: false,
report: false, report: false,
total: false, total: false,
content: "" content: "",
editloading: false,
multiloading: false,
totalloading: false
} }
}, },
mixins: [methods], mixins: [methods],
methods: { methods: {
async showEditContract(contract) { async showEditContract(contract) {
this.editloading = true
setTimeout(() => {
if ( contract != undefined ) {
this.selected_contract = contract this.selected_contract = contract
//this.selected_contract = contract } else {
this.selected_contract = new ContractType()
}
this.tab = "edit" this.tab = "edit"
this.edit = true this.edit = true
this.editloading = false
},1000)
}, },
async getContractsList() { async getContractsList() {
this.loading = true this.loading = true
let url = this.$api_url + "/customers/contracts/list" let url = this.$api_url + "/customers/contracts/list"
console.log("Getting Contracts list...") let c_id = this.$route.params.id || ""
console.log("Getting Contracts list..." + c_id)
axios.get(url, { axios.get(url, {
params: { params: {
limit: this.limit, limit: this.limit,
query: this.searchQuery query: this.searchQuery,
c_id: c_id
} }
}) })
.then(resp => { .then(resp => {
@ -207,12 +222,16 @@ export default {
}) })
}, },
getContractPrint(contract, total = false) { getContractPrint(contract, total = false) {
if (total){ this.totalloading = true } else { this.multiloading = true }
axios.get(this.$api_url + "/customers/contracts/" + contract + "/info") axios.get(this.$api_url + "/customers/contracts/" + contract + "/info")
.then(resp => { .then(resp => {
this.selected_contract = resp.data this.selected_contract = resp.data
this.total = total this.total = total
this.tab = "report" this.tab = "report"
this.report = true this.report = true
}).finally(() => {
this.totalloading = false
this.multiloading = false
}) })
}, },
findContract(id) { findContract(id) {
@ -249,7 +268,7 @@ export default {
</script> </script>
<style scoped> <style scoped>
.scroller { .scroller {
height: 600px; height: 70vw;
} }
.item { .item {

View file

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

View file

@ -1,33 +1,76 @@
<template> <template>
<h3>Customer List</h3>
<v-responsive
max-width="500"
>
<v-text-field <v-text-field
clearable
label="Search" label="Search"
variant="outlined" variant="outlined"
v-model="searchQuery" v-model="searchQuery"
density="compact" density="compact"
append-inner-icon="mdi-magnify"></v-text-field> append-inner-icon="mdi-magnify"></v-text-field>
</v-responsive> <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 <RecycleScroller
class="scroller" class="scroller"
:items="filteredCustomers" :items="filteredCustomers"
:item-size="50" :item-size="60"
key-field="acc_no" key-field="acc_no"
v-slot="{ item }" v-slot="{ item }"
> >
<v-row class="customer"> <v-list-item >
<v-col cols="6"> <v-sheet class="clickable" @click="getCustomerInfo(item)" rounded :class="{ 'bg-error' : item.at_risk }">
{{ item.acc_no }} - {{ item.name }} <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-col>
</v-row> </v-row>
<hr />
</RecycleScroller>
</template> </template>
<script> <script>
import axios from 'axios'; import axios from 'axios';
import RecentOrders from '@/components/RecentOrders.vue'
import CustomerComments from '@/components/CustomerComments.vue'
import Customer from '@/types/CustomerType.vue'
export default { export default {
props: { props: {
site_info: {}, site_info: {},
@ -36,15 +79,16 @@ export default {
data() { data() {
return { return {
searchQuery: "", searchQuery: "",
showSearch: true, customer_list: [],
open: false, customers_loading: null,
list: [], selected_cust: new Customer(),
limit: 5000, limit: 5000,
loading: true, listreceived: false,
listreceived: false
} }
}, },
components: { components: {
RecentOrders,
CustomerComments
}, },
computed: { computed: {
filteredCustomers() { filteredCustomers() {
@ -52,38 +96,58 @@ export default {
if (!this.listreceived){ if (!this.listreceived){
this.getCustomerList() this.getCustomerList()
} }
let clist = this.list.filter(q => let clist = this.customer_list.filter(q =>
q.name.toLowerCase().includes(query) || q.name.toLowerCase().includes(query) ||
q.acc_no.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)
) )
return clist return clist
}, },
}, },
methods: { 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() { async getCustomerList() {
this.loading = true this.customers_loading = true
let url = this.$api_url + "/customers/list" let url = this.$api_url + "/customers/list"
axios axios
.get(url,{ .get(url,{
params: { limit: this.limit, query: this.searchQuery }}) params: {
limit: this.limit,
query: "" }})
.then(resp => { .then(resp => {
this.list = resp.data this.customer_list = resp.data
this.listreceived = true this.listreceived = true
this.loading = false
}) })
.catch(error => (console.log(error))) .catch(error => (console.log(error)))
.finally(() => {
this.customers_loading = false
})
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.scroller { .scroller {
height: 500px; height: 300px;
} }
.customer { .scroller.small {
height: 32%; height: 200px;
padding: 0 12px;
display: flex;
align-items: center;
} }
</style> </style>

View file

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

View file

@ -74,6 +74,7 @@
size is 2 tonnes.</p> size is 2 tonnes.</p>
<p>If you require any further information about the veterinary <p>If you require any further information about the veterinary
medicinal products we stock, please get in touch.</p> 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> <h4>The customer requires this order on {{ formatDate(mf.date_required,"DD/MM/yyyy") }}</h4>
</main> </main>
</div> </div>

View file

@ -0,0 +1,34 @@
<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>

135
src/views/salesorders/\ Normal file
View file

@ -0,0 +1,135 @@
<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 Normal file
View file

@ -0,0 +1,34 @@
### 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

File diff suppressed because it is too large Load diff