06. Admin Rights
Update Profile Picture
This feature is not specific to any particular role, so we will add it to the DashCompo
so that it will be available to all of the three dashboards.
Note Dealing with images requires understanding how SQLite stores images. It stores media data as
BLOB
, which is a binary large object. Note that storing images in the database can slow down queries, increase the size of the database file, and make backups more difficult.
Let's update the code.
js filename=static/components/DashCompo.js
const DashCompo = {
name: "DashCompo",
template: `
<div>
...
<main>
<div class="sidebar bg-black">
<div class="sidebar-icon mb-5">
<!-- Add your icon or content here -->
<span
style="color: #343a40; font-size: 24px; display: inline-block; overflow: hidden; width: 100px; height: 100px; border-radius: 50%;">
<img v-if="this.$store.state.authenticatedUser.image"
:src="'data:image/jpeg;base64,' + this.$store.state.authenticatedUser.image" alt="Profile"
style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%;">
<i v-else class="fas fa-user" style="font-size: 100%;"></i>
</span>
<a class="pointer-on-hover" @click="change">change</a>
<form v-if="picUpdate" @submit.prevent="updatePic" enctype="multipart/form-data">
<label class="form-label" for="image">upload</label>
<input class="form-control" type="file" id="image" @change="handleFileUpload" accept="image/*" required>
<br>
<input type="submit" class="btn btn-outline-secondary" value="update">
</form>
</div>
<!-- Add any sidebar content here -->
</div>
<router-view></router-view>
</main>
...
</div>
`,
data() {
return {
picUpdate: false,
profilePic: null
};
},
methods: {
change() {
if (this.picUpdate == false) {
this.picUpdate = true;
}
},
async updatePic() {
const formData = new FormData();
formData.append("image", this.profilePic);
try {
const response = await fetch(
"http://127.0.0.1:5000/update/profile/" +
this.$store.state.authenticatedUser.id,
{
method: "PUT",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: formData,
}
);
if (response.status === 201) {
const data = await response.json();
console.log(data.resource);
this.$store.commit("setAuthenticatedUser", data.resource);
this.picUpdate = false;
} else {
alert(data.message);
}
} catch (error) {
console.error(error);
}
}
},
mounted() {
...
},
};
export default DashCompo;
The feature is now available for all the dashboards.
Note The code above uses the
fetch
API to send a request to the server to update the profile picture. If you are not familiar with thefetch
API, you can learn about it heredeveloper.mozilla.org/en-US/docs/Web/API/Fetch_API
. Note that we cannot have an HTML form with direct submission when using thefetch
API. Instead, we need to manually create aFormData
object and send it in the request.
The list of things that you should know are here:
-
The
handleFileUpload
function will take the image file and store it in theprofilePic
variable. When thechange
button is clicked, thepicUpdate
variable will be set totrue
, which will display the form for updating the profile picture. -
The current user information is stored in the Vuex store, allowing us to access it via
this.$store.state.authenticatedUser
. To update the user's information, such as after changing a profile picture, we can usethis.$store.commit("setAuthenticatedUser", data.resource)
. This action will modify the store's state, triggering an automatic UI update to reflect the changes.
We need to store the user data and authentication token in the Vuex store. Let's update the code in the index.js
file of the static/store
folder to include the user state and a mutation to set it.
js filename=static/store/index.js
const store = new Vuex.Store({
state: {
authenticatedUser: "", // [tl! add]
},
getters: {
// write your code here
},
mutations: {
// write your code here
setAuthenticatedUser: (state, user) => {
// [tl! add:start]
state.authenticatedUser = user;
}, // [tl! add:end]
},
actions: {
// write your code here
},
});
export default store;
Note Vuex is a state management library for Vue.js. It helps you manage the state of your application in a predictable and maintainable way. The way Vuex organizes the code is a bit different from the way we use VueX in our Vue app. All the methods related to add, update, and delete are defined in the mutations section. For more information about Vuex, you can refer to the official documentation
vuex.vuejs.org/guide/
.
Backend Implementation
Now that the frontend implementation is complete, we can focus on implementing the backend logic.
python filename=app.py
...
# ---------------------- All operational endpoints --------------#
main = Blueprint('main', __name__) # [tl! add:start]
@main.route('/update/profile/<int:id>', methods=['PUT'])
@jwt_required()
def update_profile(id):
if isinstance(id, int):
if request.method == 'PUT':
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 jsonify({'message': "User profile updated successfully\
in the database",
'resource': user_data}), 201
else:
return jsonify({'message': "Not found"}), 404
else:
return '', 400 # [tl! add:end]
...
app.register_blueprint(main)
...
Add Category
Now we're going to add the feature where the admin can add categories through the application. To add any new features, we'll need to update the following components of the application:
-
- Update the database in the
database.py
file.
- Update the database in the
-
- Create a new component or modify an existing component in the Vue application.
-
- Update Vue Router and/or Vuex if necessary.
-
- Create a new route in the
app.py
file.
- Create a new route in the
Database Update
For storing the categories we will create a new table in the database. The table will have the following structure:
python filename=database.py
...
class Category(db.Model): # [tl! add:start]
__tablename__ = 'category'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, nullable=False) # [tl! add:end]
Create Category Component
This Create Category Component will allow the admin to submit the database from frontend to backend for adding a new category.
js filename=static/components/CreateCatCompo.js
const CreateCatCompo = {
name: "CreateCatCompo",
template: `
<div class="row justify-content-center m-3 text-color-light">
<div class="card bg-light" style="width: 18rem;">
<div class="card-body">
<div class="d-flex justify-content-end">
<!-- Cross button to close the card -->
<button type="button" class="btn-close" aria-label="Close" @click="closeCard"></button>
</div>
<h5 class="card-title">Add Category</h5>
<form @submit.prevent="addcategory">
<div class="mb-3">
<label for="name" class="form-label">Category Name</label>
<input type="text" class="form-control" v-model="name" required>
<div v-if="message" class="alert alert-warning">
{{message}}
</div>
</div>
<button type="submit" class="btn btn-outline-primary">Add</button>
</form>
</div>
</div>
</div>
`,
data() {
return {
name: "",
message: "",
};
},
methods: {
closeCard() {
if (this.$store.state.authenticatedUser.role === "admin") {
if (this.$route.path != "/app/admin") {
this.$router.push("/app/admin");
}
} else {
if (this.$route.path != "/app/manager") {
this.$router.push("/app/manager");
}
}
},
async addcategory() {
try {
const response = await fetch("http://127.0.0.1:5000/add/cat", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: JSON.stringify({
name: this.name,
}),
});
if (response.status === 201) {
const data = await response.json();
console.log(data.resource);
if (this.$store.state.authenticatedUser.role === "admin") {
this.$store.commit("addCat", data.resource);
} else {
this.$store.commit("addNoti", data.resource);
}
this.closeCard();
} else {
alert(data.message);
}
} catch (error) {
console.error(error);
}
},
},
};
export default CreateCatCompo;
We need to store the categories data in the Vuex store to make our application reactive. Now, let's create a list in Vuex's state to hold the categories data.
js filename=static/store/index.js
const store = new Vuex.Store({
state: {
authenticatedUser: "",
categories: [] // [tl! add]
},
getters: {
// write your code here
},
mutations: {
// write your code here
...
setCategories: (state, categories) => { // [tl! add:start]
state.categories = categories;
} // [tl! add:end]
},
actions: {
// write your code here
},
});
export default store;
We will now add the "Add Category" feature to both the admin and manager dashboards. Even though this feature is common to both users, its backend implementation will be slightly different. All CRUD operations initiated by a manager will require approval from an admin.
js filename=static/views/AdminApp.js
const AdminApp = {
...
...
<nav class="navbar navbar-expand-lg navbar-dark bg-success">
...
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link pointer-on-hover" @click="home">Home</a>
</li>
<li class="nav-item"> <!-- [tl! add:start] -->
<a class="nav-link pointer-on-hover" @click="createCat">Add category</a>
</li> <!-- [tl! add:end] -->
</ul>
...
</nav>
<main>
<div class="sidebar bg-black">
...
<ul class="list-group">
<li class="list-group-item" v-for="category in this.$store.state.categories" :key="category.id">
<input @click="searchByCat(category.name, category.id)" class="form-check-input me-1 pointer-on-hover" type="radio" :name="category.name" :value="category.id" :id="category.id" :checked="checkedValue === category.id">
<label class="form-check-label" :for="category.id"> {{ category.name }} </label>
<a class="pointer-on-hover" @click="editCat(category.id)">edit</a>
</li>
</ul>
</div>
</main>
...
methods: {
...
createCat() { // [tl! add:start]
if (this.$route.path != "/app/admin/cat/create") {
this.$router.push("/app/admin/cat/create");
}
} // [tl! add:end]
},
mounted() { // [tl! add:start]
this.$store.dispatch("fetchCategories");
} // [tl! add:end]
}
export default AdminApp;
Note We use mounted() hook to fetch the categories from the backend. What happens during the mounted phase? During the mounted() phase, the following things have already happened:
-
- The component's template has been compiled and rendered.
-
- The component's data has been initialized.
-
- The component's watchers have been set up.
-
- The component's DOM element has been created and inserted into the DOM.
Backend Implementation
python filename=app.py
...
from role_auth import role_required # [tl! add]
...
@main.route('/add/cat', methods=['POST']) # [tl! add:start]
@jwt_required()
@role_required(['admin'])
def create():
data = request.get_json()
if data:
if not Category.query.filter_by(name=data['name']).first():
category = Category(name=data['name'])
db.session.add(category)
db.session.commit()
return jsonify({
'message': f"Category {data['name']} created successfully",
'resource': {
'id': category.id,
'name': category.name
}
}), 201
abort(409, message="Resource already exists")
else:
abort(404, message="Not found") # [tl! add:end]
...
We are also implementing authorization by protecting this route with role verification. This ensures that only authorized users can access this route.
Update Category
Updating a category involves two operations: editing and deleting. Now that we have already created the table in the database, we can start by creating the edit component.
Edit Category Component
What we want to do is that whenever we click on the edit button, it should display a form to edit the category and all the fields should show the current data of the category.
js filename=static/components/EditCatCompo.js
const EditCatCompo = {
name: "EditCatCompo",
template: `
<div class="row justify-content-center m-3 text-color-light">
<div class="card bg-light" style="width: 18rem;">
<div class="card-body">
<div class="d-flex justify-content-end">
<!-- Cross button to close the card -->
<button type="button" class="btn-close" aria-label="Close" @click="closeCard"></button>
</div>
<h5 class="card-title">Add Category</h5>
<form @submit.prevent="updatecategory">
<div class="mb-3">
<label for="name" class="form-label">Category Name</label>
<input type="text" class="form-control" v-model="name" required>
<div v-if="message" class="alert alert-warning">
{{message}}
</div>
</div>
<div class="d-flex">
<button type="submit" class="btn btn-outline-primary me-5">Update</button>
</div>
</form>
</div>
</div>
</div>
`,
data() {
return {
name: "",
message: "",
};
},
methods: {
closeCard() {
if (this.$route.path != "/app/admin") {
this.$router.push("/app/admin");
}
},
async fetchcategory() {
try {
const response = await fetch(
"http://127.0.0.1:5000/update/" + this.$route.params.id,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);
if (response.status === 200) {
const data = await response.json();
this.name = data.name;
} else if (response.status === 404) {
alert(data.message);
}
} catch (error) {
console.error(error);
}
},
async updatecategory() {
if (confirm("Are you sure?")) {
try {
const response = await fetch(
"http://127.0.0.1:5000/update/" + this.$route.params.id,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: JSON.stringify({
name: this.name,
}),
}
);
if (response.status === 201) {
const data = await response.json();
console.log(data, "printed data");
if (this.$store.state.authenticatedUser.role === "admin") {
this.$store.commit("updateCategory", data.resource);
} else {
this.$store.commit("addNoti", data.resource);
}
this.closeCard();
} else {
const data = await response.json();
alert(data.message);
}
} catch (error) {
console.error(error);
}
}
},
},
mounted() {
this.fetchcategory();
},
};
export default EditCatCompo;
Note It is always a good practice to ask for user confirmation before performing any destructive actions such as updating or deleting data. This is what we are implementing using the
confirm
function. Theconfirm()
function is part of the Window interface in the browser's Document Object Model (DOM). It shows a modal dialog with an optional message and two buttons: "OK" and "Cancel". We will use this function to ask the user for confirmation before updating or deleting any data. Additionally, from now on, we will have to submit the authorization token in the header whenever we make a request to the server.
Since we are storing all the data in the VueX store, so we will have to write mutation for updating the category in the static/store/index.js
file.
js filename=static/store/index.js
...
mutations: {
...
updateCategory: (state, updatedCategory) => {
const index = state.categories.findIndex(
(p) => p.id === updatedCategory.id
);
if (index !== -1) {
// Replace the existing product with the updated one
state.categories.splice(index, 1, updatedCategory);
}
}
...
}
Backend Implementation
python filename=app.py
...
@main.route('/update/<int:cat_id>', methods=['GET', 'PUT', 'DELETE'])
@jwt_required()
@role_required(['admin'])
def update(cat_id):
if isinstance(cat_id, int):
if request.method == 'GET':
category = Category.query.filter_by(id=cat_id).first()
if category:
cat = {
'id': category.id,
'name': category.name,
}
return jsonify(cat)
else:
abort(404, message="Not found")
if request.method == 'PUT':
data = request.get_json()
category = Category.query.filter_by(id=cat_id).first()
category.name = data['name']
db.session.commit()
return jsonify({
'message': f"Category {data['name']} update successfully\
in database",
'resource': {
'id': category.id, 'name': category.name}}), 201
else:
return '', 400
...
-
Things to note here are that we are using
@jwt_required()
middleware to check for the authentication, and@role_required(['admin'])
middleware to check for the authorization of the user. There are following things you should know: -
- In route for such operations we require to pass the primary id (in this case, the id of the category) for fetching the correct row from the table.
<int: cat_id>
- In route for such operations we require to pass the primary id (in this case, the id of the category) for fetching the correct row from the table.
-
- By default the route allow
GET
method if you want to allow other methods then you need to allow explicitly.methods=['GET', 'PUT', 'DELETE']
- By default the route allow
-
- The data which you submit from the frontend can access by the
request
object from flask. For example like thisrequest.get_json()
.
- The data which you submit from the frontend can access by the
Delete Category
Deleting is just to add delete button on the EditCatCompo
itself.
Deleting UI
We will add the delete button on the EditCatCompo itself that's it.
js filename=static/components/EditCatCompo.js
const EditCatCompo = {
...
<div class="d-flex">
<button type="submit" class="btn btn-outline-primary me-5">Update</button>
<a class="btn btn-outline-danger" @click="deletecategory">Delete</a> // [tl! add]
</div>
...
methods: {
...
async deletecategory() { // [tl! add:start]
if (confirm("Are you sure?")) {
try {
const response = await fetch(
"http://127.0.0.1:5000/update/" + this.$route.params.id,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);
if (response.status === 201) {
const data = await response.json();
console.log(data, "printed data");
if (this.$store.state.authenticatedUser.role === "admin") {
this.$store.commit("deleteCategory", data.resource.id);
} else {
this.$store.commit("addNoti", data.resource);
}
this.closeCard();
} else {
const data = await response.json();
alert(data.message);
}
} catch (error) {
console.error(error);
}
}
} // [tl! add:end]
},
mounted() {
this.fetchcategory();
},
};
export default EditCatCompo;
So on clicking this delete button make a delete request to the backend.
Backend Implementation
python filename=app.py
...
@main.route('/update/<int:cat_id>', methods=['GET', 'PUT', 'DELETE'])
@jwt_required()
@role_required(['admin'])
def update(cat_id):
if isinstance(cat_id, int):
...
if request.method == 'DELETE':
category = Category.query.filter_by(id=cat_id).first()
if category:
products = Product.query.filter_by(
category_id=int(cat_id)).all()
for product in products:
carts = Cart.query.filter_by(
product_id=product.id).all()
for cart in carts:
db.session.delete(cart)
db.session.commit()
db.session.delete(product)
db.session.commit()
db.session.delete(category)
db.session.commit()
return jsonify(
{
'message': f"Category { category.name } Deleted\
successfully from database",
'resource': {
'id': category.id,
'name': category.name
}
}), 201
return jsonify({'message': "Not found"}), 404
else:
return '', 400
...
Now admin can delete the category from the database.
Create Product
Now that we have implemented the CRUD operations for categories, we can follow a similar approach to implement the CRUD operations for products, orders, and other resources in the database.
Database Update
We will create a product table in the database to store the products. Each product belongs to a category, so we will create a one-to-many relationship between the product table and the category table. This means that one category can have multiple products.
python filename=database.py
...
class Category(db.Model):
__tablename__ = 'category'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, nullable=False)
products = db.relationship('Product', backref='category', lazy=True) # [tl! add]
class Product(db.Model):
__tablename__ = 'product'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
quantity = db.Column(db.Integer, nullable=False)
avg_rate = db.Column(db.Integer)
name = db.Column(db.String, nullable=False)
description = db.Column(db.String(1000))
manufacture = db.Column(db.DateTime, nullable=False)
expiry = db.Column(db.DateTime, nullable=False)
rpu = db.Column(db.Float, nullable=False)
unit = db.Column(db.String, nullable=False)
image = db.Column(db.BLOB, nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
Create Product Component
Just as we created the category component, we will similarly create the product component.
js filename=static/components/CreateProCompo.js
const CreateProCompo = {
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">Add Product</h5>
<form @submit.prevent="addProduct" enctype="multipart/form-data">
<label class="form-label" for="name">Product Name:</label>
<input class="form-control" v-model="product.name" type="text" id="name" name="name" required>
<br>
<label class="form-label" for="quantity">Quantity:</label>
<input class="form-control" v-model="product.quantity" type="number" id="quantity" name="quantity" required>
<br>
<label class="form-label" for="manufacture">Manufacture Date:</label>
<input class="form-control" v-model="product.manufacture" type="date" id="manufacture" name="manufacture" required>
<br>
<label class="form-label" for="expiry">Expiry Date:</label>
<input class="form-control" v-model="product.expiry" type="date" id="expiry" name="expiry" required>
<br>
<label class="form-label" for="rpu">Rate Per Unit:</label>
<input class="form-control" v-model="product.rpu" type="number" id="rpu" name="rpu" step="0.01" required>
<br>
<label class="form-label" for="description">description:</label>
<textarea class="form-control" v-model="product.description" type="text" id="description" name="description" required></textarea>
<br>
<label class="form-label" for="Select Category">Select Category:</label>
<select class="form-select" name="Select Category" id="Select Category"
v-model="product.category_id" required>
<option v-for="category in this.$store.state.categories" :key="category.id" :value="category.id">{{category.name}}</option>
</select>
<label class="form-label" for="unit">Unit:</label>
<select class="form-select" name="Select Unit"
v-model="product.unit" required>
<option value="l">l</option>
<option value="ml">ml</option>
<option value="g">g</option>
<option value="kg">kg</option>
<option value="m">m</option>
<option value="cm">cm</option>
<option value="inch">inch</option>
<option value="piece">piece</option>
<option value="dozen">dozen</option>
</select>
<br>
<label class="form-label" for="image">Image:</label>
<input class="form-control" type="file" id="image" @change="handleFileUpload" accept="image/*" required>
<br>
<input type="submit" class="btn btn-outline-primary" value="Add Product">
</form>
</div>
</div>
</div>
`,
data() {
return {
product: {
name: "",
quantity: 0,
manufacture: "",
expiry: "",
rpu: 0,
unit: "",
description: "",
image: null,
category_id: "",
},
};
},
methods: {
closeCard() {
if (this.$store.state.authenticatedUser.role === "admin") {
if (this.$route.path != "/app/admin") {
this.$router.push("/app/admin");
}
} else {
if (this.$route.path != "/app/manager") {
this.$router.push("/app/manager");
}
}
},
handleFileUpload(event) {
this.product.image = event.target.files[0];
},
async addProduct() {
const formData = new FormData();
formData.append("name", this.product.name);
formData.append("quantity", this.product.quantity);
formData.append("manufacture", this.product.manufacture);
formData.append("expiry", this.product.expiry);
formData.append("rpu", this.product.rpu);
formData.append("unit", this.product.unit);
formData.append("description", this.product.description);
formData.append("image", this.product.image);
formData.append("category_id", this.product.category_id);
try {
const response = await fetch(
"http://127.0.0.1:5000/add/product",
{
method: "POST",
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${localStorage.getItem(
"token"
)}`,
},
body: formData,
}
);
if (response.status === 201) {
const data = await response.json();
console.log(data.resource);
this.$store.commit("addProduct", data.resource);
this.closeCard();
} else if (response.status === 409) {
const data = await response.json();
alert(data.message);
} else {
alert(data.message);
}
} catch (error) {
console.error(error);
}
},
},
};
export default CreateProCompo;
Let's add the product list to store list of products in the vue store.
js filename=static/store/index.js
const store = new Vuex.Store({
state: {
authenticatedUser: "",
categories: [],
products: []// [tl! add]
},
getters: {
// write your code here
},
mutations: {
// write your code here
...
setCategories: (state, categories) => {
state.categories = categories;
},
addProduct: (state, newProduct) => {// [tl! add:start]
state.products.push(newProduct);
} // [tl! add:end]
},
actions: {
// write your code here
}
});
export default store;
Now, let's add the UI of the admin dashboard. We will create a component for listing products on the admin dashboard. Let's create a file ProductCompo.js
in the static/components
folder.
js filename=static/components/ProductCompo.js
const ProductCompo = {
name: "ProductCompo",
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">
<i v-for="n in item.avg_rate" :key="n" class="fas fa-star"></i>
</div>
</div>
<div v-if="this.$store.state.products.length == 0">
<h5>No Products</h5>
</div>
</div>
</div>
`,
methods: {
editPro(id) {
if (this.$route.path != "/app/admin/pro/edit/" + id) {
this.$router.push("/app/admin/pro/edit/" + id);
}
},
},
mounted() {
this.$store.dispatch("fetchProducts");
},
};
export default ProductCompo;
Note Base64 is a group of binary-to-text encoding schemes that represent binary data in an ASCII string format by translating it into a radix-64 representation. The main use of Base64 is to:
- Encode binary data for transmission: Base64 encoding allows you to transmit binary data, such as images, audio, and video, over text-based protocols like HTTP, email, and JSON.
- Browser decodes the Base64 data: The browser decodes the Base64-encoded data using a built-in decoder. This process involves converting the Base64 characters back into their original binary format.
Now we need to add this component in the router file. We will also learn how to create nested routes.
js filename=static/router/index.js
...
import AdminApp from "../views/AdminApp.js"; // [tl! add:start]
import ManagerApp from "../views/ManagerApp.js";
import UserApp from "../views/UserApp.js";
import CreateCatCompo from "../components/CreateCatCompo.js";
import EditCatCompo from "../components/EditCatCompo.js";
import CreateProCompo from "../components/CreateProCompo.js";
import ProductCompo from "../components/ProductCompo.js"; // [tl! add:end]
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes: [
...
{
path: "/app/admin",
component: AdminApp,
children: [
{ path: "/app/admin", component: ProductCompo },
{ path: "/app/admin/cat/create", component: CreateCatCompo },
{ path: "/app/admin/cat/edit/:id", component: EditCatCompo },
{ path: "/app/admin/pro/create", component: CreateProCompo }
],
},
{
path: "/app/manager",
component: ManagerApp,
children: [
{ path: "/app/manager", component: ProductCompo }
]
},
{
path: "/app/user",
component: UserApp,
children: [
{ path: "/app/user", component: ProductUserCompo }
]
}
]
});
export default router;
Above you can see I am using children to create nested routes.
Also to fetch the products and categories, we will use the fetchProducts
and fetchCategories
actions in the store file.
js filename=static/store/index.js
const store = new Vuex.Store({
...
actions: { // [tl! add:start]
// write your code here
async fetchCategories({ commit }) {
try {
const response = await fetch("http://127.0.0.1:5000/get/categories", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
if (response.status === 200) {
const data = await response.json();
console.log(data, "categories fetched");
commit("setCategories", data);
} else {
const data = await response.json();
alert(data.message);
}
} catch (error) {
console.error(error);
}
},
async fetchProducts({ commit }) {
try {
const response = await fetch("http://127.0.0.1:5000/get/products", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
if (response.status === 200) {
const data = await response.json();
console.log(data, "products fetched");
commit("setProducts", data);
} else {
const data = await response.json();
alert(data.message);
}
} catch (error) {
console.error(error);
}
},
}, // [tl! add:end]
});
export default store;
Backend Implementation
Now for creating product we will write the backend flask code.
python filename=app.py
...
@main.route('/add/product', methods=['POST'])
@jwt_required()
@role_required(['admin'])
def product():
if request.method == 'POST':
name_exist = Product.query.filter_by(name=request.form['name']).first()
if name_exist:
return jsonify({'message': "Resource already exists"}), 409
else:
name = request.form['name']
quantity = int(request.form['quantity'])
manufacture = datetime.strptime(
request.form['manufacture'], '%Y-%m-%d')
expiry = datetime.strptime(request.form['expiry'], '%Y-%m-%d')
rpu = float(request.form['rpu'])
category_id = float(request.form['category_id'])
unit = request.form['unit']
description = request.form['description']
# Handle file upload
image = request.files['image'].read()
new_product = Product(
name=name,
quantity=quantity,
manufacture=manufacture,
expiry=expiry,
description=description,
rpu=rpu,
unit=unit,
image=image,
category_id=category_id
)
prod_data = {
'id': new_product.id,
'quantity': new_product.quantity,
'name': new_product.name,
'manufacture': new_product.manufacture,
'expiry': new_product.expiry,
'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')
}
db.session.add(new_product)
db.session.commit()
return jsonify({
'message': f"Product {request.form['name']} add\
successfully in the database",
'resource': prod_data}), 201
...
Note You can see that we are using base64 for encoding the image data to store in the database.
Not only data we will also have to add add product
button on navigation bar. So let's update the static/views/AdminApp.js
file.
js filename=static/views/AdminApp.js
...
<nav class="navbar navbar-expand-lg navbar-dark bg-success">
...
<ul class="navbar-nav ms-auto">
...
<li class="nav-item"> <!-- [tl! add:start] -->
<a class="nav-link pointer-on-hover" @click="createPro">Add product</a>
</li> <!-- [tl! add:end] -->
</ul>
...
</nav>
...
Now, you can test out creating a product by running the app.
Edit Product Component
Now let's update the static/components/EditProCompo.js
file.
js filename=static/components/EditProCompo.js
const EditProCompo = {
name: "EditProCompo",
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">Update Product</h5>
<form @submit.prevent="updateproduct" enctype="multipart/form-data">
<label class="form-label" for="name">Product Name:</label>
<input class="form-control" v-model="product.name" type="text" id="name" name="name" required>
<br>
<label class="form-label" for="quantity">Quantity:</label>
<input class="form-control" v-model="product.quantity" type="number" id="quantity" name="quantity" required>
<br>
<label class="form-label" for="manufacture">Manufacture Date:</label>
<input class="form-control" v-model="product.manufacture" type="date" id="manufacture" name="manufacture" required>
<br>
<label class="form-label" for="expiry">Expiry Date:</label>
<input class="form-control" v-model="product.expiry" type="date" id="expiry" name="expiry" required>
<br>
<label class="form-label" for="description">description:</label>
<textarea class="form-control" v-model="product.description" type="text" id="description" name="description" required></textarea>
<br>
<label class="form-label" for="rpu">Rate Per Unit:</label>
<input class="form-control" v-model="product.rpu" type="number" id="rpu" name="rpu" step="0.01" required>
<br>
<label class="form-label" for="Select Category">Select Category:</label>
<select class="form-select" name="Select Category"
v-model="product.category_id" required>
<option v-for="category in this.$store.state.categories" :key="category.id" :value="category.id">{{category.name}}</option>
</select>
<label class="form-label" for="unit">Unit:</label>
<select class="form-select" name="Select Unit"
v-model="product.unit" required>
<option value="l">l</option>
<option value="ml">ml</option>
<option value="g">g</option>
<option value="kg">kg</option>
<option value="m">m</option>
<option value="cm">cm</option>
<option value="inch">inch</option>
<option value="piece">piece</option>
<option value="dozen">dozen</option>
</select>
<br>
<label class="form-label" for="image">Image:</label>
<input class="form-control" type="file" id="image" @change="handleFileUpload" accept="image/*" required>
<br>
<div class="d-flex">
<button type="submit" class="btn btn-outline-primary me-5">Update</button>
</div>
</form>
</div>
</div>
</div>
`,
data() {
return {
product: {
name: "",
quantity: 0,
manufacture: "",
expiry: "",
rpu: 0,
unit: "",
description: "",
image: null,
category_id: "",
},
};
},
methods: {
closeCard() {
if (this.$route.path != "/app/admin") {
this.$router.push("/app/admin");
}
},
handleFileUpload(event) {
this.product.image = event.target.files[0];
},
async fetchproduct() {
try {
const response = await fetch(
"http://127.0.0.1:5000/update/product/" + this.$route.params.id,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);
if (response.status === 200) {
const data = await response.json();
this.product = data;
} else if (response.status === 404) {
alert(data.message);
}
} catch (error) {
console.error(error);
}
},
async updateproduct() {
if (confirm("Are you sure?")) {
try {
const formData = new FormData();
formData.append("name", this.product.name);
formData.append("quantity", this.product.quantity);
formData.append("manufacture", this.product.manufacture);
formData.append("expiry", this.product.expiry);
formData.append("rpu", this.product.rpu);
formData.append("unit", this.product.unit);
formData.append("description", this.product.description);
formData.append("image", this.product.image);
formData.append("category_id", this.product.category_id);
const response = await fetch(
"http://127.0.0.1:5000/update/product/" + this.$route.params.id,
{
method: "PUT",
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: formData,
}
);
if (response.status === 201) {
const data = await response.json();
console.log(data, "printed data");
if (this.$store.state.authenticatedUser.role === "admin") {
this.$store.commit("updateProduct", data.resource);
} else {
this.$store.commit("addNoti", data.resource);
}
this.closeCard();
} else {
const data = await response.json();
alert(data.message);
}
} catch (error) {
console.error(error);
}
}
}
},
mounted() {
this.fetchproduct();
},
};
export default EditProCompo;
Note Notice that
FormData
is utilized to package and send data to the backend. This is essential when handling multiple data types in the request body. For more details aboutFormData
, refer to the MDN Web Docsdeveloper.mozilla.org/en-US/docs/Web/API/FormData
.
Also we will have to include this component in the vue router and vue store.
js filename=static/router/index.js
...
import EditProCompo from "../components/EditProCompo.js"; // [tl! add]
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes: [
...
{
path: "/app/admin",
component: AdminApp,
children: [
...
{ path: "/app/admin/pro/create", component: CreateProCompo },
{ path: "/app/admin/pro/edit/:id", component: EditProCompo } // [tl! add]
]
},
...
]
});
export default router;
Backend Implementation
python filename=app.py
@main.route('/update/product/<int:prod_id>', methods=['GET', 'PUT', 'DELETE'])
@jwt_required()
@role_required(['admin'])
def update_product(prod_id):
if isinstance(prod_id, int):
if request.method == 'GET':
product = Product.query.filter_by(id=prod_id).first()
if product:
prod_data = {
'id': product.id,
'quantity': product.quantity,
'name': product.name,
'manufacture': product.manufacture,
'description': product.description,
'expiry': product.expiry,
'rpu': product.rpu,
'unit': product.unit,
# Assuming image is stored as a base64-encoded string
'image': base64.b64encode(product.image).decode('utf-8')
}
return jsonify(prod_data), 200
else:
abort(404, message="Not found")
elif request.method == 'PUT':
product = Product.query.filter_by(id=prod_id).first()
if product:
product.name = request.form['name']
product.quantity = int(request.form['quantity'])
product.manufacture = datetime.strptime(
request.form['manufacture'], '%Y-%m-%d')
product.expiry = datetime.strptime(
request.form['expiry'], '%Y-%m-%d')
product.rpu = float(request.form['rpu'])
product.category_id = float(request.form['category_id'])
product.unit = request.form['unit']
product.description = request.form['description']
# Handle file upload
product.image = request.files['image'].read()
db.session.commit()
prod_data = {
'id': product.id,
'quantity': product.quantity,
'name': product.name,
'manufacture': product.manufacture,
'expiry': product.expiry,
'description': product.description,
'rpu': product.rpu,
'unit': product.unit,
# Assuming image is stored as a base64-encoded string
'image': base64.b64encode(
product.image).decode('utf-8')
}
return jsonify({
'message': f"Product {request.form['name']} updated\
successfully in the database",
'resource': prod_data}), 201
else:
return jsonify({'message': "Not found"}), 404
else:
return '', 400
Note When working with dates and times, you have two options: you can either store them as
datetime
objects in the database, or you can store them as strings. Storing them asdatetime
objects allows you to perform date and time arithmetic directly on the database values, while storing them as strings requires converting them todatetime
objects first.
Next, we will add the feature to manage managers by the admin. The admin can approve/hire a manager or dismiss/remove a manager.
Delete Product Component
We need to add the delete button on the EditProCompo.
js filename=static/components/EditProCompo.js
const EditProCompo = {
name: "EditProCompo",
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">Update Product</h5>
<form @submit.prevent="updateproduct" enctype="multipart/form-data">
...
<div class="d-flex">
...
<a class="btn btn-outline-danger" @click="deleteproduct">Delete</a> <!-- [tl! add] -->
</div>
</form>
</div>
</div>
</div>
`,
...
methods: {
...
, // [tl! add:start]
async deleteproduct() {
if (confirm("Are you sure?")) {
try {
const response = await fetch(
"http://127.0.0.1:5000/update/product/" + this.$route.params.id,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);
if (response.status === 201) {
const data = await response.json();
console.log(data, "printed data");
if (this.$store.state.authenticatedUser.role === "admin") {
this.$store.commit("deleteProduct", data.resource.id);
alert(data.message);
} else {
this.$store.commit("addNoti", data.resource);
alert(data.message);
}
this.closeCard();
} else {
alert(data.message);
}
} catch (error) {
console.error(error);
}
}
} // [tl! add:end]
},
mounted() {
this.fetchproduct();
},
};
export default EditProCompo;
Now we can Delete the product.
Backend Implementation
python filename=app.py
@main.route('/update/product/<int:prod_id>', methods=['GET', 'PUT', 'DELETE'])
@jwt_required()
@role_required(['admin'])
def update_product(prod_id):
if isinstance(prod_id, int):
...
elif request.method == 'DELETE':
product = Product.query.filter_by(id=prod_id).first()
carts = Cart.query.filter_by(product_id=product.id).all()
for cart in carts:
db.session.delete(cart)
db.session.commit()
db.session.delete(product)
db.session.commit()
return jsonify(
{'message': f"Product {product.name} deleted\
successfully from the database",
'resource': prod_id}), 201
return jsonify({'message': "Not found"}), 404
else:
return '', 400
Manage Managers
Managers will have access of CRUD operations to manage category/products.
We don't need to create tables or anything related to manager users because the user table already has a role attribute that defines the role of a user. Therefore, we can directly jump to creating the Managers component.
Managers Component
js filename=static/components/ManagersCompo.js
const ManagersCompo = {
name: "ManagersCompo",
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>
`,
methods: {
warn() {
if (this.$route.path != "/app/admin/warning") {
this.$router.push("/app/admin/warning");
}
},
async deletemanager(id) {
if (confirm("Are you sure?")) {
try {
const response = await fetch(
"http://127.0.0.1:5000/delete/man/" + id,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);
if (response.status === 200) {
const data = await response.json();
console.log(data, "printed data");
this.$store.commit("deleteManager", data.resource);
alert(data.message);
} else {
alert(data.message);
}
} catch (error) {
console.error(error);
}
}
},
},
mounted() {
this.$store.dispatch("fetchManagers");
},
};
export default ManagersCompo;
Admin have these features:
-
- Approve the requested of new manager.
-
- See all the managers at one page.
-
- Send Warning to the Managers.
-
- Fire/Delete a manager.
Above we have added a route to redirect to wanring page. We will add a view for warning page.
Let's also add fetchManagers
action to fetch all the managers. Let's write code in the static/store/index.js
file.
js filename=static/store/index.js
const store = new Vuex.Store({
state: {
products: [],
managers: [] // [tl! add]
},
mutations: {
...
setManagers: (state, managers) => {
state.managers = managers;
}
}
actions: {
// write your code here
...
async fetchManagers({ commit }) {
try {
const response = await fetch("http://127.0.0.1:5000/get/all/managers", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
if (response.status === 200) {
const data = await response.json();
console.log(data, "categories fetched");
commit("setManagers", data.resource);
} else {
const data = await response.json();
alert(data.message);
}
} catch (error) {
console.error(error);
}
}
}
});
export default store;
Warning Page
Let's create the send warning feature for admin dashboard.
js filename=static/components/WarningCompo.js
const sendWarning = {
name: "sendWarning",
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>
`,
data() {
return {
managers: {
id: "",
name: "",
email: "",
},
message: "",
};
},
methods: {
closeCard() {
if (this.$route.path != "/app/admin") {
this.$router.push("/app/admin");
}
},
async fetchManagers() {
try {
const response = await fetch("http://127.0.0.1:5000/send/alert", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
if (response.status === 200) {
const data = await response.json();
console.log(data, "managers alerts");
this.managers = data;
console.log(this.managers, "printed managers");
} else if (response.status === 404) {
alert(data.message);
}
} catch (error) {
console.error(error);
}
},
async sendWarning() {
if (confirm("Are you sure?")) {
try {
const response = await fetch("http://127.0.0.1:5000/send/alert", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: JSON.stringify({
id: this.managers.id,
message: this.message,
email: this.managers.email,
}),
});
if (response.status === 200) {
const data = await response.json();
console.log(data, "printed data");
alert(data.message);
this.closeCard();
} else {
const data = await response.json();
alert(data.message);
}
} catch (error) {
console.error(error);
}
}
},
},
mounted() {
this.fetchManagers();
},
};
export default sendWarning;
The fetchManagers()
will have us to fetch the managers data from the backend. The sendWarning()
will have us to send the warning to the manager.
Backend Implementation
I will keep admin major operations in a separate file withing a file backendjobs/admin.py
.
python filename=static/backendjobs/admin.py
...
from flask import current_app as app
admin_bp = Blueprint('admin_bp', __name__)
@admin_bp.route('/send/alert', methods=['GET', 'POST'])
@jwt_required()
@role_required(['admin'])
def send_alert():
if request.method == 'GET':
managers = User.query.filter_by(role='manager').all()
man_list = []
for man in managers:
man_data = {
'id': man.id,
'name': man.name,
'email': man.email,
}
man_list.append(man_data)
return jsonify(man_list), 200
if request.method == 'POST':
# will implement the medium to send the alert
# later.
return jsonify({'message': "sent"}), 200
I have implemented the logic to send alert, but I want to send the alert via mail which I will implement later.
Backend Implementation
python filename=static/backendjobs/admin.py
...
from flask import current_app as app
admin_bp = Blueprint('admin_bp', __name__)
@admin_bp.route('/delete/man/<int:id>', methods=['DELETE'])
@jwt_required()
@role_required(['admin'])
def delete_man(id):
man = User.query.filter_by(id=id).first()
if man:
db.session.delete(man)
db.session.commit()
return jsonify({'message': 'Deleted manager', 'resource': id}), 200
else:
return jsonify({'message': 'Not found'}), 404
Logout
We have already implemented the backend code just need to add the logout method in the frontend.
js filename=static/views/AdminApp.js
...
<nav class="navbar navbar-expand-lg navbar-dark bg-success">
...
<ul class="navbar-nav ms-auto">
...
<li class="nav-item"> <!-- [tl! add:start] -->
<a class="nav-link pointer-on-hover" @click="logout">logout</a>
</li> <!-- [tl! add:end] -->
</ul>
...
</nav>
...
methods: {
...
async logout() {
try {
const response = await fetch("http://127.0.0.1:5000/logout", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
if (response.status === 200) {
const data = await response.json();
this.$store.commit("setAuthenticatedUser", "");
localStorage.removeItem("token");
if (this.$route.path != "/app") {
this.$router.push("/app");
}
} else {
const data = await response.json();
alert(data.message);
}
} catch (error) {
console.error(error);
}
}
}
Testing it out
Now we can start the app and test all the feature implemented on this page.
Run the app.
python3 app.py
Commit the changes to the local git.
git add .
git commit -m "added admin features"