Make URLs dynamic, add after_install hook, add README

This commit is contained in:
Adam Moussa 2025-05-02 23:05:24 +00:00
commit bc0f4f83bc
18 changed files with 414 additions and 0 deletions

22
README.md Normal file
View 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

View file

@ -0,0 +1 @@
__version__ = "0.0.1"

Binary file not shown.

Binary file not shown.

View file

View file

@ -0,0 +1,10 @@
from frappe import _
def get_data():
return [
{
"module_name": "Portal Dashboard",
"type": "module",
"label": _("Portal Dashboard")
}
]

View 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
View 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"
# ]

View 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.")

View file

@ -0,0 +1 @@
Portal Dashboard

View file

View file

View file

Binary file not shown.

View 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>

View 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