06. Admin Additional Features
Admin will have additional features. Admin will have access to approve new manager requests, approval of each requests created by manager. Admin can delete and existing manager so to keep control on the application data.
Managing Managers
Managers View
We will create a page to list of the managers. A page where admin can see all the managers and do some actions
vue filename=src/components/ManagersCompo.vue
<template>
<div class="container">
<div class="row">
<!-- Repeat the following structure for each manager -->
<div class="col-md-4">
<div v-for="item in this.$store.state.managers" :key="item.id" class="manager-profile">
<!-- Profile Icon -->
<img :src="'data:image/jpeg;base64,' + item.image" alt="Manager Profile" class="profile-icon">
<!-- Basic Info -->
<div class="basic-info">
<p>{{ item.email }}</p>
<p>Date of Joining: {{ item.doj }}</p>
<p>Years of Service: {{ item.exp }}</p>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<button class="btn btn-danger" @click="deletemanager(item.id)">Delete</button>
<button class="btn btn-warning" @click="warn">Send Warning</button>
</div>
</div>
<div v-if="this.$store.state.managers.length == 0">
<h5>No Managers Found</h5>
</div>
</div>
</div>
</div>
</template>
<script type="module">
import { deleteManager } from '../services/apiServices';
export default {
data() {
return {
email: '',
name: '',
password: '',
role: '',
message: ''
}
},
methods: {
warn() {
if (this.$route.path != '/admin/warning') {
this.$router.push('/admin/warning')
}
},
async deletemanager(id) {
if (confirm("Are you sure?")) {
try {
const data = await deleteManager(id);
this.$store.commit('deleteManager', data.resource)
} catch (error) {
console.error(error.msg);
}
}
}
},
mounted() {
this.$store.dispatch('fetchManagers')
}
}
</script>
In this component, admin can see all the managers, send them warning for their mistakes and can also fire/delete any manager.
For sending the warning we will design a separate template for the warning page.
Warning Page
vue filename=src/components/SendWarning.vue
<template>
<div class="row justify-content-center m-3 text-color-light">
<div class="card bg-light" style="width: 36rem;">
<div class="card-body">
<div class="d-flex justify-content-end">
<button type="button" class="btn-close" aria-label="Close" @click="closeCard"></button>
</div>
<h5 class="card-title">Send Warning</h5>
<form @submit.prevent="sendWarning" enctype="multipart/form-data">
<label class="form-label" for="message">message:</label>
<textarea class="form-control" v-model="message" type="text" id="message" name="message" required></textarea>
<br>
<label class="form-label" for="Select Manager">Select Manager:</label>
<select class="form-select" name="Select Category" id="Select Manager" v-model="managers.email" required>
<option v-for="manager in managers" :key="manager.id" :value="manager.email">{{ manager.email }}</option>
</select>
<br>
<input type="submit" class="btn btn-outline-primary" value="Send">
</form>
</div>
</div>
</div>
</template>
<script type="module">
import home from '../utils/navigation.js';
import { fetchWarnings, sendWarning } from '../services/apiServices';
export default {
data() {
return {
managers: {
id: '',
name: '',
email: '',
},
message: ''
}
},
methods: {
closeCard() {
home(this.$store, this.$route, this.$router);
},
async fetchWarnings() {
try {
const data = await fetchWarnings()
this.managers = data
} catch (error) {
console.error(error.msg);
}
},
async sendWarning() {
if (confirm("Are you sure?")) {
try {
const data = await sendWarning();
this.closeCard()
} catch (error) {
console.error(error.msg);
}
}
}
},
mounted() {
this.fetchManagers()
}
}
</script>
But before moving to the implementation of the backend we will have to create these api services in the apiServices.js file.
sendWarning
deleteManager
js filename=static/services/apiServices.js
...
async function sendWarning() {
return await fetch(`${API_BASE}/send/alert`, {
method: 'POST',
credentials: 'include',
headers: getHeader()
}).then(handleResponse);
}
async function deleteManager(id) {
return await fetch(`${API_BASE}/delete/manager/${id}`, {
method: 'DELETE',
credentials: 'include',
headers: getHeader()
}).then(handleResponse);
}
async function fetchManagers() {
return await fetch(`${API_BASE}/get/managers`, {
method: 'GET',
credentials: 'include',
headers: getHeader()
}).then(handleResponse);
}
async function fetchWarnings() {
return await fetch(`${API_BASE}/send/alert`, {
method: 'GET',
credentials: 'include',
headers: getHeader()
}).then(handleResponse);
}
...
export {
...
sendWarning,
deleteManager,
fetchManagers,
fetchWarnings,
}
And will also have to implement these actions in the store/index.js file.
js filename=static/store/index.js
const store = new Vuex.Store({
state: {
...
}
...
mutations: {
...
addManager: (state, newManager) => {
state.managers.push(newManager);
},
updateManager: (state, updatedManager) => {
const index = state.managers.findIndex((m) => m.id === updatedManager.id);
if (index !== -1) {
state.managers.splice(index, 1, updatedManager);
}
},
setManagers: (state, managers) => {
state.managers = managers;
},
}
actions: {
...
async fetchManagers({ commit }) {
try {
const response = await fetchManagers();
commit("setManagers", data.resource);
} catch (error) {
console.error(error);
}
},
}
})
export default store;
So far we have not implement the UI for showing the products to the admin, manager and user. So let's add the UI for showing the products to the admin, manager and user.
Product UI Component For Admin and Manager
vue filename=src/components/ProductCompo.vue
<template>
<div class="container d-flex justify-content-center mt-2">
<div class="row gap-2">
<div v-for="item in this.$store.state.products" :key="item.id"
class="card shadow p-3 mb-5 bg-body-tertiary rounded" style="width: 18rem;">
<img :src="'data:image/jpeg;base64,' + item.image" class="card-img-top img-thumbnail" :alt="item.name">
<div class="card-body">
<h5 class="card-title">{{ item.name }}</h5>
<p class="card-text"> Price: ₹ {{ item.rpu }}</p>
<p class="card-text">{{ item.description }}</p>
<button class="btn btn-dark" @click="editPro(item.id)">Edit</button>
</div>
<div v-if="item.avg_rate" class="card-footer">
<font-awesome-icon v-for="n in item.avg_rate" :key="n" :icon="['fa', 'star']" />
<font-awesome-icon :icon="['fas', 'user-secret']" />
</div>
</div>
<div v-if="this.$store.state.products.length == 0">
<h5>No Products</h5>
</div>
</div>
</div>
</template>
<script>
export default {
methods: {
editPro(id) {
if (this.$route.path != '/admin/pro/edit/' + id) {
this.$router.push('/admin/pro/edit/' + id)
}
},
},
mounted() {
this.$store.dispatch('fetchProducts')
}
}
</script>
Here you can see that we are showing the products and it has a button to Edit the product which we use to switch to edit product page.
Product UI Component For User
vue filename=src/components/ProductUserCompo.vue
<template>
<div class="container d-flex justify-content-center mt-2" style="margin-bottom: 100px; overflow-y: scroll;">
<div class="row gap-2">
<!-- card -->
<div v-for="item in this.$store.state.products" :key="item.id"
class="card shadow p-3 mb-5 bg-body-tertiary rounded" style="width: 18rem;">
<img :src="'data:image/jpeg;base64,' + item.image" class="card-img-top img-thumbnail" :alt="item.name">
<div class="card-body">
<h5 class="card-title">{{ item.name }}</h5>
<p class="card-text"> Price: ₹ {{ item.rpu }}</p>
<p class="card-text">{{ item.description }}</p>
<button v-if="item.quantity > 0" class="btn btn-outline-primary"
@click="addToCart(item.id, item.name, item.rpu)">Add to cart</button>
<button v-else class="btn btn-danger" disabled>out of stock</button>
</div>
<div v-if="item.avg_rate" class="card-footer">
<font-awesome-icon v-for="n in item.avg_rate" :key="n" :icon="['fa', 'star']" />
</div>
</div>
</div>
</div>
</template>
<script>
export default {
methods: {
async addToCart(id, name, rpu) {
try {
const response = await fetch('http://127.0.0.1:5000/add/to/cart', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
},
body: JSON.stringify({
"id": id,
'name': name,
'rpu': rpu
}),
});
if (response.status === 201) {
const data = await response.json();
console.log(data.resource)
this.$store.commit('addToCart', data.resource)
} else if (response.status === 209) {
const data = await response.json();
alert(data.message);
this.$store.commit('updateToCart', data.resource)
} else if (response.status === 200) {
const data = await response.json();
alert(data.message);
}
else {
const data = await response.json();
alert(data.message);
}
} catch (error) {
console.error(error);
}
}
},
mounted() {
this.$store.dispatch('fetchProducts')
}
}
</script>
For user page we have a button to add to cart which we use to add the product to cart. Also we are using star icon for showing average rating of the product.
In both of the components we are calling fetchProducts action to fetch the products from the backend. To implement this action we will write the api call in the apiService.js file. and define the action in the store/index.js file.
Let's update the code in the apiService.js file. We will not add the fetchProducts instead we will write all the fetch calls which are resposible to fetch all the data related with application for initial load. At inital load we will fetch products, categories.
js filename=static/services/apiServices.js
...
async function fetchCategories() {
return await fetch(`${API_BASE}/get/categories`, {
method: 'GET',
credentials: 'include',
headers: getHeader()
}).then(handleResponse);
}
async function fetchProducts() {
return await fetch(`${API_BASE}/get/products`, {
method: 'GET',
credentials: 'include',
headers: getHeader()
}).then(handleResponse);
}
export {
...
fetchCategories,
fetchProducts,
};
Now we will implement the calls in the actions of the store.
js filename=static/store/index.js
const store = createStore({
state: {
products: [],
categories: [],
authenticatedUser: "" // [tl! add]
},
getters: {
// write your code here
},
mutations: {
...
setAuthenticatedUser: (state, user) => {
state.authenticatedUser = user;
},
},
actions: {
// write your code here
async fetchProducts({ commit }) { // [tl! add: start]
try {
const data = await fetchProducts();
commit("setProducts", data);
} catch (error) {
console.error(error);
}
},
async fetchCategories({ commit }) {
try {
const data = await fetchCategories();
commit("setCategories", data);
} catch (error) {
console.error(error);
}
}, // [tl! add: end]
},
});
export default store;
Now we have the CRUD for Category and Products.
Authticated User
I want to implement a feature such that when someone try to access frontend without login, it will make call to the backend to check if the user is currently authenticated or not. If the user is authenticated then it will redirect to the dashboard else it will redirect to the login page.
For that we write a api call in the apiService.js file and call it from the action of the store. Skilled in full-stack development with
Implementing API service
js filename=static/services/apiServices.js
...
async function fetchAuthUser() {
return await fetch(`${API_BASE}/auth/user`, {
method: 'GET',
credentials: 'include',
headers: getHeader()
}).then(handleResponse);
}
export {
...
fetchAuthUser,
}
Now implement the calls in the actions of the store.
js filename=static/store/index.js
const store = createStore({
state: {
...
}
...
actions: {
...
async fetchAuthUser({ commit }) {
try {
const data = await fetchAuthUser();
commit("setAuthenticatedUser", data.resource);
} catch (error) {
console.error(error);
}
},
}
})
In this utility folder we create helpers for our application.
I want to call this `fetchAuthUser` action whenever someone will try to access any of these dashboards. Admin, Manager or User. Since we have already created a basic common layout `DashCompo.vue` so we directly call this method in the `DashCompo.vue` file in mounted option.
js filename=src/components/DashCompo.vue ...
Now this action will check if the user is authenticated or not and redirect to the login page if the user is not authenticated.
### Backend implementation
python filename=main.py ...
class AuthUser(Resource): def get(self): verify_jwt_in_request() id = get_jwt_identity() current_user = User.query.filter_by(id=id).first() if not current_user: # if the user doesn't exist or password is wrong, reload the page return {'error': 'wrong credentials'}, 404 else: user_data = { 'id': current_user.id, 'role': current_user.role, 'email': current_user.email, 'image': base64.b64encode( current_user.image ).decode('utf-8') if current_user.image else None } return {'msg': 'User login successfully', 'resource': user_data}, 200 ...
api.add_resource(AuthUser, '/auth/user', '/update/profile/
## Profile Update
We will add profile update feature in the dashboard of each type of users. Since it is common feature for all type fo user so we will add this in the `DashCompo.vue` file.
js filename=src/views/DashCompo.vue
### Backend Implementation
python filename=main.py
...
class AuthUser(Resource):
...
@jwt_required()
def put(self, id):
user = User.query.filter_by(id=id).first()
if user:
# Handle file upload
user.image = request.files['image'].read()
db.session.commit()
user_data = {
'id': user.id,
'role': user.role,
'email': user.email,
# Assuming image is stored as a base64-encoded string
'image': base64.b64encode(user.image).decode('utf-8')
}
return {
'msg': f"User profile updated successfully in the database",
'resource': user_data}, 201
else:
return {'msg': "Not found"}, 404
...
This `@jwt_required()` decorator ensures that only authenticated users can access this route.
Now all type of users will be able to update profile picture.
## Filter by Category
Again this is common feature for all type of users. So we will add it into the `DashCompo.vue` file.
### Frontend Implementation
js filename=src/views/DashCompo.vue
One thing you might have noticed that we have created a slot with name edit i.e `<slot name="edit" :categoryId="category.id"></slot>`. This slot is used to render the edit button for each category. Since Admin and Manager should have this option not user.
### Backend Implementation
python filename=main.py ... class SearchCatResource(Resource): #[tl! add:start] def post(self): data = request.get_json() query = data.get('query') # Search for products and categories based on the query products = Product.query.filter(or_( Product.name.ilike(f'%{query}%'), Product.rpu.ilike(f'%{query}%'), Product.manufacture.ilike(f'%{query}%'), Product.description.ilike(f'%{query}%'), Product.expiry.ilike(f'%{query}%'), Product.category.has(Category.name.ilike(f'%{query}%')) )).all() category = Category.query.filter_by(name=query).first() products = Product.query.filter_by(category_id=category.id).all() product_list = [] categories = [] for new_product in products: prod_data = { 'id': new_product.id, 'quantity': new_product.quantity, 'name': new_product.name, 'manufacture': new_product.manufacture.strftime("%Y-%m-%d"), 'expiry': new_product.expiry.strftime("%Y-%m-%d"), 'description': new_product.description, 'rpu': new_product.rpu, 'unit': new_product.unit, # Assuming image is stored as a base64-encoded string 'image': base64.b64encode(new_product.image).decode('utf-8') } if new_product.category not in categories: categories.append(new_product.category) product_list.append(prod_data) categories_list = [] for category in categories: cat = { 'id': category.id, 'name': category.name, } categories_list.append(cat) return {"cat": categories_list, 'pro': product_list}, 200 # [tl! add:end] ... api.add_resource(SearchCatResource, '/search/by/catgory') #[tl! add] ...
## Testing it out
Now you can start the backend and frontend and test all the features whatever we have implemented so far.
To start the backend run the following command:
shell python3 main.py
To start the frontend run the following command:
shell npm run dev
Let's commit the change using the following command.
shell git add . git commit -m "database integrated" ```