Skip to content

07. Manager Access

The user with manager role will have the same set of features as the admin, but every action of manager will ask for admin approval. Whenever the manager will do some action it will be stored as a request in the database and send to the admin, now admin can approve or reject the request. It's up to him/her.

So, let's start with request for CRUD operations on category.

Request to Add Category

Since every action of a manager will be taken as a request, so we will create a new table to store those requests. This table will contain the details of the request like the type of the request, message which will contain the actual data of the request, sender and reciever of the request, status of the request and the timestamp when the request was created.

Database Update

The table will have the following structure:

python filename=database.py

...
class RequestResponse(db.Model): # [tl! add:start]
    __tablename__ = 'request_response'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    status = db.Column(db.String, nullable=False) # approved or rejected or pending
    type = db.Column(db.String, nullable=False)
    message = db.Column(db.String, nullable=False)
    image = db.Column(db.BLOB)
    sender = db.Column(db.Integer, db.ForeignKey('user.id'))
    receiver = db.Column(db.Integer, db.ForeignKey('user.id'))
    timestamp = db.Column(db.DateTime, nullable=False) # [tl! add:end]

Note Even a new manager signup on the app will ask for the approval from the admin.

All the columns in the table can be easily interpreted by their names, but the two columns which I need to explain are type and message:

    1. type is the type of the request. It can be manager, update, delete or view. type | Description manager | New manager signup request category | Adding New category request category update | Updating an existing category request category delete | Deleting an existing category request product | Adding New product request product update | Updating an existing product request product delete | Deleting an existing product request
    1. message will treat specially. This message will contain the data of the request. For example, if a new user wants to signup as a manager the message will store the comma separated stirng with all the details message = f"{data['email']},{data['name']},{data['role']},{data['password']}".

Updating Dashboard

Since we have separate Dashboard js file for manager so we will have to implement independent componets there.

Let's update the manager dashboard.

js filename=static/views/ManagerApp.js

const ManagerApp = {
  name: "ManagerApp",
  template: `
    ...
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav ms-auto">
                    ...
                    <li class="nav-item"> <!-- [tl! add:start] -->
                        <a class="nav-link pointer-on-hover" @click="createCat">Add category</a>
                    </li> <!-- [tl! add:end] -->
                </ul>
            </div>
    ...
    `,
  ...
  methods: {
    createCat() { // [tl! add:start]
      if (this.$route.path != "/app/manager/cat/create") {
        this.$router.push("/app/manager/cat/create");
      }
    } // [tl! add:end]
  },
  mounted() {
    this.$store.dispatch("fetchCategories");// [tl! add]
  },
};
export default ManagerApp;

Our manager dashboard will look same as of the admin dashboard.

Backend Implementation

python filename=app.py

...
from role_auth import role_required
...
@main.route('/add/cat', methods=['POST'])
@jwt_required()
@role_required(['admin', 'manager'])
def create():
    data = request.get_json()
    if data:
        if not Category.query.filter_by(name=data['name']).first():
            if current_user.role == 'manager': # [tl! add:start]
                admin = User.query.filter_by(role='admin').first()
                message = data['name']
                requested = RequestResponse(status='pending',
                                            type='category',
                                            message=message,
                                            sender=current_user.email,
                                            receiver=admin.email,
                                            timestamp=datetime.now())
                db.session.add(requested)
                db.session.commit()
                noti_data = {
                    'id': requested.id,
                    'state': requested.status,
                    'message': requested.message,
                    'sender': requested.sender,
                    'timestamp': requested.timestamp.strftime('%Y-%m-%d'),
                }
                return jsonify({'message': 'Created request',
                                'resource': noti_data}), 201
            else: # [tl! add:end]
                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")
...

Request to Update Category

The way we have created add request similarly we will create update request.

Edit Category Component

What we want to do is that whenever we click on edit it should show a form to edit and all the fields should show the current data.

js filename=static/components/EditCatCompo.js

const EditCatCompo = {
    name: "EditCatCompo",
    ...
    methods: {
        closeCard() { // [tl! remove:start]
            if (this.$route.path != "/app/admin") {
                this.$router.push("/app/admin");
            }
        } // [tl! remove:end]
      closeCard() {// [tl! add:start]
        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");
          }
        }
      } // [tl! add:end]
    },
    mounted() {
        this.fetchcategory();
    },
};
export default EditCatCompo;

