Add SaaS workplace frontend

This commit is contained in:
metatroncubeswdev 2026-08-31 15:21:49 -04:00
parent 09130ef5b7
commit 21958eca15
10 changed files with 1100 additions and 0 deletions

View File

@ -19,8 +19,11 @@
"security/security.xml", "security/security.xml",
"security/ir.model.access.csv", "security/ir.model.access.csv",
"data/sequence_data.xml", "data/sequence_data.xml",
"data/workplace_data.xml",
"views/support_ticket_views.xml", "views/support_ticket_views.xml",
"views/workplace_backend_views.xml",
"views/website_templates.xml", "views/website_templates.xml",
"views/workplace_templates.xml",
"views/website_menus.xml", "views/website_menus.xml",
"data/website_data.xml", "data/website_data.xml",
], ],

View File

@ -13,6 +13,143 @@ class McsCorporateWebsite(http.Controller):
) )
return request.render(template, values) 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): def _contact(self):
return { return {
"phone": "+1-647-679-7651", "phone": "+1-647-679-7651",
@ -98,6 +235,69 @@ class McsCorporateWebsite(http.Controller):
def privacy(self, **kwargs): def privacy(self, **kwargs):
return self._render("mcs_corporate_website.page_privacy") 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/<int:ticket_id>"], 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"]) @http.route(["/support/submit"], type="http", auth="public", website=True, methods=["POST"])
def submit_support(self, **post): def submit_support(self, **post):
ticket = request.env["mcs.support.ticket"].sudo().create( ticket = request.env["mcs.support.ticket"].sudo().create(

View File

@ -0,0 +1,29 @@
<odoo noupdate="1">
<record id="knowledge_getting_started" model="mcs.knowledge.article">
<field name="name">Getting started with the workplace portal</field>
<field name="category">getting_started</field>
<field name="summary">Use the dashboard to access attendance, leave, support, announcements, and profile information.</field>
<field name="audience">all</field>
<field name="body"><![CDATA[<p>The workplace portal brings daily employee workflows and customer support into one authenticated Odoo-backed interface.</p>]]></field>
</record>
<record id="knowledge_support_ticket" model="mcs.knowledge.article">
<field name="name">How to raise a useful support ticket</field>
<field name="category">technical</field>
<field name="summary">Include project name, affected page or module, priority, screenshots, and expected outcome.</field>
<field name="audience">all</field>
<field name="body"><![CDATA[<p>Clear ticket details help the team triage, assign, and resolve issues faster.</p>]]></field>
</record>
<record id="knowledge_billing" model="mcs.knowledge.article">
<field name="name">Billing and invoice support</field>
<field name="category">billing</field>
<field name="summary">Create a support ticket for invoice questions, payment references, or account updates.</field>
<field name="audience">client</field>
<field name="body"><![CDATA[<p>Billing queries are tracked through support tickets so communication remains attached to the customer account.</p>]]></field>
</record>
<record id="announcement_workplace_launch" model="mcs.announcement">
<field name="name">Metatroncube Workplace portal update</field>
<field name="category">company</field>
<field name="audience">all</field>
<field name="content"><![CDATA[<p>The workplace portal now provides a cleaner dashboard for employee and customer support workflows.</p>]]></field>
</record>
</odoo>

View File

@ -1,2 +1,4 @@
from . import support_ticket from . import support_ticket
from . import website_setup from . import website_setup
from . import knowledge
from . import announcement

View File

@ -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)

View File

@ -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)

View File

@ -1,3 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 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_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_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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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
3 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
4 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
5 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
6 access_mcs_announcement_user mcs.announcement.user model_mcs_announcement mcs_corporate_website.group_mcs_website_support_user 1 1 1 0
7 access_mcs_announcement_manager mcs.announcement.manager model_mcs_announcement mcs_corporate_website.group_mcs_website_support_manager 1 1 1 1

View File

