From 21958eca1591d58f8a24522cea7424a80d10e02d Mon Sep 17 00:00:00 2001
From: metatroncubeswdev
Date: Mon, 31 Aug 2026 15:21:49 -0400
Subject: [PATCH] Add SaaS workplace frontend
---
addons/mcs_corporate_website/__manifest__.py | 3 +
.../mcs_corporate_website/controllers/main.py | 200 ++++++++
.../data/workplace_data.xml | 29 ++
.../mcs_corporate_website/models/__init__.py | 2 +
.../models/announcement.py | 37 ++
.../mcs_corporate_website/models/knowledge.py | 30 ++
.../security/ir.model.access.csv | 4 +
.../static/src/scss/corporate_website.scss | 473 ++++++++++++++++++
.../views/workplace_backend_views.xml | 82 +++
.../views/workplace_templates.xml | 240 +++++++++
10 files changed, 1100 insertions(+)
create mode 100644 addons/mcs_corporate_website/data/workplace_data.xml
create mode 100644 addons/mcs_corporate_website/models/announcement.py
create mode 100644 addons/mcs_corporate_website/models/knowledge.py
create mode 100644 addons/mcs_corporate_website/views/workplace_backend_views.xml
create mode 100644 addons/mcs_corporate_website/views/workplace_templates.xml
diff --git a/addons/mcs_corporate_website/__manifest__.py b/addons/mcs_corporate_website/__manifest__.py
index 090f26a..fe29dcc 100644
--- a/addons/mcs_corporate_website/__manifest__.py
+++ b/addons/mcs_corporate_website/__manifest__.py
@@ -19,8 +19,11 @@
"security/security.xml",
"security/ir.model.access.csv",
"data/sequence_data.xml",
+ "data/workplace_data.xml",
"views/support_ticket_views.xml",
+ "views/workplace_backend_views.xml",
"views/website_templates.xml",
+ "views/workplace_templates.xml",
"views/website_menus.xml",
"data/website_data.xml",
],
diff --git a/addons/mcs_corporate_website/controllers/main.py b/addons/mcs_corporate_website/controllers/main.py
index 2bb0827..7867c53 100644
--- a/addons/mcs_corporate_website/controllers/main.py
+++ b/addons/mcs_corporate_website/controllers/main.py
@@ -13,6 +13,143 @@ class McsCorporateWebsite(http.Controller):
)
return request.render(template, values)
+ def _workplace_render(self, template, values=None):
+ values = values or {}
+ values.update(self._workplace_context())
+ return request.render(template, values)
+
+ def _employee(self):
+ return request.env["hr.employee"].sudo().search([("user_id", "=", request.env.user.id)], limit=1)
+
+ def _role(self):
+ user = request.env.user
+ if user.has_group("base.group_system"):
+ return "admin"
+ if user.has_group("mcs_corporate_website.group_mcs_website_support_user"):
+ return "support_agent"
+ if self._employee():
+ return "employee"
+ return "client"
+
+ def _workplace_context(self):
+ user = request.env.user
+ employee = self._employee()
+ role = self._role()
+ partner = user.partner_id.commercial_partner_id
+ return {
+ "wp_user": user,
+ "wp_role": role,
+ "wp_employee": employee,
+ "wp_first_name": (employee.name or user.name).split(" ")[0],
+ "wp_company_name": partner.name or user.company_id.name,
+ "wp_today": fields.Date.context_today(request.env.user),
+ "wp_nav": self._workplace_nav(role),
+ }
+
+ def _workplace_nav(self, role):
+ if role == "client":
+ return [
+ ("Dashboard", "/workplace", "dashboard"),
+ ("Support Tickets", "/workplace/tickets", "support"),
+ ("Create Ticket", "/workplace/tickets/new", "create"),
+ ("Knowledge Base", "/workplace/knowledge", "knowledge"),
+ ("Profile", "/workplace/profile", "profile"),
+ ]
+ items = [
+ ("Dashboard", "/workplace", "dashboard"),
+ ("Attendance", "/workplace/attendance", "attendance"),
+ ("Leave", "/workplace/leave", "leave"),
+ ("Support", "/workplace/tickets", "support"),
+ ("Knowledge", "/workplace/knowledge", "knowledge"),
+ ("Announcements", "/workplace/announcements", "announcements"),
+ ]
+ if role in ("support_agent", "admin"):
+ items.insert(4, ("Queue", "/workplace/support-queue", "queue"))
+ return items
+
+ def _ticket_domain(self, role):
+ if role in ("admin", "support_agent"):
+ return []
+ return [("partner_id", "child_of", request.env.user.partner_id.commercial_partner_id.id)]
+
+ def _ticket_values(self, tickets):
+ mapped = []
+ for ticket in tickets:
+ mapped.append(
+ {
+ "id": ticket.id,
+ "number": ticket.name,
+ "subject": ticket.subject,
+ "priority": dict(ticket._fields["priority"].selection).get(ticket.priority),
+ "priority_key": ticket.priority,
+ "status": dict(ticket._fields["state"].selection).get(ticket.state),
+ "status_key": ticket.state,
+ "customer": ticket.partner_id.name or ticket.customer_name,
+ "assigned_team": dict(ticket._fields["service_area"].selection).get(ticket.service_area),
+ "created": ticket.create_date,
+ "updated": ticket.write_date,
+ "updated_label": fields.Datetime.to_string(ticket.write_date) if ticket.write_date else "",
+ }
+ )
+ return mapped
+
+ def _dashboard_values(self, role):
+ employee = self._employee()
+ today = fields.Date.context_today(request.env.user)
+ Ticket = request.env["mcs.support.ticket"].sudo()
+ tickets = Ticket.search(self._ticket_domain(role), order="write_date desc", limit=8)
+ values = {
+ "tickets": self._ticket_values(tickets),
+ "announcements": self._announcements(role, 3),
+ "articles": self._knowledge(role, 3),
+ "ticket_counts": {
+ "open": Ticket.search_count(self._ticket_domain(role) + [("state", "=", "new")]),
+ "in_progress": Ticket.search_count(self._ticket_domain(role) + [("state", "=", "in_progress")]),
+ "waiting": Ticket.search_count(self._ticket_domain(role) + [("state", "=", "waiting")]),
+ "resolved": Ticket.search_count(self._ticket_domain(role) + [("state", "in", ["resolved", "closed"])]),
+ },
+ }
+ if employee and role != "client":
+ Attendance = request.env["hr.attendance"].sudo()
+ attendance = Attendance.search(
+ [("employee_id", "=", employee.id), ("check_in", ">=", fields.Datetime.to_string(today))],
+ order="check_in desc",
+ limit=1,
+ )
+ month_start = today.replace(day=1)
+ values.update(
+ {
+ "attendance": attendance,
+ "worked_today": round(attendance.worked_hours, 2) if attendance else 0,
+ "month_present": Attendance.search_count(
+ [("employee_id", "=", employee.id), ("check_in", ">=", fields.Date.to_string(month_start))]
+ ),
+ "leave_balance": self._leave_balance(employee),
+ "recent_activity": self._recent_activity(employee),
+ }
+ )
+ return values
+
+ def _leave_balance(self, employee):
+ LeaveType = request.env["hr.leave.type"].sudo()
+ return [
+ {"name": leave_type.name, "days": round(leave_type.with_context(employee_id=employee.id).virtual_remaining_leaves, 2)}
+ for leave_type in LeaveType.search([], limit=3)
+ ]
+
+ def _recent_activity(self, employee):
+ Attendance = request.env["hr.attendance"].sudo()
+ records = Attendance.search([("employee_id", "=", employee.id)], order="check_in desc", limit=5)
+ return [{"label": "Attendance", "detail": record.check_in, "status": "Present" if record.check_out else "Open"} for record in records]
+
+ def _knowledge(self, role, limit=12):
+ domain = [("active", "=", True), ("audience", "in", ["all", "client" if role == "client" else "employee"])]
+ return request.env["mcs.knowledge.article"].sudo().search(domain, limit=limit)
+
+ def _announcements(self, role, limit=10):
+ domain = [("active", "=", True), ("audience", "in", ["all", "client" if role == "client" else "employee"])]
+ return request.env["mcs.announcement"].sudo().search(domain, order="date desc, id desc", limit=limit)
+
def _contact(self):
return {
"phone": "+1-647-679-7651",
@@ -98,6 +235,69 @@ class McsCorporateWebsite(http.Controller):
def privacy(self, **kwargs):
return self._render("mcs_corporate_website.page_privacy")
+ @http.route(["/workplace"], type="http", auth="user", website=True)
+ def workplace_dashboard(self, **kwargs):
+ role = self._role()
+ template = "mcs_corporate_website.workplace_client_dashboard" if role == "client" else "mcs_corporate_website.workplace_employee_dashboard"
+ return self._workplace_render(template, self._dashboard_values(role))
+
+ @http.route(["/workplace/attendance"], type="http", auth="user", website=True)
+ def workplace_attendance(self, **kwargs):
+ employee = self._employee()
+ if not employee:
+ return request.redirect("/workplace")
+ today = fields.Date.context_today(request.env.user)
+ month_start = today.replace(day=1)
+ records = request.env["hr.attendance"].sudo().search(
+ [("employee_id", "=", employee.id), ("check_in", ">=", fields.Date.to_string(month_start))],
+ order="check_in desc",
+ limit=40,
+ )
+ return self._workplace_render(
+ "mcs_corporate_website.workplace_attendance",
+ {"attendance_records": records, "attendance": records[:1], "worked_hours": round(sum(records.mapped("worked_hours")), 2)},
+ )
+
+ @http.route(["/workplace/leave"], type="http", auth="user", website=True)
+ def workplace_leave(self, **kwargs):
+ employee = self._employee()
+ if not employee:
+ return request.redirect("/workplace")
+ leaves = request.env["hr.leave"].sudo().search([("employee_id", "=", employee.id)], order="request_date_from desc", limit=20)
+ return self._workplace_render("mcs_corporate_website.workplace_leave", {"leave_balance": self._leave_balance(employee), "leaves": leaves})
+
+ @http.route(["/workplace/tickets", "/workplace/support-queue"], type="http", auth="user", website=True)
+ def workplace_tickets(self, **kwargs):
+ role = self._role()
+ tickets = request.env["mcs.support.ticket"].sudo().search(self._ticket_domain(role), order="priority desc, write_date desc", limit=80)
+ template = "mcs_corporate_website.workplace_support_queue" if role in ("admin", "support_agent") and request.httprequest.path.endswith("support-queue") else "mcs_corporate_website.workplace_ticket_list"
+ return self._workplace_render(template, {"tickets": self._ticket_values(tickets)})
+
+ @http.route(["/workplace/tickets/new"], type="http", auth="user", website=True)
+ def workplace_ticket_new(self, **kwargs):
+ return self._workplace_render("mcs_corporate_website.workplace_ticket_new")
+
+ @http.route(["/workplace/tickets/"], type="http", auth="user", website=True)
+ def workplace_ticket_detail(self, ticket_id, **kwargs):
+ role = self._role()
+ ticket = request.env["mcs.support.ticket"].sudo().search(self._ticket_domain(role) + [("id", "=", ticket_id)], limit=1)
+ if not ticket:
+ return request.not_found()
+ messages = ticket.message_ids.sorted(lambda item: item.date)
+ return self._workplace_render("mcs_corporate_website.workplace_ticket_detail", {"ticket": ticket, "messages": messages})
+
+ @http.route(["/workplace/knowledge"], type="http", auth="user", website=True)
+ def workplace_knowledge(self, **kwargs):
+ return self._workplace_render("mcs_corporate_website.workplace_knowledge", {"articles": self._knowledge(self._role())})
+
+ @http.route(["/workplace/announcements"], type="http", auth="user", website=True)
+ def workplace_announcements(self, **kwargs):
+ return self._workplace_render("mcs_corporate_website.workplace_announcements", {"announcements": self._announcements(self._role())})
+
+ @http.route(["/workplace/profile"], type="http", auth="user", website=True)
+ def workplace_profile(self, **kwargs):
+ return self._workplace_render("mcs_corporate_website.workplace_profile")
+
@http.route(["/support/submit"], type="http", auth="public", website=True, methods=["POST"])
def submit_support(self, **post):
ticket = request.env["mcs.support.ticket"].sudo().create(
diff --git a/addons/mcs_corporate_website/data/workplace_data.xml b/addons/mcs_corporate_website/data/workplace_data.xml
new file mode 100644
index 0000000..80c1b0a
--- /dev/null
+++ b/addons/mcs_corporate_website/data/workplace_data.xml
@@ -0,0 +1,29 @@
+
+
+ Getting started with the workplace portal
+ getting_started
+ Use the dashboard to access attendance, leave, support, announcements, and profile information.
+ all
+ The workplace portal brings daily employee workflows and customer support into one authenticated Odoo-backed interface.
]]>
+
+
+ How to raise a useful support ticket
+ technical
+ Include project name, affected page or module, priority, screenshots, and expected outcome.
+ all
+ Clear ticket details help the team triage, assign, and resolve issues faster.]]>
+
+
+ Billing and invoice support
+ billing
+ Create a support ticket for invoice questions, payment references, or account updates.
+ client
+ Billing queries are tracked through support tickets so communication remains attached to the customer account.]]>
+
+
+ Metatroncube Workplace portal update
+ company
+ all
+ The workplace portal now provides a cleaner dashboard for employee and customer support workflows.]]>
+
+
diff --git a/addons/mcs_corporate_website/models/__init__.py b/addons/mcs_corporate_website/models/__init__.py
index 17c91c3..9ae3e32 100644
--- a/addons/mcs_corporate_website/models/__init__.py
+++ b/addons/mcs_corporate_website/models/__init__.py
@@ -1,2 +1,4 @@
from . import support_ticket
from . import website_setup
+from . import knowledge
+from . import announcement
diff --git a/addons/mcs_corporate_website/models/announcement.py b/addons/mcs_corporate_website/models/announcement.py
new file mode 100644
index 0000000..cb5366b
--- /dev/null
+++ b/addons/mcs_corporate_website/models/announcement.py
@@ -0,0 +1,37 @@
+from odoo import fields, models
+
+
+class McsAnnouncement(models.Model):
+ _name = "mcs.announcement"
+ _description = "Metatroncube Announcement"
+ _inherit = ["mail.thread", "mail.activity.mixin"]
+ _order = "date desc, id desc"
+
+ name = fields.Char(string="Title", required=True, tracking=True)
+ author_id = fields.Many2one(
+ "res.users",
+ default=lambda self: self.env.user,
+ required=True,
+ tracking=True,
+ )
+ date = fields.Date(default=fields.Date.context_today, required=True, tracking=True)
+ category = fields.Selection(
+ [
+ ("company", "Company"),
+ ("hr", "HR"),
+ ("delivery", "Delivery"),
+ ("it", "Internal IT"),
+ ("client", "Client"),
+ ],
+ default="company",
+ required=True,
+ tracking=True,
+ )
+ audience = fields.Selection(
+ [("all", "All"), ("employee", "Employees"), ("client", "Clients")],
+ default="employee",
+ required=True,
+ tracking=True,
+ )
+ content = fields.Html(required=True)
+ active = fields.Boolean(default=True)
diff --git a/addons/mcs_corporate_website/models/knowledge.py b/addons/mcs_corporate_website/models/knowledge.py
new file mode 100644
index 0000000..5320865
--- /dev/null
+++ b/addons/mcs_corporate_website/models/knowledge.py
@@ -0,0 +1,30 @@
+from odoo import fields, models
+
+
+class McsKnowledgeArticle(models.Model):
+ _name = "mcs.knowledge.article"
+ _description = "Metatroncube Knowledge Article"
+ _order = "sequence, name"
+
+ name = fields.Char(required=True)
+ category = fields.Selection(
+ [
+ ("getting_started", "Getting Started"),
+ ("account", "Account"),
+ ("billing", "Billing"),
+ ("website", "Website"),
+ ("technical", "Technical Support"),
+ ("internal_it", "Internal IT"),
+ ],
+ default="getting_started",
+ required=True,
+ )
+ summary = fields.Char(required=True)
+ body = fields.Html()
+ audience = fields.Selection(
+ [("all", "All"), ("employee", "Employees"), ("client", "Clients")],
+ default="all",
+ required=True,
+ )
+ sequence = fields.Integer(default=10)
+ active = fields.Boolean(default=True)
diff --git a/addons/mcs_corporate_website/security/ir.model.access.csv b/addons/mcs_corporate_website/security/ir.model.access.csv
index 756e9d3..8eae497 100644
--- a/addons/mcs_corporate_website/security/ir.model.access.csv
+++ b/addons/mcs_corporate_website/security/ir.model.access.csv
@@ -1,3 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mcs_support_ticket_user,mcs.support.ticket.user,model_mcs_support_ticket,mcs_corporate_website.group_mcs_website_support_user,1,1,1,0
access_mcs_support_ticket_manager,mcs.support.ticket.manager,model_mcs_support_ticket,mcs_corporate_website.group_mcs_website_support_manager,1,1,1,1
+access_mcs_knowledge_article_user,mcs.knowledge.article.user,model_mcs_knowledge_article,mcs_corporate_website.group_mcs_website_support_user,1,1,1,0
+access_mcs_knowledge_article_manager,mcs.knowledge.article.manager,model_mcs_knowledge_article,mcs_corporate_website.group_mcs_website_support_manager,1,1,1,1
+access_mcs_announcement_user,mcs.announcement.user,model_mcs_announcement,mcs_corporate_website.group_mcs_website_support_user,1,1,1,0
+access_mcs_announcement_manager,mcs.announcement.manager,model_mcs_announcement,mcs_corporate_website.group_mcs_website_support_manager,1,1,1,1
diff --git a/addons/mcs_corporate_website/static/src/scss/corporate_website.scss b/addons/mcs_corporate_website/static/src/scss/corporate_website.scss
index 387b315..21e81c8 100644
--- a/addons/mcs_corporate_website/static/src/scss/corporate_website.scss
+++ b/addons/mcs_corporate_website/static/src/scss/corporate_website.scss
@@ -399,3 +399,476 @@
padding: 56px 0;
}
}
+
+.mcs-workplace {
+ --background: #F6F7F9;
+ --surface: #FFFFFF;
+ --sidebar: #101114;
+ --sidebar-hover: #1B1D21;
+ --text-primary: #17181B;
+ --text-secondary: #6B7280;
+ --border: #E6E8EC;
+ --accent: #1C4ED8;
+ --success: #16A34A;
+ --warning: #F59E0B;
+ --danger: #DC2626;
+
+ min-height: calc(100vh - 76px);
+ display: grid;
+ grid-template-columns: 250px minmax(0, 1fr);
+ background: var(--background);
+ color: var(--text-primary);
+ font-family: Inter, Manrope, "Segoe UI", sans-serif;
+
+ h1,
+ h2,
+ h3 {
+ letter-spacing: 0;
+ }
+}
+
+.mcs-wp-sidebar {
+ position: sticky;
+ top: 0;
+ min-height: calc(100vh - 76px);
+ background: var(--sidebar);
+ padding: 22px 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 26px;
+
+ nav {
+ display: grid;
+ gap: 6px;
+ }
+}
+
+.mcs-wp-brand {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ color: #fff;
+ text-decoration: none;
+ padding: 0 8px;
+
+ img {
+ width: 42px;
+ height: 42px;
+ object-fit: contain;
+ }
+
+ span {
+ display: grid;
+ font-size: 1.05rem;
+ font-weight: 800;
+ line-height: 1.1;
+ }
+
+ small {
+ color: #9CA3AF;
+ font-size: 0.66rem;
+ letter-spacing: 0.16em;
+ margin-top: 3px;
+ }
+}
+
+.mcs-wp-nav,
+.mcs-wp-sidebar-bottom a {
+ color: #CBD5E1;
+ text-decoration: none;
+ border-radius: 10px;
+ padding: 11px 12px;
+ font-weight: 700;
+
+ &:hover,
+ &.active {
+ color: #fff;
+ background: var(--sidebar-hover);
+ }
+}
+
+.mcs-wp-sidebar-bottom {
+ margin-top: auto;
+ display: grid;
+ gap: 4px;
+}
+
+.mcs-wp-main {
+ min-width: 0;
+}
+
+.mcs-wp-topbar {
+ height: 72px;
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 32px;
+}
+
+.mcs-wp-kicker {
+ display: block;
+ color: var(--text-secondary);
+ font-size: 0.78rem;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+}
+
+.mcs-wp-user {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ color: var(--text-secondary);
+ font-weight: 700;
+}
+
+.mcs-avatar {
+ width: 36px;
+ height: 36px;
+ display: inline-grid;
+ place-items: center;
+ border-radius: 999px;
+ background: var(--accent);
+ color: #fff;
+ font-weight: 800;
+}
+
+.mcs-avatar--lg {
+ width: 84px;
+ height: 84px;
+ font-size: 2rem;
+}
+
+.mcs-wp-content {
+ max-width: 1500px;
+ margin: 0 auto;
+ padding: 34px;
+}
+
+.mcs-wp-page-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+ margin-bottom: 24px;
+
+ p {
+ margin: 0 0 6px;
+ color: var(--text-secondary);
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ font-size: 0.78rem;
+ }
+
+ h1 {
+ margin: 0;
+ font-size: 2.15rem;
+ line-height: 1.12;
+ font-weight: 820;
+ }
+}
+
+.mcs-wp-grid {
+ display: grid;
+ gap: 18px;
+ margin-bottom: 22px;
+}
+
+.mcs-wp-grid--2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.mcs-wp-grid--3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.mcs-wp-grid--4 {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.mcs-wp-two {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 380px;
+ gap: 22px;
+ margin-bottom: 22px;
+ align-items: start;
+}
+
+.mcs-wp-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 22px;
+ box-shadow: 0 14px 38px rgba(17, 24, 39, 0.06);
+}
+
+.mcs-metric {
+ display: grid;
+ gap: 8px;
+
+ span {
+ color: var(--text-secondary);
+ font-weight: 800;
+ font-size: 0.82rem;
+ }
+
+ strong {
+ font-size: 1.9rem;
+ line-height: 1;
+ }
+
+ small {
+ color: var(--text-secondary);
+ }
+}
+
+.mcs-card-head,
+.mcs-list-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+}
+
+.mcs-card-head {
+ margin-bottom: 16px;
+
+ h2 {
+ margin: 0;
+ font-size: 1.15rem;
+ font-weight: 820;
+ }
+}
+
+.mcs-list-row {
+ padding: 13px 0;
+ border-top: 1px solid var(--border);
+
+ span {
+ color: var(--text-secondary);
+ }
+}
+
+.mcs-list-row--stack {
+ display: grid;
+ justify-content: start;
+}
+
+.mcs-wp-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 0;
+ border-radius: 10px;
+ padding: 11px 16px;
+ background: var(--accent);
+ color: #fff !important;
+ font-weight: 800;
+ text-decoration: none;
+ box-shadow: 0 10px 24px rgba(28, 78, 216, 0.2);
+}
+
+.mcs-table-wrap {
+ width: 100%;
+ overflow-x: auto;
+}
+
+.mcs-wp-table {
+ width: 100%;
+ border-collapse: collapse;
+
+ th,
+ td {
+ padding: 13px 12px;
+ border-bottom: 1px solid var(--border);
+ white-space: nowrap;
+ text-align: left;
+ }
+
+ th {
+ color: var(--text-secondary);
+ font-size: 0.78rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ }
+}
+
+.mcs-status {
+ display: inline-flex;
+ align-items: center;
+ min-height: 26px;
+ border-radius: 999px;
+ padding: 4px 10px;
+ background: #EEF2FF;
+ color: #3730A3;
+ font-size: 0.78rem;
+ font-weight: 800;
+}
+
+.mcs-ticket-in_progress,
+.mcs-priority-1 {
+ background: #FEF3C7;
+ color: #92400E;
+}
+
+.mcs-ticket-resolved,
+.mcs-ticket-closed,
+.mcs-att-present {
+ background: #DCFCE7;
+ color: #166534;
+}
+
+.mcs-ticket-waiting {
+ background: #F3E8FF;
+ color: #6B21A8;
+}
+
+.mcs-priority-2 {
+ background: #FEE2E2;
+ color: #991B1B;
+}
+
+.mcs-filterbar {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-bottom: 16px;
+
+ span {
+ border: 1px solid var(--border);
+ background: #fff;
+ border-radius: 999px;
+ padding: 8px 12px;
+ color: var(--text-secondary);
+ font-weight: 800;
+ }
+}
+
+.mcs-calendar-strip {
+ display: flex;
+ gap: 8px;
+ overflow-x: auto;
+ margin-bottom: 16px;
+
+ span {
+ min-width: 62px;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 10px;
+ display: grid;
+ text-align: center;
+ font-weight: 800;
+ }
+
+ small {
+ color: var(--success);
+ font-size: 0.7rem;
+ }
+}
+
+.mcs-wp-form {
+ display: grid;
+ gap: 15px;
+
+ label {
+ display: grid;
+ gap: 7px;
+ color: var(--text-secondary);
+ font-weight: 800;
+ }
+
+ input,
+ select,
+ textarea {
+ width: 100%;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 12px;
+ color: var(--text-primary);
+ background: #fff;
+ }
+
+ textarea {
+ min-height: 160px;
+ }
+}
+
+.mcs-search {
+ width: min(420px, 100%);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 12px 14px;
+}
+
+.mcs-timeline {
+ display: grid;
+ gap: 12px;
+}
+
+.mcs-message {
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 16px;
+ background: #F9FAFB;
+}
+
+.mcs-message--support {
+ background: #EFF6FF;
+ border-color: #BFDBFE;
+}
+
+.mcs-reply {
+ display: grid;
+ gap: 12px;
+ margin-top: 16px;
+
+ textarea {
+ min-height: 120px;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 12px;
+ }
+}
+
+.mcs-empty {
+ padding: 24px;
+ color: var(--text-secondary);
+ text-align: center;
+}
+
+.mcs-profile-card {
+ text-align: center;
+}
+
+@media (max-width: 991.98px) {
+ .mcs-workplace {
+ grid-template-columns: 1fr;
+ }
+
+ .mcs-wp-sidebar {
+ position: static;
+ min-height: auto;
+ }
+
+ .mcs-wp-grid--2,
+ .mcs-wp-grid--3,
+ .mcs-wp-grid--4,
+ .mcs-wp-two {
+ grid-template-columns: 1fr;
+ }
+
+ .mcs-wp-content {
+ padding: 22px;
+ }
+
+ .mcs-wp-page-head,
+ .mcs-wp-topbar {
+ align-items: flex-start;
+ flex-direction: column;
+ height: auto;
+ padding: 20px;
+ }
+}
diff --git a/addons/mcs_corporate_website/views/workplace_backend_views.xml b/addons/mcs_corporate_website/views/workplace_backend_views.xml
new file mode 100644
index 0000000..93ba8fc
--- /dev/null
+++ b/addons/mcs_corporate_website/views/workplace_backend_views.xml
@@ -0,0 +1,82 @@
+
+
+ mcs.knowledge.article.tree
+ mcs.knowledge.article
+
+
+
+
+
+
+
+
+
+
+
+ mcs.knowledge.article.form
+ mcs.knowledge.article
+
+
+
+
+
+ Knowledge Articles
+ mcs.knowledge.article
+ tree,form
+
+
+
+ mcs.announcement.tree
+ mcs.announcement
+
+
+
+
+
+
+
+
+
+
+
+
+ mcs.announcement.form
+ mcs.announcement
+
+
+
+
+
+ Announcements
+ mcs.announcement
+ tree,form
+
+
+
+
+
diff --git a/addons/mcs_corporate_website/views/workplace_templates.xml b/addons/mcs_corporate_website/views/workplace_templates.xml
new file mode 100644
index 0000000..4df6593
--- /dev/null
+++ b/addons/mcs_corporate_website/views/workplace_templates.xml
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dashboard
+
Good morning,
+
+
+
+
+ Attendance StatusToday
+ Worked TodayhRecorded hours
+ Monthly AttendancePresent days
+ Assigned TicketsOpen queue
+
+
+
+ Clock In / Clock Out
Live
+ Attendance is recorded in Odoo. Use the main Attendance app for clock actions until biometric/device integration is enabled here.
+ Open Attendance
+
+
+
+
+ days
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No announcements yet.
+
+
+
+
+
+
+
+
+
+ OpenNew tickets
+ In ProgressBeing handled
+ Waiting for YouNeeds reply
+ ResolvedClosed work
+
+
+
+
+
+
+
+
+ | Ticket | Subject | Priority | Status | Assigned Team | Updated | |
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ View |
+
+
+
+
+
No tickets found.
+
+
+
+
+
+
+
+ No articles are published yet.
+
+
+
+
+
+
+ Present DaysThis month
+ Late Days0Requires policy setup
+ Total HoursThis month
+ Overtime0hRequires policy setup
+
+ Present
+
+
+
+
+
+
+ | Date | Check In | Check Out | Worked Hours | Status |
+ | | | | Present |
+
+
+
+
+
+
+ Leave & Time Off
Leave & Time Off
Request Leave
+
+ Request History
+
+
+
+
+
+ Support Tickets
Support Tickets
Create Ticket
+
+
+
+
+
+
+ Support team queue
Support Tickets
+ AllMy TicketsUnassignedUrgentOverdueWaiting for Customer
+
+
+
+
+
+
+ Create Support Ticket
Create Support Ticket
+
+
+
+
+
+
+
+
+
+
+
+ Conversation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Announcements
Company Announcements
+
+
+
+
+
+
+
+
+
+ Information
Email
Phone
Designation
Department
Reporting Manager
+
+
+
+