Make URLs dynamic, add after_install hook, add README
This commit is contained in:
commit
bc0f4f83bc
18 changed files with 414 additions and 0 deletions
22
README.md
Normal file
22
README.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Sea Haven Portal Dashboard (`portal_dashboard`)
|
||||
|
||||
A lightweight, role-based landing page app for Frappe/ERPNext. Shows a clean tile-style interface for navigating to apps like ERP, Wiki, CRM — all based on user roles.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- Clean, responsive tile dashboard
|
||||
- Icons + labels based on Frappe user roles
|
||||
- Dynamic subdomain routing (e.g., `accounting.yourdomain.com`)
|
||||
- Auto-redirect for non-portal subdomains
|
||||
- Auto-sets homepage on install
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Installation
|
||||
|
||||
### 1. Get the app
|
||||
|
||||
```bash
|
||||
bench get-app portal_dashboard https://github.com/yourusername/portal_dashboard.git
|
||||
1
portal_dashboard/__init__.py
Normal file
1
portal_dashboard/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
__version__ = "0.0.1"
|
||||
BIN
portal_dashboard/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
portal_dashboard/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
portal_dashboard/__pycache__/hooks.cpython-310.pyc
Normal file
BIN
portal_dashboard/__pycache__/hooks.cpython-310.pyc
Normal file
Binary file not shown.
0
portal_dashboard/config/__init__.py
Normal file
0
portal_dashboard/config/__init__.py
Normal file
10
portal_dashboard/config/desktop.py
Normal file
10
portal_dashboard/config/desktop.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
{
|
||||
"module_name": "Portal Dashboard",
|
||||
"type": "module",
|
||||
"label": _("Portal Dashboard")
|
||||
}
|
||||
]
|
||||
10
portal_dashboard/config/docs.py
Normal file
10
portal_dashboard/config/docs.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
"""
|
||||
Configuration for docs
|
||||
"""
|
||||
|
||||
# source_link = "https://github.com/[org_name]/portal_dashboard"
|
||||
# headline = "App that does everything"
|
||||
# sub_heading = "Yes, you got that right the first time, everything"
|
||||
|
||||
def get_context(context):
|
||||
context.brand_html = "Portal Dashboard"
|
||||
219
portal_dashboard/hooks.py
Normal file
219
portal_dashboard/hooks.py
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
app_name = "portal_dashboard"
|
||||
app_title = "Portal Dashboard"
|
||||
app_publisher = "Adam Moussa"
|
||||
app_description = "A role based tile dashboard for navigation to internal resources"
|
||||
app_email = "adam@seahaven.com"
|
||||
app_license = "MIT"
|
||||
after_install = "portal_dashboard.setup.install.run"
|
||||
web_routes = [
|
||||
{"path": "/", "controller": "portal_dashboard.www.index", "login_required": 1}
|
||||
]
|
||||
|
||||
# Includes in <head>
|
||||
# ------------------
|
||||
|
||||
# include js, css files in header of desk.html
|
||||
# app_include_css = "/assets/portal_dashboard/css/portal_dashboard.css"
|
||||
# app_include_js = "/assets/portal_dashboard/js/portal_dashboard.js"
|
||||
|
||||
# include js, css files in header of web template
|
||||
# web_include_css = "/assets/portal_dashboard/css/portal_dashboard.css"
|
||||
# web_include_js = "/assets/portal_dashboard/js/portal_dashboard.js"
|
||||
|
||||
# include custom scss in every website theme (without file extension ".scss")
|
||||
# website_theme_scss = "portal_dashboard/public/scss/website"
|
||||
|
||||
# include js, css files in header of web form
|
||||
# webform_include_js = {"doctype": "public/js/doctype.js"}
|
||||
# webform_include_css = {"doctype": "public/css/doctype.css"}
|
||||
|
||||
# include js in page
|
||||
# page_js = {"page" : "public/js/file.js"}
|
||||
|
||||
# include js in doctype views
|
||||
# doctype_js = {"doctype" : "public/js/doctype.js"}
|
||||
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
|
||||
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
|
||||
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
|
||||
|
||||
# Home Pages
|
||||
# ----------
|
||||
|
||||
# application home page (will override Website Settings)
|
||||
# home_page = "login"
|
||||
|
||||
# website user home page (by Role)
|
||||
# role_home_page = {
|
||||
# "Role": "home_page"
|
||||
# }
|
||||
|
||||
# Generators
|
||||
# ----------
|
||||
|
||||
# automatically create page for each record of this doctype
|
||||
# website_generators = ["Web Page"]
|
||||
|
||||
# Jinja
|
||||
# ----------
|
||||
|
||||
# add methods and filters to jinja environment
|
||||
# jinja = {
|
||||
# "methods": "portal_dashboard.utils.jinja_methods",
|
||||
# "filters": "portal_dashboard.utils.jinja_filters"
|
||||
# }
|
||||
|
||||
# Installation
|
||||
# ------------
|
||||
|
||||
# before_install = "portal_dashboard.install.before_install"
|
||||
# after_install = "portal_dashboard.install.after_install"
|
||||
|
||||
# Uninstallation
|
||||
# ------------
|
||||
|
||||
# before_uninstall = "portal_dashboard.uninstall.before_uninstall"
|
||||
# after_uninstall = "portal_dashboard.uninstall.after_uninstall"
|
||||
|
||||
# Integration Setup
|
||||
# ------------------
|
||||
# To set up dependencies/integrations with other apps
|
||||
# Name of the app being installed is passed as an argument
|
||||
|
||||
# before_app_install = "portal_dashboard.utils.before_app_install"
|
||||
# after_app_install = "portal_dashboard.utils.after_app_install"
|
||||
|
||||
# Integration Cleanup
|
||||
# -------------------
|
||||
# To clean up dependencies/integrations with other apps
|
||||
# Name of the app being uninstalled is passed as an argument
|
||||
|
||||
# before_app_uninstall = "portal_dashboard.utils.before_app_uninstall"
|
||||
# after_app_uninstall = "portal_dashboard.utils.after_app_uninstall"
|
||||
|
||||
# Desk Notifications
|
||||
# ------------------
|
||||
# See frappe.core.notifications.get_notification_config
|
||||
|
||||
# notification_config = "portal_dashboard.notifications.get_notification_config"
|
||||
|
||||
# Permissions
|
||||
# -----------
|
||||
# Permissions evaluated in scripted ways
|
||||
|
||||
# permission_query_conditions = {
|
||||
# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
|
||||
# }
|
||||
#
|
||||
# has_permission = {
|
||||
# "Event": "frappe.desk.doctype.event.event.has_permission",
|
||||
# }
|
||||
|
||||
# DocType Class
|
||||
# ---------------
|
||||
# Override standard doctype classes
|
||||
|
||||
# override_doctype_class = {
|
||||
# "ToDo": "custom_app.overrides.CustomToDo"
|
||||
# }
|
||||
|
||||
# Document Events
|
||||
# ---------------
|
||||
# Hook on document methods and events
|
||||
|
||||
# doc_events = {
|
||||
# "*": {
|
||||
# "on_update": "method",
|
||||
# "on_cancel": "method",
|
||||
# "on_trash": "method"
|
||||
# }
|
||||
# }
|
||||
|
||||
# Scheduled Tasks
|
||||
# ---------------
|
||||
|
||||
# scheduler_events = {
|
||||
# "all": [
|
||||
# "portal_dashboard.tasks.all"
|
||||
# ],
|
||||
# "daily": [
|
||||
# "portal_dashboard.tasks.daily"
|
||||
# ],
|
||||
# "hourly": [
|
||||
# "portal_dashboard.tasks.hourly"
|
||||
# ],
|
||||
# "weekly": [
|
||||
# "portal_dashboard.tasks.weekly"
|
||||
# ],
|
||||
# "monthly": [
|
||||
# "portal_dashboard.tasks.monthly"
|
||||
# ],
|
||||
# }
|
||||
|
||||
# Testing
|
||||
# -------
|
||||
|
||||
# before_tests = "portal_dashboard.install.before_tests"
|
||||
|
||||
# Overriding Methods
|
||||
# ------------------------------
|
||||
#
|
||||
# override_whitelisted_methods = {
|
||||
# "frappe.desk.doctype.event.event.get_events": "portal_dashboard.event.get_events"
|
||||
# }
|
||||
#
|
||||
# each overriding function accepts a `data` argument;
|
||||
# generated from the base implementation of the doctype dashboard,
|
||||
# along with any modifications made in other Frappe apps
|
||||
# override_doctype_dashboards = {
|
||||
# "Task": "portal_dashboard.task.get_dashboard_data"
|
||||
# }
|
||||
|
||||
# exempt linked doctypes from being automatically cancelled
|
||||
#
|
||||
# auto_cancel_exempted_doctypes = ["Auto Repeat"]
|
||||
|
||||
# Ignore links to specified DocTypes when deleting documents
|
||||
# -----------------------------------------------------------
|
||||
|
||||
# ignore_links_on_delete = ["Communication", "ToDo"]
|
||||
|
||||
# Request Events
|
||||
# ----------------
|
||||
# before_request = ["portal_dashboard.utils.before_request"]
|
||||
# after_request = ["portal_dashboard.utils.after_request"]
|
||||
|
||||
# Job Events
|
||||
# ----------
|
||||
# before_job = ["portal_dashboard.utils.before_job"]
|
||||
# after_job = ["portal_dashboard.utils.after_job"]
|
||||
|
||||
# User Data Protection
|
||||
# --------------------
|
||||
|
||||
# user_data_fields = [
|
||||
# {
|
||||
# "doctype": "{doctype_1}",
|
||||
# "filter_by": "{filter_by}",
|
||||
# "redact_fields": ["{field_1}", "{field_2}"],
|
||||
# "partial": 1,
|
||||
# },
|
||||
# {
|
||||
# "doctype": "{doctype_2}",
|
||||
# "filter_by": "{filter_by}",
|
||||
# "partial": 1,
|
||||
# },
|
||||
# {
|
||||
# "doctype": "{doctype_3}",
|
||||
# "strict": False,
|
||||
# },
|
||||
# {
|
||||
# "doctype": "{doctype_4}"
|
||||
# }
|
||||
# ]
|
||||
|
||||
# Authentication and authorization
|
||||
# --------------------------------
|
||||
|
||||
# auth_hooks = [
|
||||
# "portal_dashboard.auth.validate"
|
||||
# ]
|
||||
8
portal_dashboard/install.py
Normal file
8
portal_dashboard/install.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import frappe
|
||||
|
||||
def run():
|
||||
# Force home page to our portal dashboard
|
||||
frappe.db.set_single_value("Website Settings", "home_page", "index")
|
||||
|
||||
# Optional: log it
|
||||
frappe.logger().info("Set home_page to 'index' for portal_dashboard.")
|
||||
1
portal_dashboard/modules.txt
Normal file
1
portal_dashboard/modules.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Portal Dashboard
|
||||
0
portal_dashboard/patches.txt
Normal file
0
portal_dashboard/patches.txt
Normal file
0
portal_dashboard/portal_dashboard/__init__.py
Normal file
0
portal_dashboard/portal_dashboard/__init__.py
Normal file
0
portal_dashboard/public/.gitkeep
Normal file
0
portal_dashboard/public/.gitkeep
Normal file
0
portal_dashboard/templates/__init__.py
Normal file
0
portal_dashboard/templates/__init__.py
Normal file
0
portal_dashboard/templates/pages/__init__.py
Normal file
0
portal_dashboard/templates/pages/__init__.py
Normal file
BIN
portal_dashboard/www/__pycache__/index.cpython-310.pyc
Normal file
BIN
portal_dashboard/www/__pycache__/index.cpython-310.pyc
Normal file
Binary file not shown.
104
portal_dashboard/www/index.html
Normal file
104
portal_dashboard/www/index.html
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<!-- portal_dashboard/www/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<div class="header" style="display: flex; justify-content: flex-end; align-items: center; max-width: 800px; margin: 0 auto 30px auto;">
|
||||
<div class="user-menu" style="position: relative;">
|
||||
<button onclick="toggleMenu()" style="background: none; border: none; cursor: pointer;">
|
||||
<i class="fas fa-user-circle" style="font-size: 1.5rem; color: #007bff;"></i>
|
||||
</button>
|
||||
<div id="dropdown" style="display: none; position: absolute; right: 0; background: #fff; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 10px; min-width: 150px; z-index: 10;">
|
||||
<div style="padding: 5px 10px; font-size: 0.9rem;">{{ frappe.session.full_name }}</div>
|
||||
<hr style="margin: 5px 0;">
|
||||
<a href="/me" style="text-decoration: none; display: block; padding: 5px 10px; color: #333;">My Account</a>
|
||||
<a href="/app" style="text-decoration: none; display: block; padding: 5px 10px; color: #333;">ERP Desk</a>
|
||||
<a href="/?cmd=logout" style="text-decoration: none; display: block; padding: 5px 10px; color: #c00;">Log Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleMenu() {
|
||||
var dropdown = document.getElementById('dropdown');
|
||||
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// Close menu when clicking outside
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!e.target.closest('.user-menu')) {
|
||||
document.getElementById('dropdown').style.display = 'none';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<head>
|
||||
<title>Sea Haven Portal</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
background-color: #f9f9f9;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 40px;
|
||||
font-size: 2.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 30px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.app-link {
|
||||
background: #fff;
|
||||
padding: 30px 20px;
|
||||
border-radius: 10px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.app-link:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.app-link i {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.app-label {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% if frappe.session.user == "Guest" %}
|
||||
<script>
|
||||
window.location.href = "/login?redirect-to=/";
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<h1>Sea Haven Portal</h1>
|
||||
<div class="grid">
|
||||
{% for app in apps %}
|
||||
<a href="{{ app.url }}" class="app-link">
|
||||
<i class="fas {{ app.icon }}"></i>
|
||||
<div class="app-label">{{ app.label }}</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
39
portal_dashboard/www/index.py
Normal file
39
portal_dashboard/www/index.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import frappe
|
||||
|
||||
def get_context(context):
|
||||
host = frappe.local.request.host
|
||||
base_domain = ".".join(host.split(".")[1:])
|
||||
|
||||
if not host.startswith("portal."):
|
||||
frappe.local.flags.redirect_location = "/app"
|
||||
raise frappe.Redirect
|
||||
|
||||
user = frappe.session.user
|
||||
roles = frappe.get_roles(user)
|
||||
|
||||
user = frappe.session.user
|
||||
roles = frappe.get_roles(user)
|
||||
|
||||
apps = [
|
||||
{
|
||||
"label": "ERP",
|
||||
"icon": "fa-warehouse",
|
||||
"url": "https://accounting.{base_domain}",
|
||||
"roles": ["Accounts User", "Accounts Manager"]
|
||||
},
|
||||
{
|
||||
"label": "Wiki",
|
||||
"icon": "fa-book",
|
||||
"url": "https://wiki.{base_domain}",
|
||||
"roles": ["Employee", "System Manager"]
|
||||
},
|
||||
{
|
||||
"label": "CRM",
|
||||
"icon": "fa-user-friends",
|
||||
"url": "https://crm.{base_domain}",
|
||||
"roles": ["Sales User", "Sales Manager"]
|
||||
}
|
||||
]
|
||||
|
||||
context.apps = [app for app in apps if any(role in roles for role in app["roles"])]
|
||||
return context
|
||||
Loading…
Add table
Reference in a new issue