Request to Delete Category

Already we have implemented the button for deleting the category, we just need the backend implementation for manager.

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( # [tl! remove:start]
                    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 # [tl! remove:end]
                if current_user.role == 'manager': # [tl! add:start]
                    admin = User.query.filter_by(role='admin').first()
                    message = f"{cat_id},{category.name}"
                    requested = RequestResponse(status='pending',
                                                type='category delete',
                                                message=message,
                                                sender=current_user.email,
                                                receiver=admin.email,
                                                timestamp=datetime.now())
                    db.session.add(requested)
                    db.session.commit()
                    noti_data = {
                        'id': requested.id,
                        'state': requested.status,
                        'message': requested.message,
                        'sender': requested.sender,
                        'timestamp': requested.timestamp.strftime('%Y-%m-%d'),
                    }
                    return jsonify(
                        {
                            'message': 'Created request',
                            'resource': noti_data
                        }
                    ), 201
                else:
                    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 # [tl! add:end]
            return jsonify({'message': "Not found"}), 404
    else:
        return '', 400
...

Now we will move to the product.

Request to Create Product

Updating the UI

js filename=static/components/EditProCompo.js

const EditProCompo = {
  ...
  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");
        }
      }
    }
  }
  ...
};
export default EditProCompo;

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':
            ...
        elif request.method == 'PUT':
            product = Product.query.filter_by(id=prod_id).first()
            if product: # [tl! remove:start]
                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 # [tl! remove:end]
            if product: # [tl! add:start]
                if current_user.role == 'manager':
                    admin = User.query.filter_by(role='admin').first()
                    message = f"{prod_id},{request.form['name']},\
                        {request.form['quantity']},\
                        {request.form['manufacture']},\
                        {request.form['expiry']},{request.form['rpu']},\
                        {request.form['category_id']},{request.form['unit']},\
                        {request.form['description']}"

                    requested = RequestResponse(
                        status='pending',
                        type='product update',
                        message=message,
                        sender=current_user.email,
                        image=request.files['image'].read(),
                        receiver=admin.email,
                        timestamp=datetime.now())
                    db.session.add(requested)
                    db.session.commit()
                    noti_data = {
                        'id': requested.id,
                        'state': requested.status,
                        'message': requested.message,
                        'sender': requested.sender,
                        'timestamp': requested.timestamp.strftime('%Y-%m-%d'),
                    }
                    return jsonify(
                        {'message': 'Created request',
                         'resource': noti_data}), 201
                else:
                    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 # [tl! add:end]
    else:
        return '', 400

Request to Delete Product

Update the Component

js filename=static/components/EditProCompo.js

const EditProCompo = {
  ...
  methods: {
    closeCard() {
      if (this.$route.path != "/app/admin") { // [tl! remove: start]
        this.$router.push("/app/admin");
      } // [tl! remove:end]
      if (this.$store.state.authenticatedUser.role === "admin") { // [tl! add:start]
        if (this.$route.path != "/app/admin") {
          this.$router.push("/app/admin");
        }
      } else {
        if (this.$route.path != "/app/manager") {
          this.$router.push("/app/manager");
        }
      }// [tl! add:end]
    },
    ...
  },
  mounted() {
    this.fetchproduct();
  },
};
export default EditProCompo;

Here we just need to implement the backend.

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()
            if product:
                if current_user.role == 'manager':
                    admin = User.query.filter_by(role='admin').first()
                    message = f"{prod_id},{product.name}"
                    requested = RequestResponse(status='pending',
                                                type='product delete',
                                                message=message,
                                                sender=current_user.email,
                                                receiver=admin.email,
                                                timestamp=datetime.now())
                    db.session.add(requested)
                    db.session.commit()
                    noti_data = {
                        'id': requested.id,
                        'state': requested.status,
                        'message': requested.message,
                        'sender': requested.sender,
                        'timestamp': requested.timestamp.strftime('%Y-%m-%d'),
                    }
                    return jsonify(
                        {'message': 'Created request',
                         'resource': noti_data}), 201
                else:
                    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

Now manager is able to create delete request for products.

Logout

We have already implemented the backend code just need to add the logout method in the frontend.

js filename=static/views/ManagerApp.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"

Continue to add User features...