@ -399,3 +399,476 @@
padding: 56px 0; 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;
}
}

View File

@ -0,0 +1,82 @@
<odoo>
<record id="view_mcs_knowledge_article_tree" model="ir.ui.view">
<field name="name">mcs.knowledge.article.tree</field>
<field name="model">mcs.knowledge.article</field>
<field name="arch" type="xml">
<tree>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="category"/>
<field name="audience"/>
<field name="active"/>
</tree>
</field>
</record>
<record id="view_mcs_knowledge_article_form" model="ir.ui.view">
<field name="name">mcs.knowledge.article.form</field>
<field name="model">mcs.knowledge.article</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
<field name="summary"/>
<field name="category"/>
<field name="audience"/>
<field name="sequence"/>
<field name="active"/>
</group>
<field name="body"/>
</sheet>
</form>
</field>
</record>
<record id="action_mcs_knowledge_article" model="ir.actions.act_window">
<field name="name">Knowledge Articles</field>
<field name="res_model">mcs.knowledge.article</field>
<field name="view_mode">tree,form</field>
</record>
<record id="view_mcs_announcement_tree" model="ir.ui.view">
<field name="name">mcs.announcement.tree</field>
<field name="model">mcs.announcement</field>
<field name="arch" type="xml">
<tree>
<field name="date"/>
<field name="name"/>
<field name="author_id"/>
<field name="category"/>
<field name="audience"/>
<field name="active"/>
</tree>
</field>
</record>
<record id="view_mcs_announcement_form" model="ir.ui.view">
<field name="name">mcs.announcement.form</field>
<field name="model">mcs.announcement</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
<field name="date"/>
<field name="author_id"/>
<field name="category"/>
<field name="audience"/>
<field name="active"/>
</group>
<field name="content"/>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="action_mcs_announcement" model="ir.actions.act_window">
<field name="name">Announcements</field>
<field name="res_model">mcs.announcement</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_mcs_knowledge_article" name="Knowledge Base" parent="menu_mcs_website_root" action="action_mcs_knowledge_article" sequence="20"/>
<menuitem id="menu_mcs_announcement" name="Announcements" parent="menu_mcs_website_root" action="action_mcs_announcement" sequence="30"/>
</odoo>

View File

