How to Create a Role-Based Custom Dashboard in Frappe v16 | Step-by-Step Guide

Frappe Role Based Custom Dashboard

Frappe Custom Dashboard Tutorial: Restricting Workspaces by User Roles (v16)

Frappe gives you an incredibly powerful Desk interface right out of the box. But let’s be real—most production business systems eventually need tailored experiences.

An HR manager shouldn't land on the same screen as a warehouse worker. A sales rep needs instant access to leads and targets, while a finance user needs aging reports and payment entries. A system admin? They just want the global operational metrics.

In Frappe v16 (and ERPNext), the cleanest, most efficient way to build this is by using Workspaces.

Workspaces act as the customized pages users see inside the Desk sidebar. They can house cards, shortcuts, charts, links, reports, and even custom HTML blocks. More importantly, they can be restricted by Role, making them the perfect foundation for role-specific dashboards.

Let's dive into how you can build a production-ready, role-based dashboard in Frappe v16—from the no-code Workspace builder to a developer-friendly deployment setup! 🛠️


⏱️ Time to Complete

~30 Minutes

🎯 What You’ll Achieve / Learn

  • Role Management: Create and assign custom roles (e.g., Sales Manager).
  • Workspace Magic: Build tailored, role-restricted Workspaces.
  • Dynamic Content: Implement Custom HTML Blocks powered by Python backend APIs.
  • Seamless UX: Write a custom JavaScript redirect for a flawless post-login experience.
  • Deployment Ready: Export your setup as fixtures for version-controlled deployment.

The Core Concept 🧠

In the Frappe ecosystem, a role-based dashboard usually breaks down into four essential parts:

  1. A Role (e.g., Sales Manager, HR User, Branch Officer).
  2. A Workspace (e.g., Sales Dashboard).
  3. Role Restrictions applied directly to that Workspace.
  4. (Optional but awesome) Custom blocks, charts, scripts, or backend APIs for dynamic data.

For 95% of use cases, you do not need a custom Desk page. A Workspace is completely sufficient! Rely on custom Desk pages (using frameworks like Vue.js or React) only when the dashboard requires complex, state-heavy interactions that are hard to maintain inside standard Workspace blocks.


Step 1: Define Your Role 🎭

First things first, we need to create the role that will access this shiny new dashboard.

Go to: Role > New

Example Setup:

  • Role Name: Sales Manager
  • Desk Access: Enabled ✅

Next, assign this role to the relevant users: Go to: User > Select User > Roles and check Sales Manager. Save the user.

⚠️ Pro Tip: A role alone doesn't magically grant data access. You still need to configure DocType permissions, Report permissions, or User Permissions depending on what data your dashboard will display!

Step 2: Create the Workspace 🏗️

Now, let's create the actual dashboard Workspace. You can access Workspaces from the Desk global search or the Workspace list.

Create a new Workspace with configurations like this:

  • Title: Sales Dashboard
  • Module: Selling
  • Public: Enabled ✅
  • Icon: bar-chart 📊

If a Workspace is private, only the owner can see it. Since we want our Sales Manager group to see it, it needs to be Public but restricted by role.

Open the Workspace builder and start adding blocks: Shortcuts, Number Cards, Charts, Reports, Link Cards, and Custom HTML Blocks.

Keep the first screen focused. A great dashboard answers one simple question: "What does this user need to do next?"

Step 3: Restrict by Role 🔐

Open your Workspace document and scroll down to the Roles table. Add Sales Manager.

Now, only users with this specific role will see the Workspace in their sidebar (assuming they also have access to the related module).

Remember, Frappe Workspace visibility depends on two things:

  1. Module Access (e.g., Does the user have access to the Selling module?)
  2. Role Access

If your user still can't see the Workspace, double-check their allowed modules!

Step 4: Curate Useful Content 📦

A dashboard shouldn't just be a chaotic list of every DocType the user can access. Design it around their daily workflow.

📈 For a Sales Manager:

  • Open Leads, Pending Quotations, Overdue Follow-ups, Sales Analytics Chart.

👥 For an HR Manager:

  • Pending Leave Applications, New Onboarding Tasks, Employee Directory Shortcut.

💰 For an Accounts User:

  • Unpaid Invoices, Payment Entries, Bank Reconciliation Report.

Use Number Cards for quick KPIs, Charts for trends, and Shortcuts for immediate actions.

Step 5: Level Up with Custom HTML Blocks 🎨

Sometimes default blocks aren't enough. Enter the Custom HTML Block.

This feature lets you write raw HTML, CSS, and JavaScript directly inside a Workspace block. It’s perfect for custom banners, progress trackers, or dynamic summaries.

Example HTML:

<div class="sales-dashboard-card p-4 bg-blue-50 rounded-md shadow-sm">
  <h3 class="text-lg font-bold text-blue-800">Welcome Back! 👋</h3>
  <p id="sales-user-summary" class="text-gray-600 mt-2">Fetching your stats...</p>
</div>

Example Script (Client-Side):

frappe.call({
    method: "my_app.api.dashboard.get_sales_summary",
}).then((r) => {
    const data = r.message || {};
    document.querySelector("#sales-user-summary").innerHTML = 
        `🔥 You have <b>${data.open_leads || 0}</b> open leads waiting for you.`;
});

Golden Rule: Keep your business logic on the server! The frontend should only display data, not enforce security.

Step 6: Power it up with a Backend API 🐍

For dynamic data, create a whitelisted Python method in your custom app.

# my_app/api/dashboard.py
import frappe

@frappe.whitelist()
def get_sales_summary():
    user = frappe.session.user

    # Server-side security check!
    if "Sales Manager" not in frappe.get_roles(user):
        frappe.throw("Not permitted", frappe.PermissionError)

    open_leads = frappe.db.count("Lead", {"status": "Open"})
    pending_quotes = frappe.db.count("Quotation", {"docstatus": 0})

    return {
        "open_leads": open_leads,
        "pending_quotations": pending_quotes,
    }

Always validate roles and permissions on the backend. Hiding a card via JavaScript is not security! 🛡️

Step 7: The "Magic" Redirect After Login ✨

Workspace restrictions control visibility, but they don't guarantee a user lands on their specific dashboard right after login. Let's fix that with a quick JS redirect.

In your custom app's hooks.py:

app_include_js = [
    "/assets/my_app/js/role_dashboard_redirect.js"
]

Create the script:

// public/js/role_dashboard_redirect.js

frappe.after_ajax(() => {
  if (!frappe.user_roles) return;

  const route = frappe.get_route();
  const current = route ? route.join("/") : "";

  // Only redirect if they are landing on the default Workspaces page
  if (current && current !== "Workspaces") return;

  if (frappe.user_roles.includes("Sales Manager")) {
    frappe.set_route("Workspaces", "Sales Dashboard");
  } else if (frappe.user_roles.includes("HR Manager")) {
    frappe.set_route("Workspaces", "HR Dashboard");
  }
});

Note: Test this carefully so you don't accidentally create an infinite redirect loop! 🔄

Step 8: Export the Workspace as a Fixture 📦

If you are building this inside a custom app, do not leave your Workspace stuck in the database! Export it as a fixture so you can easily deploy it to staging and production environments via Git.

In hooks.py:

fixtures = [
    {
        "dt": "Workspace",
        "filters": [["name", "in", ["Sales Dashboard", "HR Dashboard"]]],
    },
    {
        "dt": "Custom HTML Block",
        "filters": [["name", "in", ["Sales Summary Block"]]],
    },
]

Then run in your bench terminal:

bench --site your-site export-fixtures

This saves everything into your app’s fixtures directory. Commit them, push them, and you're safe! 🐙

Step 9: Deploy and Rebuild 🚀

When moving your code to a new server, run:

bench build --app my_app
bench --site your-site migrate
bench --site your-site clear-cache

Step 10: Test With Real Users 🕵️‍♂️

Never test only as an Administrator. You have superuser powers, so everything will look like it works.

Create test users ([email protected], [email protected]), assign them their specific roles, and verify:

  • Can they see the correct Workspace?
  • Are restricted Workspaces hidden?
  • Do the custom APIs block unauthorized requests?
  • Does the login redirect work smoothly?

Common Gotchas & Troubleshooting 🛠️

  • Workspace Isn't Visible? Check if the Workspace is Public, if the Role is in the table, if the user has Module access, and don't forget to bench clear-cache!
  • Data is Missing? The user might see the Workspace, but lack Role Permissions for the actual DocTypes (like Lead or Quotation). Update the Role Permission Manager.
  • Redirect Loops? Ensure your JS script checks if (current !== "Workspaces") return; so it doesn't run on every single page load.

Recommended App Structure 📁

For a clean, maintainable setup, organize your custom app like this:

my_app/
  hooks.py
  api/
    dashboard.py
  public/
    js/
      role_dashboard_redirect.js
  fixtures/
    workspace.json
    custom_html_block.json

Final Thoughts 💡

In Frappe v16, a well-configured Workspace is the best starting point for a role-specific dashboard. Keep it simple: use Workspace roles for visibility, DocType permissions for data security, and Custom HTML blocks with Python backend APIs for that extra dynamic flair.

Export everything as fixtures, and you'll have a maintainable, professional dashboard system that your users will love! 🎉

References 🔗

Related posts