@ -0,0 +1,240 @@
<odoo>
<template id="workplace_layout" name="MCS Workplace App Layout">
<t t-call="website.layout">
<div id="wrap" class="mcs-workplace">
<aside class="mcs-wp-sidebar">
<a class="mcs-wp-brand" href="/workplace">
<img src="/mcs_corporate_website/static/src/img/metatroncube-logo.png" alt="Metatroncube"/>
<span>Metatroncube<small>WORKPLACE</small></span>
</a>
<nav>
<t t-foreach="wp_nav" t-as="item">
<a t-att-href="item[1]" t-attf-class="mcs-wp-nav {{ 'active' if request.httprequest.path == item[1] else '' }}">
<span t-esc="item[0]"/>
</a>
</t>
</nav>
<div class="mcs-wp-sidebar-bottom">
<a href="/workplace/profile">Profile</a>
<a href="/my/account">Settings</a>
<a href="/web/session/logout?redirect=/">Logout</a>
</div>
</aside>
<main class="mcs-wp-main">
<header class="mcs-wp-topbar">
<div>
<span class="mcs-wp-kicker" t-esc="wp_role.replace('_', ' ').title()"/>
<strong t-esc="wp_company_name"/>
</div>
<div class="mcs-wp-user">
<span t-esc="wp_user.name"/>
<span class="mcs-avatar" t-esc="wp_user.name[:1]"/>
</div>
</header>
<div class="mcs-wp-content">
<t t-out="0"/>
</div>
</main>
</div>
</t>
</template>
<template id="mcs_metric_card" name="MCS Metric Card">
<article class="mcs-wp-card mcs-metric">
<span t-esc="label"/>
<strong t-esc="value"/>
<small t-if="hint" t-esc="hint"/>
</article>
</template>
<template id="workplace_employee_dashboard" name="Workplace Employee Dashboard">
<t t-call="mcs_corporate_website.workplace_layout">
<section class="mcs-wp-page-head">
<div>
<p>Dashboard</p>
<h1>Good morning, <t t-esc="wp_first_name"/></h1>
</div>
<span t-esc="wp_today.strftime('%A, %d %B %Y')"/>
</section>
<section class="mcs-wp-grid mcs-wp-grid--4">
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">Attendance Status</t><t t-set="value"><t t-esc="'Clocked in' if attendance and not attendance.check_out else 'Not clocked in'"/></t><t t-set="hint">Today</t></t>
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">Worked Today</t><t t-set="value"><t t-esc="worked_today"/>h</t><t t-set="hint">Recorded hours</t></t>
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">Monthly Attendance</t><t t-set="value" t-value="month_present"/><t t-set="hint">Present days</t></t>
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">Assigned Tickets</t><t t-set="value" t-value="ticket_counts['open'] + ticket_counts['in_progress']"/><t t-set="hint">Open queue</t></t>
</section>
<section class="mcs-wp-two">
<article class="mcs-wp-card">
<div class="mcs-card-head"><h2>Clock In / Clock Out</h2><span class="mcs-status mcs-status--blue">Live</span></div>
<p>Attendance is recorded in Odoo. Use the main Attendance app for clock actions until biometric/device integration is enabled here.</p>
<a class="mcs-wp-btn" href="/web#menu_id=120&amp;action=136">Open Attendance</a>
</article>
<article class="mcs-wp-card">
<div class="mcs-card-head"><h2>Leave Balance</h2><a href="/workplace/leave">View all</a></div>
<t t-foreach="leave_balance" t-as="leave">
<div class="mcs-list-row"><span t-esc="leave['name']"/><strong><t t-esc="leave['days']"/> days</strong></div>
</t>
</article>
</section>
<section class="mcs-wp-two">
<article class="mcs-wp-card">
<div class="mcs-card-head"><h2>Recent Support Tickets</h2><a href="/workplace/tickets">View all</a></div>
<t t-call="mcs_corporate_website.ticket_table"/>
</article>
<article class="mcs-wp-card">
<div class="mcs-card-head"><h2>Announcements</h2><a href="/workplace/announcements">View all</a></div>
<t t-foreach="announcements" t-as="announcement">
<div class="mcs-list-row mcs-list-row--stack"><strong t-esc="announcement.name"/><span t-field="announcement.date"/></div>
</t>
<t t-if="not announcements"><p>No announcements yet.</p></t>
</article>
</section>
</t>
</template>
<template id="workplace_client_dashboard" name="Workplace Client Dashboard">
<t t-call="mcs_corporate_website.workplace_layout">
<section class="mcs-wp-page-head">
<div><p>Client dashboard</p><h1>Welcome, <t t-esc="wp_company_name"/></h1></div>
<a class="mcs-wp-btn" href="/workplace/tickets/new">Create New Ticket</a>
</section>
<section class="mcs-wp-grid mcs-wp-grid--4">
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">Open</t><t t-set="value" t-value="ticket_counts['open']"/><t t-set="hint">New tickets</t></t>
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">In Progress</t><t t-set="value" t-value="ticket_counts['in_progress']"/><t t-set="hint">Being handled</t></t>
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">Waiting for You</t><t t-set="value" t-value="ticket_counts['waiting']"/><t t-set="hint">Needs reply</t></t>
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">Resolved</t><t t-set="value" t-value="ticket_counts['resolved']"/><t t-set="hint">Closed work</t></t>
</section>
<section class="mcs-wp-two">
<article class="mcs-wp-card"><div class="mcs-card-head"><h2>Recent Tickets</h2><a href="/workplace/tickets">View all</a></div><t t-call="mcs_corporate_website.ticket_table"/></article>
<article class="mcs-wp-card"><div class="mcs-card-head"><h2>Knowledge Base</h2><a href="/workplace/knowledge">Open</a></div><t t-call="mcs_corporate_website.article_list"/></article>
</section>
</t>
</template>
<template id="ticket_table" name="MCS Ticket Table">
<div class="mcs-table-wrap">
<table class="mcs-wp-table">
<thead><tr><th>Ticket</th><th>Subject</th><th>Priority</th><th>Status</th><th>Assigned Team</th><th>Updated</th><th></th></tr></thead>
<tbody>
<t t-foreach="tickets" t-as="ticket">
<tr>
<td t-esc="ticket['number']"/>
<td t-esc="ticket['subject']"/>
<td><span t-attf-class="mcs-status mcs-priority-{{ ticket['priority_key'] }}" t-esc="ticket['priority']"/></td>
<td><span t-attf-class="mcs-status mcs-ticket-{{ ticket['status_key'] }}" t-esc="ticket['status']"/></td>
<td t-esc="ticket['assigned_team']"/>
<td t-esc="ticket['updated_label']"/>
<td><a t-att-href="'/workplace/tickets/%s' % ticket['id']">View</a></td>
</tr>
</t>
</tbody>
</table>
<div t-if="not tickets" class="mcs-empty">No tickets found.</div>
</div>
</template>
<template id="article_list" name="MCS Article List">
<t t-foreach="articles" t-as="article">
<div class="mcs-list-row mcs-list-row--stack"><strong t-esc="article.name"/><span t-esc="article.summary"/></div>
</t>
<t t-if="not articles"><p>No articles are published yet.</p></t>
</template>
<template id="workplace_attendance" name="Workplace Attendance">
<t t-call="mcs_corporate_website.workplace_layout">
<section class="mcs-wp-page-head"><div><p>Attendance</p><h1>Attendance</h1></div><span t-esc="wp_today.strftime('%A, %d %B %Y')"/></section>
<section class="mcs-wp-grid mcs-wp-grid--4">
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">Present Days</t><t t-set="value" t-value="len(attendance_records)"/><t t-set="hint">This month</t></t>
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">Late Days</t><t t-set="value">0</t><t t-set="hint">Requires policy setup</t></t>
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">Total Hours</t><t t-set="value" t-value="worked_hours"/><t t-set="hint">This month</t></t>
<t t-call="mcs_corporate_website.mcs_metric_card"><t t-set="label">Overtime</t><t t-set="value">0h</t><t t-set="hint">Requires policy setup</t></t>
</section>
<article class="mcs-wp-card"><div class="mcs-card-head"><h2>Monthly Attendance</h2><a href="/web#menu_id=120&amp;action=136">Open Odoo Attendance</a></div><div class="mcs-calendar-strip"><t t-foreach="attendance_records[:20]" t-as="record"><span><t t-esc="record.check_in.strftime('%d')"/> <small>Present</small></span></t></div><t t-call="mcs_corporate_website.attendance_table"/></article>
</t>
</template>
<template id="attendance_table" name="MCS Attendance Table">
<div class="mcs-table-wrap">
<table class="mcs-wp-table">
<thead><tr><th>Date</th><th>Check In</th><th>Check Out</th><th>Worked Hours</th><th>Status</th></tr></thead>
<tbody><t t-foreach="attendance_records" t-as="record"><tr><td><span t-field="record.check_in"/></td><td><span t-field="record.check_in"/></td><td><span t-field="record.check_out"/></td><td t-esc="round(record.worked_hours, 2)"/><td><span class="mcs-status mcs-att-present">Present</span></td></tr></t></tbody>
</table>
</div>
</template>
<template id="workplace_leave" name="Workplace Leave">
<t t-call="mcs_corporate_website.workplace_layout">
<section class="mcs-wp-page-head"><div><p>Leave &amp; Time Off</p><h1>Leave &amp; Time Off</h1></div><a class="mcs-wp-btn" href="/web#model=hr.leave&amp;view_type=form">Request Leave</a></section>
<section class="mcs-wp-grid mcs-wp-grid--3"><t t-foreach="leave_balance" t-as="leave"><article class="mcs-wp-card mcs-metric"><span t-esc="leave['name']"/><strong><t t-esc="leave['days']"/> days</strong><small>Available balance</small></article></t></section>
<article class="mcs-wp-card"><div class="mcs-card-head"><h2>Request History</h2></div><div class="mcs-table-wrap"><table class="mcs-wp-table"><thead><tr><th>Leave Type</th><th>From</th><th>To</th><th>Status</th></tr></thead><tbody><t t-foreach="leaves" t-as="leave"><tr><td t-esc="leave.holiday_status_id.name"/><td t-esc="leave.request_date_from"/><td t-esc="leave.request_date_to"/><td><span class="mcs-status" t-esc="leave.state"/></td></tr></t></tbody></table></div></article>
</t>
</template>
<template id="workplace_ticket_list" name="Workplace Tickets">
<t t-call="mcs_corporate_website.workplace_layout">
<section class="mcs-wp-page-head"><div><p>Support Tickets</p><h1>Support Tickets</h1></div><a class="mcs-wp-btn" href="/workplace/tickets/new">Create Ticket</a></section>
<article class="mcs-wp-card"><t t-call="mcs_corporate_website.ticket_table"/></article>
</t>
</template>
<template id="workplace_support_queue" name="Workplace Support Queue">
<t t-call="mcs_corporate_website.workplace_layout">
<section class="mcs-wp-page-head"><div><p>Support team queue</p><h1>Support Tickets</h1></div></section>
<div class="mcs-filterbar"><span>All</span><span>My Tickets</span><span>Unassigned</span><span>Urgent</span><span>Overdue</span><span>Waiting for Customer</span></div>
<article class="mcs-wp-card"><t t-call="mcs_corporate_website.ticket_table"/></article>
</t>
</template>
<template id="workplace_ticket_new" name="Workplace Create Ticket">
<t t-call="mcs_corporate_website.workplace_layout">
<section class="mcs-wp-page-head"><div><p>Create Support Ticket</p><h1>Create Support Ticket</h1></div></section>
<section class="mcs-wp-two">
<form action="/support/submit" method="post" class="mcs-wp-card mcs-wp-form" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<label>Subject<input name="subject" required="required"/></label>
<label>Category<select name="service_area"><option value="general">General</option><option value="website">Website</option><option value="erp">ERP / Odoo</option><option value="seo">SEO / Content</option><option value="marketing">Digital Marketing</option><option value="design">Design / Branding</option></select></label>
<label>Priority<select name="priority"><option value="0">Normal</option><option value="1">High</option><option value="2">Urgent</option></select></label>
<label>Description<textarea name="description" required="required"/></label>
<label>Contact Person<input name="customer_name" t-att-value="wp_user.name" required="required"/></label>
<label>Email<input type="email" name="customer_email" t-att-value="wp_user.email" required="required"/></label>
<label>Attachments<input type="file" name="attachments" multiple="multiple"/></label>
<button class="mcs-wp-btn" type="submit">Create Ticket</button>
</form>
<aside class="mcs-wp-card"><h2>Support hours</h2><p>Standard support requests are reviewed during business hours. Urgent production issues should include affected system, business impact, and escalation contact.</p><div class="mcs-list-row"><span>Expected response</span><strong>1 business day</strong></div><div class="mcs-list-row"><span>Emergency</span><strong>Mark Urgent</strong></div></aside>
</section>
</t>
</template>
<template id="workplace_ticket_detail" name="Workplace Ticket Detail">
<t t-call="mcs_corporate_website.workplace_layout">
<section class="mcs-wp-page-head"><div><p t-esc="ticket.name"/><h1 t-esc="ticket.subject"/></div><span class="mcs-status" t-esc="ticket.state"/></section>
<section class="mcs-wp-two">
<article class="mcs-wp-card"><div class="mcs-card-head"><h2>Conversation</h2></div><div class="mcs-timeline"><div class="mcs-message"><strong t-esc="ticket.customer_name"/><p t-esc="ticket.description"/></div><t t-foreach="messages" t-as="message"><div class="mcs-message mcs-message--support"><strong t-esc="message.author_id.name"/><p t-out="message.body"/></div></t></div><form class="mcs-reply"><textarea placeholder="Write a reply"/><button class="mcs-wp-btn" type="button">Send Reply</button></form></article>
<aside class="mcs-wp-card"><h2>Ticket details</h2><div class="mcs-list-row"><span>Status</span><strong t-esc="ticket.state"/></div><div class="mcs-list-row"><span>Priority</span><strong t-esc="ticket.priority"/></div><div class="mcs-list-row"><span>Assigned Team</span><strong t-esc="ticket.service_area"/></div><div class="mcs-list-row"><span>Created</span><strong><span t-field="ticket.create_date"/></strong></div></aside>
</section>
</t>
</template>
<template id="workplace_knowledge" name="Workplace Knowledge">
<t t-call="mcs_corporate_website.workplace_layout">
<section class="mcs-wp-page-head"><div><p>Knowledge Base</p><h1>Search the knowledge base</h1></div><input class="mcs-search" placeholder="Search the knowledge base"/></section>
<section class="mcs-wp-grid mcs-wp-grid--3"><t t-foreach="articles" t-as="article"><article class="mcs-wp-card"><span class="mcs-status" t-esc="article.category.replace('_', ' ').title()"/><h2 t-esc="article.name"/><p t-esc="article.summary"/><div t-out="article.body"/></article></t></section>
</t>
</template>
<template id="workplace_announcements" name="Workplace Announcements">
<t t-call="mcs_corporate_website.workplace_layout">
<section class="mcs-wp-page-head"><div><p>Announcements</p><h1>Company Announcements</h1></div></section>
<section class="mcs-wp-grid mcs-wp-grid--2"><t t-foreach="announcements" t-as="announcement"><article class="mcs-wp-card"><span class="mcs-status" t-esc="announcement.category.title()"/><h2 t-esc="announcement.name"/><small><t t-esc="announcement.author_id.name"/> - <span t-field="announcement.date"/></small><div t-out="announcement.content"/></article></t></section>
</t>
</template>
<template id="workplace_profile" name="Workplace Profile">
<t t-call="mcs_corporate_website.workplace_layout">
<section class="mcs-wp-page-head"><div><p>Profile</p><h1 t-esc="wp_user.name"/></div><a class="mcs-wp-btn" href="/my/account">Edit Profile</a></section>
<section class="mcs-wp-two">
<article class="mcs-wp-card mcs-profile-card"><span class="mcs-avatar mcs-avatar--lg" t-esc="wp_user.name[:1]"/><h2 t-esc="wp_user.name"/><p t-esc="wp_role.replace('_', ' ').title()"/></article>
<article class="mcs-wp-card"><h2>Information</h2><div class="mcs-list-row"><span>Email</span><strong t-esc="wp_user.email"/></div><div class="mcs-list-row"><span>Phone</span><strong t-esc="wp_user.phone or '-'"/></div><div class="mcs-list-row"><span>Designation</span><strong t-esc="wp_employee.job_title if wp_employee else '-'"/></div><div class="mcs-list-row"><span>Department</span><strong t-esc="wp_employee.department_id.name if wp_employee and wp_employee.department_id else '-'"/></div><div class="mcs-list-row"><span>Reporting Manager</span><strong t-esc="wp_employee.parent_id.name if wp_employee and wp_employee.parent_id else '-'"/></div></article>
</section>
</t>
</template>
</odoo>