add tncsc_deployment Odoo module with executive dashboard and sidebar
Some checks failed
CI / Brand-leak check (push) Has been cancelled
CI / flake8 / pylint-odoo / manifest completeness (push) Has been cancelled
CI / Install all modules with --test-enable (push) Has been cancelled

This commit is contained in:
Alaguraj0361 2026-09-29 13:03:05 +05:30
parent 276e1e227b
commit 1b6647104e
17 changed files with 1263 additions and 371 deletions

View File

@ -26,6 +26,8 @@ that applies to the community_* product modules.
'website': 'https://www.metatroncube.com', 'website': 'https://www.metatroncube.com',
'license': 'LGPL-3', 'license': 'LGPL-3',
'depends': [ 'depends': [
'event',
'website_event',
'community_theme_base', 'community_theme_base',
'community_membership', 'community_membership',
'event_qr_ticketing', 'event_qr_ticketing',
@ -49,6 +51,7 @@ that applies to the community_* product modules.
'views/website_header_overrides.xml', 'views/website_header_overrides.xml',
'views/browser_branding.xml', 'views/browser_branding.xml',
'views/event_kanban_views.xml', 'views/event_kanban_views.xml',
'views/event_views.xml',
'views/community_dashboard_views.xml', 'views/community_dashboard_views.xml',
'views/login_templates.xml', 'views/login_templates.xml',
], ],

View File

@ -1,2 +1,3 @@
from . import res_company from . import res_company
from . import res_partner from . import res_partner
from . import event_event

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class EventEvent(models.Model):
_name = 'event.event'
_inherit = ['event.event', 'image.mixin']
registration_status = fields.Selection([
('open', 'Open for Registration'),
('closed', 'Registrations Closed'),
('sold_out', 'Sold Out'),
('ongoing', 'Happening Now'),
], string='Registration Status', compute='_compute_registration_status', compute_sudo=True)
is_registration_open = fields.Boolean(
string='Is Registration Open',
compute='_compute_registration_status',
compute_sudo=True,
)
stage_display_name = fields.Char(
string='Stage Display Name',
compute='_compute_stage_display_name',
compute_sudo=True,
)
@api.depends('stage_id', 'stage_id.name')
def _compute_stage_display_name(self):
for ev in self:
ev_sudo = ev.sudo()
ev.stage_display_name = ev_sudo.stage_id.name if ev_sudo.stage_id else 'Festival'
@api.depends(
'event_registrations_open',
'event_registrations_sold_out',
'is_done',
'is_ongoing',
'stage_id',
'stage_id.name',
'date_end',
'kanban_state',
)
def _compute_registration_status(self):
now = fields.Datetime.now()
for ev in self:
ev_sudo = ev.sudo()
stage_name = (ev_sudo.stage_id.name or '').lower() if ev_sudo.stage_id else ''
is_ended = bool(
ev_sudo.is_done
or stage_name in ['ended', 'done', 'cancelled', 'concluded']
or (ev_sudo.date_end and ev_sudo.date_end < now)
or (ev_sudo.kanban_state == 'cancel')
)
if ev_sudo.kanban_state == 'cancel':
ev.registration_status = 'closed'
ev.is_registration_open = False
elif is_ended:
ev.registration_status = 'closed'
ev.is_registration_open = False
elif ev_sudo.event_registrations_sold_out:
ev.registration_status = 'sold_out'
ev.is_registration_open = False
elif ev_sudo.is_ongoing and not ev_sudo.event_registrations_open:
ev.registration_status = 'ongoing'
ev.is_registration_open = False
elif ev_sudo.event_registrations_open:
ev.registration_status = 'open'
ev.is_registration_open = True
else:
ev.registration_status = 'closed'
ev.is_registration_open = False

View File

@ -76,4 +76,24 @@
<field name="comment">Reviews and publishes classified listings.</field> <field name="comment">Reviews and publishes classified listings.</field>
<field name="implied_ids" eval="[(4, ref('community_classifieds.group_classifieds_moderator'))]"/> <field name="implied_ids" eval="[(4, ref('community_classifieds.group_classifieds_moderator'))]"/>
</record> </record>
<!-- Public & Portal read-only access to event.stage for event badges and status display -->
<record id="access_event_stage_public" model="ir.model.access">
<field name="name">event.stage.public</field>
<field name="model_id" ref="event.model_event_stage"/>
<field name="group_id" ref="base.group_public"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
</record>
<record id="access_event_stage_portal" model="ir.model.access">
<field name="name">event.stage.portal</field>
<field name="model_id" ref="event.model_event_stage"/>
<field name="group_id" ref="base.group_portal"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
</record>
</odoo> </odoo>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 KiB

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@ -10,6 +10,7 @@ export class CommunityDashboard extends Component {
setup() { setup() {
this.actionService = useService("action"); this.actionService = useService("action");
this.menuService = useService("menu");
this.notification = useService("notification"); this.notification = useService("notification");
this.state = useState({ this.state = useState({
loading: true, loading: true,
@ -127,19 +128,39 @@ export class CommunityDashboard extends Component {
} }
} }
syncMenu(menuXmlId) {
if (!this.menuService || !menuXmlId) return;
try {
const menu = this.menuService.getAll().find((m) => m.xmlid === menuXmlId);
if (menu) {
const targetAppId = menu.appID || menu.id;
const currentApp = this.menuService.getCurrentApp();
if (targetAppId && (!currentApp || currentApp.id !== targetAppId)) {
this.menuService.setCurrentMenu(targetAppId);
}
}
} catch (e) {
console.warn("Failed to sync menu from dashboard:", e);
}
}
openMembers() { openMembers() {
this.syncMenu("community_membership.menu_membership_root");
this.actionService.doAction("community_membership.action_membership_members"); this.actionService.doAction("community_membership.action_membership_members");
} }
openTiers() { openTiers() {
this.syncMenu("community_membership.menu_membership_root");
this.actionService.doAction("community_membership.action_membership_tier"); this.actionService.doAction("community_membership.action_membership_tier");
} }
openEvents() { openEvents() {
this.syncMenu("event.event_main_menu");
this.actionService.doAction("event.action_event_view"); this.actionService.doAction("event.action_event_view");
} }
openEvent(eventId) { openEvent(eventId) {
this.syncMenu("event.event_main_menu");
if (!eventId) { if (!eventId) {
this.openEvents(); this.openEvents();
return; return;
@ -154,42 +175,52 @@ export class CommunityDashboard extends Component {
} }
openSchool() { openSchool() {
this.syncMenu("community_school.menu_school_root");
this.actionService.doAction("community_school.action_school_student"); this.actionService.doAction("community_school.action_school_student");
} }
openSchoolClasses() { openSchoolClasses() {
this.syncMenu("community_school.menu_school_root");
this.actionService.doAction("community_school.action_school_class"); this.actionService.doAction("community_school.action_school_class");
} }
openBenefits() { openBenefits() {
this.syncMenu("community_benefits.menu_benefits_root");
this.actionService.doAction("community_benefits.action_benefit"); this.actionService.doAction("community_benefits.action_benefit");
} }
openClassifieds() { openClassifieds() {
this.syncMenu("community_classifieds.menu_classifieds_root");
this.actionService.doAction("community_classifieds.action_classified_moderation"); this.actionService.doAction("community_classifieds.action_classified_moderation");
} }
openMarketplace() { openMarketplace() {
this.syncMenu("community_classifieds.menu_classifieds_root");
this.actionService.doAction("community_classifieds.action_classified_moderation"); this.actionService.doAction("community_classifieds.action_classified_moderation");
} }
openCulturalPrograms() { openCulturalPrograms() {
this.syncMenu("event.event_main_menu");
this.actionService.doAction("event.action_event_view"); this.actionService.doAction("event.action_event_view");
} }
openReports() { openReports() {
this.syncMenu("event.event_main_menu");
this.actionService.doAction("event.event_registration_action"); this.actionService.doAction("event.event_registration_action");
} }
openSettings() { openSettings() {
this.syncMenu("base.menu_administration");
this.actionService.doAction("base.action_res_users"); this.actionService.doAction("base.action_res_users");
} }
openInterac() { openInterac() {
this.syncMenu("community_interac.menu_interac_root");
this.actionService.doAction("community_interac.action_payment_transaction_interac_pending"); this.actionService.doAction("community_interac.action_payment_transaction_interac_pending");
} }
openNewEvent() { openNewEvent() {
this.syncMenu("event.event_main_menu");
this.actionService.doAction({ this.actionService.doAction({
type: "ir.actions.act_window", type: "ir.actions.act_window",
res_model: "event.event", res_model: "event.event",
@ -199,6 +230,7 @@ export class CommunityDashboard extends Component {
} }
openNewMember() { openNewMember() {
this.syncMenu("community_membership.menu_membership_root");
this.actionService.doAction({ this.actionService.doAction({
type: "ir.actions.act_window", type: "ir.actions.act_window",
res_model: "res.partner", res_model: "res.partner",
@ -209,6 +241,7 @@ export class CommunityDashboard extends Component {
} }
openNewStudent() { openNewStudent() {
this.syncMenu("community_school.menu_school_root");
this.actionService.doAction({ this.actionService.doAction({
type: "ir.actions.act_window", type: "ir.actions.act_window",
res_model: "community.school.student", res_model: "community.school.student",

View File

@ -10,6 +10,7 @@ export class TncscGlobalSidebar extends Component {
setup() { setup() {
this.actionService = useService("action"); this.actionService = useService("action");
this.menuService = useService("menu");
this.state = useState({ this.state = useState({
activeTab: "dashboard", activeTab: "dashboard",
collapsed: false, collapsed: false,
@ -31,6 +32,97 @@ export class TncscGlobalSidebar extends Component {
}); });
} }
syncNavbarMenu(action, activeTab) {
if (!this.menuService) {
return;
}
try {
const allMenus = this.menuService.getAll();
if (!allMenus || !allMenus.length) {
return;
}
let targetMenu = null;
const actionId = action?.id;
// 1. Try finding menu by actionID
if (actionId) {
targetMenu = allMenus.find((m) => m.actionID === actionId);
}
// 2. Try finding menu by action.xml_id or action.path
if (!targetMenu && action?.xml_id) {
targetMenu = allMenus.find((m) => m.xmlid === action.xml_id);
}
// 3. Try finding dashboard
if (!targetMenu && (action?.tag === "tncsc_community_dashboard" || action?.path === "community-dashboard" || activeTab === "dashboard")) {
targetMenu = allMenus.find((m) => m.xmlid === "tncsc_deployment.menu_community_dashboard_root");
}
// 4. Try finding menu by activeTab root mapping
if (!targetMenu && activeTab) {
const tabXmlMap = {
dashboard: "tncsc_deployment.menu_community_dashboard_root",
members: "community_membership.menu_membership_root",
events: "event.event_main_menu",
cultural: "event.event_main_menu",
school: "community_school.menu_school_root",
benefits: "community_benefits.menu_benefits_root",
classifieds: "community_classifieds.menu_classifieds_root",
reports: "event.event_main_menu",
settings: "base.menu_administration",
};
const xmlId = tabXmlMap[activeTab];
if (xmlId) {
targetMenu = allMenus.find((m) => m.xmlid === xmlId);
}
}
// 5. Fallback by name
if (!targetMenu && activeTab) {
const tabNames = {
dashboard: ["Community Dashboard"],
members: ["Membership"],
events: ["Events"],
cultural: ["Events"],
school: ["School", "Tamil School"],
benefits: ["Benefits", "Classifieds"],
reports: ["Events"],
settings: ["Settings"],
};
const names = tabNames[activeTab] || [];
targetMenu = allMenus.find((m) => names.includes(m.name) && (m.appID === m.id || !m.parent_id));
}
if (targetMenu) {
const targetAppId = targetMenu.appID || targetMenu.id;
const currentApp = this.menuService.getCurrentApp();
if (targetAppId && (!currentApp || currentApp.id !== targetAppId)) {
this.menuService.setCurrentMenu(targetAppId);
}
}
} catch (err) {
console.warn("Failed to sync navbar menu:", err);
}
}
syncNavbarMenuByXmlId(xmlId) {
if (!this.menuService || !xmlId) return;
try {
const menu = this.menuService.getAll().find((m) => m.xmlid === xmlId);
if (menu) {
const targetAppId = menu.appID || menu.id;
const currentApp = this.menuService.getCurrentApp();
if (targetAppId && (!currentApp || currentApp.id !== targetAppId)) {
this.menuService.setCurrentMenu(targetAppId);
}
}
} catch (err) {
console.warn("Failed to sync navbar menu by xmlId:", err);
}
}
updateActiveTab() { updateActiveTab() {
try { try {
const currentController = this.actionService.currentController; const currentController = this.actionService.currentController;
@ -96,49 +188,56 @@ export class TncscGlobalSidebar extends Component {
) { ) {
this.state.activeTab = "settings"; this.state.activeTab = "settings";
} }
this.syncNavbarMenu(action, this.state.activeTab);
} catch { } catch {
// fallback // fallback
} }
} }
async navigateTo(tab, actionXmlId, menuXmlId) {
this.state.activeTab = tab;
if (menuXmlId) {
this.syncNavbarMenuByXmlId(menuXmlId);
}
await this.actionService.doAction(actionXmlId, {
clearBreadcrumbs: true,
onActionReady: () => {
this.updateActiveTab();
},
});
}
openDashboard() { openDashboard() {
this.state.activeTab = "dashboard"; this.navigateTo("dashboard", "tncsc_deployment.action_community_dashboard", "tncsc_deployment.menu_community_dashboard_root");
this.actionService.doAction("tncsc_deployment.action_community_dashboard", { clearBreadcrumbs: true });
} }
openMembers() { openMembers() {
this.state.activeTab = "members"; this.navigateTo("members", "community_membership.action_membership_members", "community_membership.menu_membership_root");
this.actionService.doAction("community_membership.action_membership_members", { clearBreadcrumbs: true });
} }
openEvents() { openEvents() {
this.state.activeTab = "events"; this.navigateTo("events", "event.action_event_view", "event.event_main_menu");
this.actionService.doAction("event.action_event_view", { clearBreadcrumbs: true });
} }
openSchool() { openSchool() {
this.state.activeTab = "school"; this.navigateTo("school", "community_school.action_school_student", "community_school.menu_school_root");
this.actionService.doAction("community_school.action_school_student", { clearBreadcrumbs: true });
} }
openBenefits() { openBenefits() {
this.state.activeTab = "benefits"; this.navigateTo("benefits", "community_benefits.action_benefit", "community_benefits.menu_benefits_root");
this.actionService.doAction("community_benefits.action_benefit", { clearBreadcrumbs: true });
} }
openCulturalPrograms() { openCulturalPrograms() {
this.state.activeTab = "cultural"; this.navigateTo("cultural", "event.action_event_view", "event.event_main_menu");
this.actionService.doAction("event.action_event_view", { clearBreadcrumbs: true });
} }
openReports() { openReports() {
this.state.activeTab = "reports"; this.navigateTo("reports", "event.event_registration_action", "event.event_main_menu");
this.actionService.doAction("event.event_registration_action", { clearBreadcrumbs: true });
} }
openSettings() { openSettings() {
this.state.activeTab = "settings"; this.navigateTo("settings", "base.action_res_users", "base.menu_administration");
this.actionService.doAction("base.action_res_users", { clearBreadcrumbs: true });
} }
toggleCollapse() { toggleCollapse() {

View File

@ -104,11 +104,17 @@
justify-content: center !important; justify-content: center !important;
.o_chariot_img { .o_chariot_img {
width: 135px !important; width: 130px !important;
max-width: 100% !important;
height: auto !important; height: auto !important;
filter: drop-shadow(0 4px 14px rgba(216, 156, 67, 0.25)) !important; filter: drop-shadow(0 4px 12px rgba(148, 96, 21, 0.22)) !important;
mix-blend-mode: multiply !important; opacity: 0.95 !important;
opacity: 0.85 !important; transition: transform 0.3s ease, filter 0.3s ease !important;
&:hover {
transform: scale(1.04) !important;
filter: drop-shadow(0 6px 18px rgba(216, 156, 67, 0.35)) !important;
}
} }
} }
@ -176,13 +182,16 @@
border-bottom: 1px solid rgba(216, 156, 67, 0.22) !important; border-bottom: 1px solid rgba(216, 156, 67, 0.22) !important;
} }
// When on the community dashboard itself: // When on the community dashboard itself, keep the top main navbar visible with the apps grid icon:
body:has(.o_tncsc_dashboard_root) { body:has(.o_tncsc_dashboard_root) {
.o_main_navbar { .o_main_navbar {
display: none !important; display: flex !important;
background: #FFFCF7 !important;
border-bottom: 1px solid rgba(216, 156, 67, 0.22) !important;
} }
.o_action_manager { .o_action_manager {
height: 100vh !important; height: calc(100vh - var(--NavBar-height, 46px)) !important;
max-height: calc(100vh - var(--NavBar-height, 46px)) !important;
overflow: hidden !important; overflow: hidden !important;
padding: 0 !important; padding: 0 !important;
margin: 0 !important; margin: 0 !important;
@ -193,7 +202,8 @@ body:has(.o_tncsc_dashboard_root) {
margin: 0 !important; margin: 0 !important;
overflow: hidden !important; overflow: hidden !important;
background: #F7F5F0 !important; background: #F7F5F0 !important;
height: 100vh !important; height: 100% !important;
max-height: calc(100vh - var(--NavBar-height, 46px)) !important;
} }
.o_control_panel { .o_control_panel {
display: none !important; display: none !important;
@ -202,8 +212,8 @@ body:has(.o_tncsc_dashboard_root) {
.o_tncsc_dashboard_root { .o_tncsc_dashboard_root {
width: 100% !important; width: 100% !important;
height: 100vh !important; height: calc(100vh - var(--NavBar-height, 46px)) !important;
max-height: 100vh !important; max-height: calc(100vh - var(--NavBar-height, 46px)) !important;
background: #F7F5F0 !important; background: #F7F5F0 !important;
color: #243247 !important; color: #243247 !important;
color-scheme: light; color-scheme: light;
@ -222,7 +232,7 @@ body:has(.o_tncsc_dashboard_root) {
background: #F7F5F0 !important; background: #F7F5F0 !important;
overflow-y: auto !important; overflow-y: auto !important;
height: 100% !important; height: 100% !important;
min-height: 100vh !important; min-height: 0 !important;
width: 100% !important; width: 100% !important;
// Custom scrollbar // Custom scrollbar

View File

@ -245,240 +245,286 @@ body:has(.o_event_kanban_view) {
// ============================================================================ // ============================================================================
// 6. Tamil Cultural Event Card // 6. Tamil Cultural Event Card
// ============================================================================ // ============================================================================
// ============================================================================
// 6. Tamil Cultural Event Card (Upgraded with Prominent Cover Banner)
// ============================================================================
.o_event_kanban_view .o_kanban_record { .o_event_kanban_view .o_kanban_record {
background: #FFFFFF !important; background: #FFFFFF !important;
border: 1px solid rgba(216, 156, 67, 0.2) !important; border: 1px solid rgba(216, 156, 67, 0.22) !important;
border-radius: 16px !important; border-radius: 16px !important;
box-shadow: 0 4px 18px rgba(36, 50, 71, 0.08) !important; box-shadow: 0 4px 18px rgba(36, 50, 71, 0.08) !important;
overflow: hidden !important; overflow: hidden !important;
padding: 18px 20px 22px 20px !important; padding: 0 !important; // Edge-to-edge cover banner!
display: flex !important; display: flex !important;
flex-direction: column !important; flex-direction: column !important;
min-height: 520px !important; min-height: 420px !important; // Balanced height, eliminates huge white void
width: 100% !important; width: 100% !important;
position: relative !important; position: relative !important;
color: #243247 !important; color: #243247 !important;
transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease !important; transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.25s ease, box-shadow 0.25s ease !important;
cursor: pointer !important;
&:hover { &:hover {
transform: translateY(-4px) !important; transform: translateY(-5px) !important;
border-color: $tncsc-gold !important; border-color: $tncsc-gold !important;
box-shadow: 0 12px 28px rgba(36, 50, 71, 0.12), 0 0 16px rgba(216, 156, 67, 0.2) !important; box-shadow: 0 14px 32px rgba(36, 50, 71, 0.12), 0 0 20px rgba(216, 156, 67, 0.22) !important;
.o_tncsc_card_cover_img {
transform: scale(1.06) !important;
} }
// Bottom-Right Corner Artwork (Hidden in White Mode to avoid dark image box) .o_tncsc_card_title {
.o_tncsc_mandala_corner { color: #946015 !important;
display: none !important; }
} }
// Top Header: Stage Pill Badge + Three Dots Button // 1. Cover Image Banner
.o_tncsc_card_top { .o_tncsc_card_banner {
margin-bottom: 14px !important;
position: relative !important; position: relative !important;
z-index: 2 !important; width: 100% !important;
height: 180px !important; // Large, striking banner image!
overflow: hidden !important;
background: #0A1938 !important;
.o_tncsc_card_cover_img {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1) !important;
display: block !important;
}
.o_tncsc_banner_overlay {
position: absolute !important;
inset: 0 !important;
background: linear-gradient(180deg, rgba(0,0,0,0.45) 0%, rgba(0,0,0,0.05) 50%, rgba(10,25,56,0.65) 100%) !important;
pointer-events: none !important;
}
// Top Floating Row: Stage Pill on Left, Status Pill on Right
.o_tncsc_banner_top_row {
position: absolute !important;
top: 12px !important;
left: 12px !important;
right: 12px !important;
z-index: 3 !important;
.o_tncsc_stage_pill { .o_tncsc_stage_pill {
font-size: 11.5px !important; font-size: 11px !important;
font-weight: 600 !important; font-weight: 700 !important;
padding: 4px 14px !important; padding: 4px 12px !important;
border-radius: 9999px !important; border-radius: 9999px !important;
display: inline-flex !important; display: inline-flex !important;
align-items: center !important; align-items: center !important;
gap: 6px !important; gap: 5px !important;
text-transform: capitalize !important; text-transform: capitalize !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.25) !important;
backdrop-filter: blur(6px) !important;
-webkit-backdrop-filter: blur(6px) !important;
.o_dot { .o_dot {
font-size: 8px; font-size: 8px;
} }
&.pill-new { &.pill-new {
background: #FEF3C7 !important; background: rgba(254, 243, 199, 0.94) !important;
border: 1px solid #FDE68A !important; border: 1px solid #FDE68A !important;
color: #92400E !important; color: #92400E !important;
font-weight: 700 !important; .o_dot { color: #D97706 !important; }
.o_dot { color: $tncsc-gold !important; }
} }
&.pill-booked { &.pill-booked {
background: #E0F2FE !important; background: rgba(224, 242, 254, 0.94) !important;
border: 1px solid #BAE6FD !important; border: 1px solid #BAE6FD !important;
color: #0369A1 !important; color: #0369A1 !important;
.o_dot { color: #0284C7; } .o_dot { color: #0284C7 !important; }
} }
&.pill-announced { &.pill-announced {
background: #DCFCE7 !important; background: rgba(220, 252, 231, 0.94) !important;
border: 1px solid #BBF7D0 !important; border: 1px solid #BBF7D0 !important;
color: #15803D !important; color: #15803D !important;
.o_dot { color: #16A34A; } .o_dot { color: #16A34A !important; }
} }
&.pill-ended { &.pill-ended {
background: #F3E8FF !important; background: rgba(243, 232, 255, 0.94) !important;
border: 1px solid #E9D5FF !important; border: 1px solid #E9D5FF !important;
color: #7E22CE !important; color: #7E22CE !important;
.o_dot { color: #9333EA; } .o_dot { color: #9333EA !important; }
} }
} }
.o_tncsc_card_dots { .o_tncsc_kanban_status_pill {
background: #F8FAFC !important; font-size: 10.5px !important;
border: 1px solid #E2E8F0 !important;
border-radius: 9999px !important;
padding: 2px 10px !important;
color: #64748B !important;
font-size: 13px !important;
letter-spacing: 1.5px !important;
cursor: pointer !important;
line-height: 1 !important;
&:hover {
background: #EDF2F7 !important;
color: #120735 !important;
}
}
}
// Date Box Row (for New stage)
.o_tncsc_card_date_box_row {
margin-bottom: 12px !important;
position: relative !important;
z-index: 2 !important;
.o_tncsc_card_date_box {
border: 1px solid #946015 !important;
border-radius: 8px !important;
padding: 6px 14px !important;
text-align: center !important;
background: #FFF0D6 !important;
min-width: 68px !important;
.o_day {
font-size: 24px !important;
font-weight: 800 !important;
color: #243247 !important;
line-height: 1 !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
}
.o_month {
font-size: 8.5px !important;
font-weight: 700 !important; font-weight: 700 !important;
color: $tncsc-gold !important; padding: 4px 11px !important;
letter-spacing: 1px !important;
text-transform: uppercase !important;
margin-top: 3px !important;
}
}
.o_tncsc_card_time_col {
color: #475569 !important;
font-size: 12px !important;
font-weight: 500 !important;
line-height: 1.4 !important;
}
}
// Central Circular Gold Outline Icon
.o_tncsc_card_circle_icon {
width: 52px !important;
height: 52px !important;
min-width: 52px !important;
border: 1.5px solid $tncsc-gold !important;
border-radius: 50% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 16px auto 16px auto !important;
background: #FFF0D6 !important;
color: $tncsc-gold !important;
position: relative !important;
z-index: 2 !important;
}
// Event Title
.o_tncsc_card_title {
font-family: $tncsc-font-serif !important;
font-size: 18.5px !important;
font-weight: 700 !important;
color: #243247 !important;
line-height: 1.35 !important;
margin-bottom: 8px !important;
position: relative !important;
z-index: 2 !important;
}
// Subtitle / Tagline
.o_tncsc_card_subtitle {
font-size: 12.5px !important;
color: #596579 !important;
margin-bottom: 12px !important;
line-height: 1.4 !important;
position: relative !important;
z-index: 2 !important;
}
// Location
.o_tncsc_card_location {
font-size: 12.5px !important;
color: #475569 !important;
font-weight: 500 !important;
margin-bottom: 8px !important;
position: relative !important;
z-index: 2 !important;
}
// Date & Time (for Cards 2, 3, 4)
.o_tncsc_card_datetime {
font-size: 12.5px !important;
color: #475569 !important;
font-weight: 500 !important;
margin-bottom: 16px !important;
position: relative !important;
z-index: 2 !important;
}
// Action Button at Bottom
.o_tncsc_card_action {
margin-top: auto !important;
padding-top: 16px !important;
position: relative !important;
z-index: 2 !important;
.o_tncsc_btn_details {
width: fit-content !important;
font-size: 13px !important;
padding: 8px 26px !important;
border-radius: 9999px !important; border-radius: 9999px !important;
display: inline-flex !important; display: inline-flex !important;
align-items: center !important; align-items: center !important;
gap: 6px !important; gap: 5px !important;
transition: all 0.2s ease !important; text-transform: uppercase !important;
letter-spacing: 0.4px !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.25) !important;
&.btn-primary-gold { &.status-open {
background: rgba(16, 185, 129, 0.95) !important;
color: #ffffff !important;
border: 1px solid rgba(255, 255, 255, 0.35) !important;
}
&.status-closed {
background: rgba(220, 38, 38, 0.92) !important;
color: #ffffff !important;
border: 1px solid rgba(255, 255, 255, 0.3) !important;
}
&.status-soldout {
background: rgba(217, 119, 6, 0.94) !important;
color: #ffffff !important;
border: 1px solid rgba(255, 255, 255, 0.3) !important;
}
.status-pulse-dot {
width: 6.5px;
height: 6.5px;
border-radius: 50%;
background-color: #ffffff;
display: inline-block;
}
}
}
// Floating Bottom-Left Date Badge
.o_tncsc_banner_date_badge {
position: absolute !important;
bottom: 10px !important;
left: 12px !important;
background: #ffffff !important;
border-radius: 8px !important;
padding: 4px 9px !important;
text-align: center !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.22) !important;
border: 1px solid rgba(216, 156, 67, 0.35) !important;
line-height: 1 !important;
z-index: 3 !important;
.date-month {
font-size: 8.5px !important;
font-weight: 800 !important;
color: #946015 !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
display: block !important;
margin-bottom: 2px !important;
}
.date-day {
font-size: 16px !important;
font-weight: 800 !important;
color: #0A1938 !important;
display: block !important;
}
}
// Floating Bottom-Right Time Pill
.o_tncsc_banner_time_pill {
position: absolute !important;
bottom: 10px !important;
right: 12px !important;
background: rgba(10, 25, 56, 0.8) !important;
color: #ffffff !important;
border: 1px solid rgba(255, 255, 255, 0.25) !important;
border-radius: 6px !important;
padding: 3px 8px !important;
font-size: 11px !important;
font-weight: 600 !important;
backdrop-filter: blur(6px) !important;
-webkit-backdrop-filter: blur(6px) !important;
display: inline-flex !important;
align-items: center !important;
z-index: 3 !important;
}
}
// 2. Card Body Content
.o_tncsc_card_body {
padding: 16px 18px 16px 18px !important;
display: flex !important;
flex-direction: column !important;
flex-grow: 1 !important;
position: relative !important;
z-index: 2 !important;
.o_tncsc_card_title {
font-family: $tncsc-font-serif !important;
font-size: 17px !important;
font-weight: 700 !important;
color: #0A1938 !important;
line-height: 1.35 !important;
margin-bottom: 4px !important;
display: -webkit-box !important;
-webkit-line-clamp: 2 !important;
-webkit-box-orient: vertical !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
min-height: 44px !important;
transition: color 0.2s ease !important;
}
.o_tncsc_card_subtitle {
font-size: 12px !important;
color: #596579 !important;
margin-bottom: 10px !important;
line-height: 1.4 !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
&.text-muted-sub {
color: #8A97A8 !important;
font-style: italic !important;
}
}
.o_tncsc_card_meta {
font-size: 12.5px !important;
color: #475569 !important;
.o_tncsc_meta_row {
display: flex !important;
align-items: center !important;
gap: 7px !important;
line-height: 1.4 !important;
svg {
flex-shrink: 0 !important;
}
}
}
.o_tncsc_card_footer {
margin-top: auto !important;
padding-top: 12px !important;
border-top: 1px solid #F1F5F9 !important;
.o_tncsc_btn_details {
font-size: 12px !important;
font-weight: 700 !important;
padding: 6px 18px !important;
border-radius: 9999px !important;
background: linear-gradient(135deg, #F59E0B, #B45309) !important; background: linear-gradient(135deg, #F59E0B, #B45309) !important;
color: #000000 !important; color: #000000 !important;
font-weight: 700 !important;
border: none !important; border: none !important;
box-shadow: 0 4px 14px rgba(245, 158, 11, 0.35) !important; box-shadow: 0 2px 10px rgba(245, 158, 11, 0.3) !important;
transition: all 0.2s ease !important;
display: inline-flex !important;
align-items: center !important;
gap: 5px !important;
&:hover { &:hover {
transform: translateY(-1px); transform: translateY(-1px) !important;
box-shadow: 0 6px 18px rgba(245, 158, 11, 0.5) !important; box-shadow: 0 4px 14px rgba(245, 158, 11, 0.45) !important;
}
}
&.btn-outline-gold {
background: rgba(36, 50, 71, 0.05) !important;
border: 1.5px solid $tncsc-gold !important;
color: #243247 !important;
font-weight: 600 !important;
&:hover {
background: rgba(216, 156, 67, 0.15) !important;
border-color: $tncsc-gold-hover !important;
color: $tncsc-gold-hover !important;
transform: translateY(-1px);
} }
} }
} }

View File

@ -52,7 +52,7 @@
<!-- Sidebar Bottom: Chariot Art & Motto --> <!-- Sidebar Bottom: Chariot Art & Motto -->
<div class="o_sidebar_bottom text-center"> <div class="o_sidebar_bottom text-center">
<div class="o_chariot_img_wrap"> <div class="o_chariot_img_wrap">
<img src="/tncsc_deployment/static/src/img/dashboard/gold_chariot_art.jpg" alt="Tamil Chariot" class="o_chariot_img"/> <img src="/tncsc_deployment/static/src/img/dashboard/gold_chariot_art.png" alt="Tamil Chariot" class="o_chariot_img"/>
</div> </div>
<div class="o_motto_wrap"> <div class="o_motto_wrap">
<div class="o_motto_line">Our Culture</div> <div class="o_motto_line">Our Culture</div>

View File

@ -12,126 +12,165 @@
<attribute name="default_group_by">stage_id</attribute> <attribute name="default_group_by">stage_id</attribute>
</xpath> </xpath>
<!-- Add subtitle field for card rendering --> <!-- Add subtitle, image, and status fields for card rendering -->
<xpath expr="//field[@name='date_end']" position="after"> <xpath expr="//field[@name='date_end']" position="after">
<field name="subtitle"/> <field name="subtitle"/>
<field name="image_512"/>
<field name="image_128"/>
<field name="registration_status"/>
<field name="is_registration_open"/>
<field name="seats_taken"/>
<field name="is_done"/>
</xpath> </xpath>
<!-- Remove default aside date bar --> <!-- Remove default aside date bar -->
<xpath expr="//aside" position="replace"/> <xpath expr="//aside" position="replace"/>
<!-- Replace standard main content with Tamil Luxury Dark Theme Card while keeping <main> tag --> <!-- Replace standard main content with Upgraded Luxury Event Card -->
<xpath expr="//main" position="replace"> <xpath expr="//main" position="replace">
<main class="p-0 o_tncsc_card w-100"> <main class="p-0 o_tncsc_card w-100 d-flex flex-column h-100">
<!-- Bottom-right subtle mandala corner artwork --> <!-- 1. Top Cover Image Banner (Large, edge-to-edge) -->
<div class="o_tncsc_mandala_corner"/> <div class="o_tncsc_card_banner position-relative overflow-hidden">
<!-- Event Image with fallbacks -->
<t t-if="record.image_512.raw_value or record.image_128.raw_value">
<img t-att-src="'/web/image/event.event/' + record.id.raw_value + '/image_512'"
alt="Event Image"
class="o_tncsc_card_cover_img"/>
</t>
<t t-elif="'picnic' in (record.name.value or '').toLowerCase()">
<img src="/tncsc_deployment/static/src/img/events/summer-picnic.jpg"
alt="Event Image"
class="o_tncsc_card_cover_img"/>
</t>
<t t-elif="'pongal' in (record.name.value or '').toLowerCase()">
<img src="/tncsc_deployment/static/src/img/dashboard/pongal_featured_banner.jpg"
alt="Event Image"
class="o_tncsc_card_cover_img"/>
</t>
<t t-elif="'new year' in (record.name.value or '').toLowerCase()">
<img src="/tncsc_deployment/static/src/img/hero/slide-1.webp"
alt="Event Image"
class="o_tncsc_card_cover_img"/>
</t>
<t t-else="">
<img src="/tncsc_deployment/static/src/img/events/event-banner.webp"
alt="Event Image"
class="o_tncsc_card_cover_img"/>
</t>
<!-- Top Header Row: Stage Pill Badge on left, Three Dots on right --> <!-- Gradient Overlay for badge contrast -->
<div class="o_tncsc_card_top d-flex align-items-center justify-content-between"> <div class="o_tncsc_banner_overlay"/>
<!-- Floating Top Row: Stage Pill on left, Status Pill on right -->
<div class="o_tncsc_banner_top_row d-flex align-items-center justify-content-between">
<span t-attf-class="o_tncsc_stage_pill pill-{{ (record.stage_id.value or '').toLowerCase() }}"> <span t-attf-class="o_tncsc_stage_pill pill-{{ (record.stage_id.value or '').toLowerCase() }}">
<span class="o_dot">&#9679;</span> <span class="o_dot">&#9679;</span>
<t t-esc="record.stage_id.value"/> <t t-esc="record.stage_id.value"/>
</span> </span>
<div class="o_tncsc_card_dots">
<span>&#8226;&#8226;&#8226;</span> <!-- Floating Status Badge (Open / Closed) -->
</div> <t t-if="record.registration_status.raw_value == 'open' or (record.stage_id.value != 'Ended' and !record.is_done.raw_value)">
<span class="o_tncsc_kanban_status_pill status-open">
<span class="status-pulse-dot"></span> Open
</span>
</t>
<t t-elif="record.registration_status.raw_value == 'sold_out'">
<span class="o_tncsc_kanban_status_pill status-soldout">
<i class="fa fa-ticket"></i> Sold Out
</span>
</t>
<t t-else="">
<span class="o_tncsc_kanban_status_pill status-closed">
<i class="fa fa-lock"></i> Closed
</span>
</t>
</div> </div>
<!-- Date Box row (Only for Summer Picnic / New stage) --> <!-- Floating Bottom-Left Date Badge -->
<t t-if="record.stage_id.value === 'New'"> <div t-if="record.date_begin.raw_value" class="o_tncsc_banner_date_badge">
<div class="o_tncsc_card_date_box_row d-flex align-items-center gap-3"> <span class="date-month"><t t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('LLL')"/></span>
<div class="o_tncsc_card_date_box"> <span class="date-day"><t t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('d')"/></span>
<div class="o_day"><t t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('d')"/></div>
<div class="o_month"><t t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('MMM yyyy')"/></div>
</div> </div>
<div class="d-flex flex-column gap-1 o_tncsc_card_time_col">
<div class="o_time d-flex align-items-center"> <!-- Floating Bottom-Right Time Pill -->
<svg class="o_tncsc_clock_svg me-1" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#D4A359" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <div t-if="record.date_begin.raw_value" class="o_tncsc_banner_time_pill">
<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-1">
<circle cx="12" cy="12" r="10"/> <circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/> <polyline points="12 6 12 12 16 14"/>
</svg> </svg>
<t t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('t')"/> <t t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('t')"/>
</div> </div>
<div t-if="record.date_begin.raw_value !== record.date_end.raw_value" class="o_end_date d-flex align-items-center">
<svg class="o_tncsc_cal_svg me-1" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#D4A359" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
<t t-esc="luxon.DateTime.fromISO(record.date_end.raw_value).toFormat('d MMM')"/>
</div>
</div>
</div>
</t>
<!-- Central Circular Gold Icon -->
<div class="o_tncsc_card_circle_icon">
<!-- Calendar icon for New and Booked -->
<svg t-if="record.stage_id.value === 'New' or record.stage_id.value === 'Booked'" viewBox="0 0 24 24" width="24" height="24" stroke="#D4A359" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
<!-- Megaphone icon for Announced -->
<svg t-elif="record.stage_id.value === 'Announced'" viewBox="0 0 24 24" width="24" height="24" stroke="#D4A359" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 11l18-5v12L3 13v-2z"/>
<path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/>
</svg>
<!-- Checkmark circle icon for Ended -->
<svg t-elif="record.stage_id.value === 'Ended'" viewBox="0 0 24 24" width="24" height="24" stroke="#D4A359" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"/>
<polyline points="8 12 11 15 16 9"/>
</svg>
<!-- Default fallback -->
<svg t-else="" viewBox="0 0 24 24" width="24" height="24" stroke="#D4A359" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</div> </div>
<!-- 2. Card Body Content -->
<div class="o_tncsc_card_body d-flex flex-column flex-grow-1">
<!-- Event Title --> <!-- Event Title -->
<div class="o_tncsc_card_title text-center"> <div class="o_tncsc_card_title" t-att-title="record.name.value">
<field name="name"/> <field name="name"/>
</div> </div>
<!-- Subtitle / Tagline --> <!-- Subtitle / Tagline -->
<div t-if="record.subtitle.value" class="o_tncsc_card_subtitle text-center"> <div t-if="record.subtitle.value" class="o_tncsc_card_subtitle">
<field name="subtitle"/> <field name="subtitle"/>
</div> </div>
<div t-else="" class="o_tncsc_card_subtitle text-muted-sub">
Tamil Nadu Cultural Society of Canada
</div>
<!-- Meta Info List -->
<div class="o_tncsc_card_meta d-flex flex-column gap-1 mb-2">
<!-- Location --> <!-- Location -->
<div class="o_tncsc_card_location d-flex align-items-center justify-content-center gap-1"> <div class="o_tncsc_meta_row d-flex align-items-center gap-2">
<svg class="o_tncsc_pin_svg me-1" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#D4A359" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="o_tncsc_meta_svg text-gold" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#D4A359" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/> <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
<circle cx="12" cy="10" r="3"/> <circle cx="12" cy="10" r="3"/>
</svg> </svg>
<span class="text-truncate">
<field t-if="record.address_id.value" name="address_id"/> <field t-if="record.address_id.value" name="address_id"/>
<span t-else="">Online Event</span> <span t-else="">Ontario, Canada</span>
</span>
</div> </div>
<!-- Date & Time (for Booked, Announced, Ended) --> <!-- Full Date -->
<div t-if="record.stage_id.value !== 'New'" class="o_tncsc_card_datetime d-flex align-items-center justify-content-center gap-1"> <div t-if="record.date_begin.raw_value" class="o_tncsc_meta_row d-flex align-items-center gap-2">
<svg class="o_tncsc_cal_svg me-1" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#D4A359" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="o_tncsc_meta_svg text-gold" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#D4A359" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/> <rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/> <line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/> <line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/> <line x1="3" y1="10" x2="21" y2="10"/>
</svg> </svg>
<span> <span class="text-truncate">
<t t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('d MMM yyyy')"/> &#8226; <t t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('t')"/> <t t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('cccc, d LLL yyyy')"/>
</span> </span>
</div> </div>
<!-- Action Button: View Details --> <!-- Attendees count if available -->
<div class="o_tncsc_card_action text-center"> <div t-if="record.seats_taken.raw_value" class="o_tncsc_meta_row d-flex align-items-center gap-2">
<button t-attf-class="o_tncsc_btn_details {{ record.stage_id.value === 'New' ? 'btn-primary-gold' : 'btn-outline-gold' }}"> <svg class="o_tncsc_meta_svg text-gold" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#D4A359" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span><field name="seats_taken"/> Registered Attendees</span>
</div>
</div>
<!-- 3. Action Footer -->
<div class="o_tncsc_card_footer mt-auto pt-2 d-flex align-items-center justify-content-between">
<span class="o_tncsc_stage_label text-muted small">
<t t-if="record.registration_status.raw_value == 'open' or (record.stage_id.value != 'Ended' and !record.is_done.raw_value)">
<span class="text-success fw-semibold"><i class="fa fa-check-circle me-1"></i>Active</span>
</t>
<t t-else="">
<span class="text-muted"><i class="fa fa-flag-checkered me-1"></i>Concluded</span>
</t>
</span>
<button class="o_tncsc_btn_details btn-primary-gold">
View Details &#8594; View Details &#8594;
</button> </button>
</div> </div>
</div>
</main> </main>
</xpath> </xpath>
</field> </field>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Event Form View: Add Event Image Upload Option -->
<record id="view_event_form_inherit_image" model="ir.ui.view">
<field name="name">event.event.form.inherit.image</field>
<field name="model">event.event</field>
<field name="inherit_id" ref="event.view_event_form"/>
<field name="priority">20</field>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_title')]" position="before">
<field name="image_1920" widget="image" class="oe_avatar" options="{'preview_image': 'image_128', 'zoom': True}"/>
</xpath>
</field>
</record>
<!-- Event List View: Add Event Image Thumbnail -->
<record id="view_event_tree_inherit_image" model="ir.ui.view">
<field name="name">event.event.list.inherit.image</field>
<field name="model">event.event</field>
<field name="inherit_id" ref="event.view_event_tree"/>
<field name="priority">20</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="before">
<field name="image_128" widget="image" string="Image" options="{'size': [32, 32], 'img_class': 'rounded object-fit-cover'}" optional="show"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -930,6 +930,31 @@
<t t-else="">Events</t> <t t-else="">Events</t>
</h1> </h1>
<p t-if="event and event.subtitle" class="lead text-white-50 mb-3" style="font-size: 1.25rem;" t-out="event.subtitle"/> <p t-if="event and event.subtitle" class="lead text-white-50 mb-3" style="font-size: 1.25rem;" t-out="event.subtitle"/>
<t t-if="event">
<t t-set="ev_det_status" t-value="event.registration_status or ('open' if (event.event_registrations_open and not event.is_done) else 'closed')"/>
<div class="mb-3">
<t t-if="ev_det_status == 'open'">
<span class="badge px-3 py-2 rounded-pill fs-6 fw-semibold" style="background-color: #10B981; color: #ffffff; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4); display: inline-flex; align-items: center; gap: 6px;">
<span style="width: 8px; height: 8px; border-radius: 50%; background-color: #ffffff; display: inline-block;"></span> Open for Registration
</span>
</t>
<t t-elif="ev_det_status == 'sold_out'">
<span class="badge px-3 py-2 rounded-pill fs-6 fw-semibold" style="background-color: #D97706; color: #ffffff; box-shadow: 0 4px 12px rgba(217, 119, 6, 0.4); display: inline-flex; align-items: center; gap: 6px;">
<i class="fa fa-ticket"></i> Sold Out
</span>
</t>
<t t-elif="ev_det_status == 'ongoing'">
<span class="badge px-3 py-2 rounded-pill fs-6 fw-semibold" style="background-color: #EF4444; color: #ffffff; box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4); display: inline-flex; align-items: center; gap: 6px;">
<span style="width: 8px; height: 8px; border-radius: 50%; background-color: #ffffff; display: inline-block;"></span> Live Now
</span>
</t>
<t t-else="">
<span class="badge px-3 py-2 rounded-pill fs-6 fw-semibold" style="background-color: #DC2626; color: #ffffff; box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4); display: inline-flex; align-items: center; gap: 6px;">
<i class="fa fa-lock"></i> Registrations Closed
</span>
</t>
</div>
</t>
<div class="fs-5 text-white-50 fw-medium"> <div class="fs-5 text-white-50 fw-medium">
<a href="/" class="text-white-50 text-decoration-none">Home</a> <a href="/" class="text-white-50 text-decoration-none">Home</a>
<span class="mx-2 text-white-50">|</span> <span class="mx-2 text-white-50">|</span>

View File

@ -4944,23 +4944,338 @@
text-align: center; text-align: center;
position: relative; position: relative;
} }
.event-card { /* TNCSC Event Filter Bar */
background-color: #ffffff; .tncsc-event-filter-bar {
border-radius: 16px; display: flex;
padding: 24px; align-items: center;
max-width: 320px; justify-content: center;
transition: transform 0.25s ease, box-shadow 0.25s ease; gap: 12px;
flex-wrap: wrap;
margin-bottom: 35px;
} }
.event-card:hover { .tncsc-filter-btn {
transform: translateY(-4px); background: #ffffff;
box-shadow: 0 12px 30px rgba(0,0,0,0.08); color: #475569;
border: 1.5px solid #e2e8f0;
padding: 10px 22px;
border-radius: 30px;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
} }
.event-card-img { .tncsc-filter-btn:hover {
width: 100%; border-color: #D89C43;
height: 280px; color: #D89C43;
object-fit: cover; transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(216, 156, 67, 0.15);
}
.tncsc-filter-btn.active {
background: linear-gradient(135deg, #120735 0%, #1e0d5a 100%);
color: #D89C43 !important;
border-color: #D89C43;
box-shadow: 0 6px 18px rgba(18, 7, 53, 0.25);
}
.tncsc-filter-btn .badge-count {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 6px 16px rgba(0,0,0,0.08); background: #f1f5f9;
color: #475569;
transition: all 0.2s;
}
.tncsc-filter-btn.active .badge-count {
background: rgba(216, 156, 67, 0.25);
color: #ffffff;
}
/* Luxury Event Cards */
.tncsc-event-card {
background: #ffffff;
border-radius: 18px;
overflow: hidden;
border: 1px solid rgba(216, 156, 67, 0.18);
box-shadow: 0 10px 28px rgba(10, 25, 56, 0.06);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.3s ease;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}
.tncsc-event-card:hover {
transform: translateY(-7px);
box-shadow: 0 20px 42px rgba(18, 7, 53, 0.12), 0 0 24px rgba(216, 156, 67, 0.18);
border-color: #D89C43;
}
/* Card Media and Image */
.tncsc-card-media {
position: relative;
height: 230px;
overflow: hidden;
background: #0c1533;
}
.tncsc-card-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.tncsc-event-card:hover .tncsc-card-img {
transform: scale(1.06);
}
.tncsc-media-overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0) 40%, rgba(10,25,56,0.65) 100%);
pointer-events: none;
}
/* Status Badges */
.tncsc-status-badge {
position: absolute;
top: 14px;
right: 14px;
padding: 6px 14px;
border-radius: 30px;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.4px;
text-transform: uppercase;
display: inline-flex;
align-items: center;
gap: 6px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
z-index: 3;
box-shadow: 0 4px 12px rgba(0,0,0,0.22);
}
.badge-status-open {
background: rgba(16, 185, 129, 0.94);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.4);
}
.badge-status-closed {
background: rgba(220, 38, 38, 0.92);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.35);
}
.badge-status-soldout {
background: rgba(217, 119, 6, 0.94);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.35);
}
.badge-status-live {
background: rgba(239, 68, 68, 0.96);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.4);
}
@keyframes tncscPulseLive {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.8); }
70% { transform: scale(1); box-shadow: 0 0 0 6px rgba(255, 255, 255, 0); }
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
}
.status-pulse-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #ffffff;
display: inline-block;
animation: tncscPulseLive 1.6s infinite;
}
/* Category Pill */
.tncsc-category-pill {
position: absolute;
top: 14px;
left: 14px;
background: rgba(18, 7, 53, 0.82);
color: #F8FAFC;
border: 1px solid rgba(216, 156, 67, 0.4);
padding: 5px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 3;
display: inline-flex;
align-items: center;
gap: 5px;
}
/* Date Badge */
.tncsc-card-date-badge {
position: absolute;
bottom: 12px;
left: 14px;
background: #ffffff;
border-radius: 10px;
padding: 6px 12px;
text-align: center;
box-shadow: 0 4px 14px rgba(0,0,0,0.18);
border: 1px solid rgba(216, 156, 67, 0.35);
line-height: 1;
z-index: 3;
}
.tncsc-card-date-badge .date-month {
font-size: 0.72rem;
font-weight: 800;
color: #D89C43;
text-transform: uppercase;
letter-spacing: 0.6px;
display: block;
margin-bottom: 2px;
}
.tncsc-card-date-badge .date-day {
font-size: 1.35rem;
font-weight: 800;
color: #0A1938;
display: block;
}
/* Card Body Content */
.tncsc-card-body {
padding: 22px 22px 18px 22px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.tncsc-card-title {
font-family: 'Playfair Display', Georgia, serif;
font-size: 1.25rem;
font-weight: 700;
color: #0A1938;
line-height: 1.35;
margin-bottom: 6px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 50px;
}
.tncsc-card-title a {
color: #0A1938;
text-decoration: none;
transition: color 0.2s ease;
}
.tncsc-card-title a:hover {
color: #D89C43;
}
.tncsc-card-subtitle {
color: #64748B;
font-size: 0.88rem;
font-weight: 500;
margin-bottom: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tncsc-card-meta {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
font-size: 0.88rem;
color: #475569;
}
.tncsc-meta-row {
display: flex;
align-items: center;
gap: 8px;
}
.tncsc-meta-row i {
width: 16px;
text-align: center;
color: #D89C43;
flex-shrink: 0;
}
/* Status Notice Callout */
.tncsc-status-notice {
border-radius: 8px;
padding: 8px 12px;
font-size: 0.82rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
margin-top: auto;
margin-bottom: 16px;
}
.notice-open {
background-color: #f0fdf4;
color: #166534;
border: 1px solid #bbf7d0;
}
.notice-closed {
background-color: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
.notice-soldout {
background-color: #fffbeb;
color: #92400e;
border: 1px solid #fde68a;
}
/* Action CTA Buttons */
.tncsc-card-footer {
padding-top: 14px;
border-top: 1px solid #f1f5f9;
}
.btn-tncsc-event-action {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 11px 18px;
border-radius: 8px;
font-weight: 600;
font-size: 0.92rem;
text-decoration: none !important;
transition: all 0.25s ease;
}
.btn-action-open {
background: linear-gradient(135deg, #D89C43 0%, #C48A35 100%);
color: #ffffff !important;
box-shadow: 0 4px 14px rgba(216, 156, 67, 0.35);
border: 1px solid #D89C43;
}
.btn-action-open:hover {
background: linear-gradient(135deg, #e5a93c 0%, #d89c43 100%);
box-shadow: 0 6px 20px rgba(216, 156, 67, 0.5);
transform: translateY(-2px);
color: #ffffff !important;
}
.btn-action-closed {
background: #f8fafc;
color: #475569 !important;
border: 1px solid #cbd5e1;
}
.btn-action-closed:hover {
background: #f1f5f9;
color: #0f172a !important;
border-color: #94a3b8;
transform: translateY(-2px);
}
.btn-action-soldout {
background: #fffbeb;
color: #b45309 !important;
border: 1px solid #fcd34d;
}
.btn-action-soldout:hover {
background: #fef3c7;
color: #92400e !important;
transform: translateY(-2px);
}
.tncsc-event-col {
transition: opacity 0.3s ease, transform 0.3s ease;
} }
#footer:not(.tncsc-custom-footer), #footer:not(.tncsc-custom-footer),
footer#bottom:not(.tncsc-custom-footer), footer#bottom:not(.tncsc-custom-footer),
@ -5057,7 +5372,7 @@
</div> </div>
</div> </div>
<!-- Hero Main Banner Section with Navigation Bar & Page Title --> <!-- Hero Main Banner Section with Navigation Bar and Page Title -->
<div class="tncsc-hero-container"> <div class="tncsc-hero-container">
<!-- Transparent Floating Navbar --> <!-- Transparent Floating Navbar -->
<div class="tncsc-hero-nav"> <div class="tncsc-hero-nav">
@ -5098,7 +5413,7 @@
</div> </div>
</div> </div>
<!-- Inner Hero Title & Breadcrumb --> <!-- Inner Hero Title and Breadcrumb -->
<div class="tncsc-inner-hero"> <div class="tncsc-inner-hero">
<div class="container"> <div class="container">
<h1 class="display-3 fw-bold text-white mb-3 tncsc-animate tncsc-animate-visible" style="font-size: 3.5rem; text-shadow: 0 4px 12px rgba(0,0,0,0.5);">Events</h1> <h1 class="display-3 fw-bold text-white mb-3 tncsc-animate tncsc-animate-visible" style="font-size: 3.5rem; text-shadow: 0 4px 12px rgba(0,0,0,0.5);">Events</h1>
@ -5112,32 +5427,182 @@
</div> </div>
<!-- Events List Grid Section --> <!-- Events List Grid Section -->
<section class="py-5 my-4" style="background-color: #ffffff;"> <section class="py-5" style="background-color: #FAF8F5;">
<div class="container py-4"> <div class="container py-4">
<t t-set="published_events" t-value="env['event.event'].sudo().search([('website_published', '=', True)], order='date_begin asc')"/> <t t-set="published_events" t-value="env['event.event'].sudo().search([('website_published', '=', True)], order='date_begin desc')"/>
<div class="row g-4">
<!-- Section Header Intro -->
<div class="text-center max-w-700 mx-auto mb-4">
<div class="d-inline-flex align-items-center gap-2 px-3 py-1 rounded-pill mb-2" style="background: rgba(216, 156, 67, 0.12); color: #B4731E; font-size: 0.85rem; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase;">
<i class="fa fa-star"></i> தமிழ்நாடு கலாச்சார சங்கத்தின் நிகழ்வுகள்
</div>
<h2 class="display-6 fw-bold" style="color: #0A1938; font-family: 'Playfair Display', Georgia, serif;">
Upcoming &amp; Heritage Celebrations
</h2>
<p class="text-muted fs-6" style="line-height: 1.65; max-width: 650px; margin: 0 auto;">
Experience authentic Tamil culture, community feasts, traditional games, and festive celebrations across Ontario.
</p>
</div>
<!-- Filter Buttons Bar -->
<div class="tncsc-event-filter-bar" t-if="published_events">
<button type="button" class="tncsc-filter-btn active" data-filter="all">
<i class="fa fa-th-large"></i> All Celebrations
<span class="badge-count" t-out="len(published_events)"/>
</button>
<button type="button" class="tncsc-filter-btn" data-filter="open">
<span class="status-pulse-dot" style="background-color: #10B981; animation: none;"></span> Open for Registration
<span class="badge-count" t-out="len([e for e in published_events if (e.registration_status == 'open' or (e.event_registrations_open and not e.is_done))])"/>
</button>
<button type="button" class="tncsc-filter-btn" data-filter="closed">
<i class="fa fa-history"></i> Past Celebrations
<span class="badge-count" t-out="len([e for e in published_events if (e.registration_status != 'open' and not (e.event_registrations_open and not e.is_done))])"/>
</button>
</div>
<!-- Event Cards Grid -->
<div class="row g-4" id="tncscEventsGrid">
<t t-if="published_events"> <t t-if="published_events">
<t t-foreach="published_events" t-as="ev"> <t t-foreach="published_events" t-as="ev">
<div class="col-12 col-md-6 col-lg-4 d-flex justify-content-start"> <t t-set="ev_status" t-value="ev.registration_status or ('open' if (ev.event_registrations_open and not ev.is_done) else 'closed')"/>
<div class="event-card text-center tncsc-animate w-100"> <div t-attf-class="col-12 col-md-6 col-lg-4 d-flex align-items-stretch tncsc-event-col" t-att-data-status="ev_status == 'open' and 'open' or 'closed'">
<div class="mb-4"> <div class="tncsc-event-card w-100 tncsc-animate">
<img src="/tncsc_deployment/static/src/img/events/summer-picnic.jpg" class="event-card-img" t-att-alt="ev.name"/>
</div> <!-- Card Media / Banner Image -->
<h4 class="fw-bold mb-2" style="color: #0A1938; font-size: 1.25rem; line-height: 1.35;"> <div class="tncsc-card-media">
<t t-out="ev.name"/> <a t-attf-href="/event/#{ slug(ev) }">
</h4> <t t-if="ev.image_1024 or ev.image_512">
<p class="fw-bold mb-4" style="color: #E05263; font-size: 1.05rem;"> <img t-attf-src="/web/image/event.event/#{ev.id}/image_512" class="tncsc-card-img" t-att-alt="ev.name"/>
<t t-if="ev.date_begin"> </t>
<span t-field="ev.date_begin" t-options="{'widget': 'datetime', 'format': 'dd MMM yyyy, hh:mm a'}"/> <t t-elif="'picnic' in ev.name.lower()">
<img src="/tncsc_deployment/static/src/img/events/summer-picnic.jpg" class="tncsc-card-img" t-att-alt="ev.name"/>
</t>
<t t-elif="'pongal' in ev.name.lower()">
<img src="/tncsc_deployment/static/src/img/dashboard/pongal_featured_banner.jpg" class="tncsc-card-img" t-att-alt="ev.name"/>
</t>
<t t-elif="'new year' in ev.name.lower()">
<img src="/tncsc_deployment/static/src/img/hero/slide-1.webp" class="tncsc-card-img" t-att-alt="ev.name"/>
</t> </t>
<t t-else=""> <t t-else="">
Upcoming <img src="/tncsc_deployment/static/src/img/events/event-banner.webp" class="tncsc-card-img" t-att-alt="ev.name"/>
</t> </t>
</p>
<div>
<a t-attf-href="/event/#{ slug(ev) }/register" class="btn-tncsc-gold px-4 py-2" style="border-radius: 4px;">
Register Event
</a> </a>
<div class="tncsc-media-overlay"/>
<!-- Status Pill Floating Top-Right -->
<t t-if="ev_status == 'open'">
<div class="tncsc-status-badge badge-status-open">
<span class="status-pulse-dot"></span> Open
</div>
</t>
<t t-elif="ev_status == 'sold_out'">
<div class="tncsc-status-badge badge-status-soldout">
<i class="fa fa-ticket me-1"></i> Sold Out
</div>
</t>
<t t-elif="ev_status == 'ongoing'">
<div class="tncsc-status-badge badge-status-live">
<span class="status-pulse-dot"></span> Live Now
</div>
</t>
<t t-else="">
<div class="tncsc-status-badge badge-status-closed">
<i class="fa fa-lock me-1"></i> Closed
</div>
</t>
<!-- Category Pill Floating Top-Left -->
<div class="tncsc-category-pill">
<i class="fa fa-bookmark-o" style="color: #D89C43;"></i>
<t t-out="ev.stage_display_name or 'Festival'"/>
</div>
<!-- Date Badge Floating Bottom-Left -->
<div class="tncsc-card-date-badge" t-if="ev.date_begin">
<div class="date-month"><t t-esc="ev.date_begin.strftime('%b').upper()"/></div>
<div class="date-day"><t t-esc="ev.date_begin.strftime('%d')"/></div>
</div>
</div>
<!-- Card Body Content -->
<div class="tncsc-card-body">
<!-- Event Name -->
<h3 class="tncsc-card-title">
<a t-attf-href="/event/#{ slug(ev) }">
<t t-out="ev.name"/>
</a>
</h3>
<!-- Subtitle / Tagline -->
<p class="tncsc-card-subtitle" t-if="ev.subtitle" t-out="ev.subtitle"/>
<p class="tncsc-card-subtitle text-muted" t-else="">Tamil Nadu Cultural Society of Canada</p>
<!-- Meta Details List -->
<div class="tncsc-card-meta">
<div class="tncsc-meta-row" t-if="ev.date_begin">
<i class="fa fa-calendar-check-o"></i>
<span><span t-field="ev.date_begin" t-options="{'widget': 'datetime', 'format': 'EEEE, dd MMMM yyyy'}"/></span>
</div>
<div class="tncsc-meta-row" t-if="ev.date_begin">
<i class="fa fa-clock-o"></i>
<span>
<span t-field="ev.date_begin" t-options="{'widget': 'datetime', 'format': 'hh:mm a'}"/>
<t t-if="ev.date_end"> - <span t-field="ev.date_end" t-options="{'widget': 'datetime', 'format': 'hh:mm a'}"/></t>
<small class="text-muted ms-1" t-if="ev.date_tz">(<t t-esc="ev.date_tz"/>)</small>
</span>
</div>
<div class="tncsc-meta-row">
<i class="fa fa-map-marker" style="color: #E05263;"></i>
<span>
<t t-if="ev.address_id">
<t t-out="ev.address_id.name or ev.address_id.city or 'Ontario, Canada'"/>
</t>
<t t-else="">
Online / Ontario, Canada
</t>
</span>
</div>
</div>
<!-- Status Banner Callout -->
<t t-if="ev_status == 'open'">
<div class="tncsc-status-notice notice-open">
<i class="fa fa-check-circle text-success me-1"></i>
<span>Registration open • Book tickets now</span>
</div>
</t>
<t t-elif="ev_status == 'sold_out'">
<div class="tncsc-status-notice notice-soldout">
<i class="fa fa-ticket text-warning me-1"></i>
<span>All available seats have been reserved</span>
</div>
</t>
<t t-else="">
<div class="tncsc-status-notice notice-closed">
<i class="fa fa-info-circle text-danger me-1"></i>
<span>Event ended • Registration closed</span>
</div>
</t>
<!-- Card Action Button Footer -->
<div class="tncsc-card-footer">
<t t-if="ev_status == 'open'">
<a t-attf-href="/event/#{ slug(ev) }/register" class="btn-tncsc-event-action btn-action-open">
Register Event <i class="fa fa-arrow-right ms-2"></i>
</a>
</t>
<t t-elif="ev_status == 'sold_out'">
<a t-attf-href="/event/#{ slug(ev) }" class="btn-tncsc-event-action btn-action-soldout">
Sold Out • View Details <i class="fa fa-chevron-right ms-2"></i>
</a>
</t>
<t t-else="">
<a t-attf-href="/event/#{ slug(ev) }" class="btn-tncsc-event-action btn-action-closed">
Event Ended • View Details <i class="fa fa-chevron-right ms-2"></i>
</a>
</t>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -5149,6 +5614,15 @@
</div> </div>
</t> </t>
</div> </div>
<!-- Empty State Notice for Filter -->
<div id="tncscEmptyFilterNotice" class="text-center py-5" style="display: none;">
<div class="p-4 rounded-4 mx-auto" style="max-width: 500px; background: #ffffff; border: 1px dashed #cbd5e1; box-shadow: 0 4px 12px rgba(0,0,0,0.04);">
<i class="fa fa-calendar-times-o fs-1 text-muted mb-3 d-block"></i>
<h5 class="fw-bold" style="color: #0A1938;">No Events in this Category</h5>
<p class="text-muted small mb-0">Please switch to "All Celebrations" to explore our full event schedule.</p>
</div>
</div>
</div> </div>
</section> </section>
@ -5199,6 +5673,48 @@
<script type="text/javascript"> <script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
// Interactive Event Filter Tabs
const filterBtns = document.querySelectorAll(".tncsc-filter-btn");
const eventCols = document.querySelectorAll(".tncsc-event-col");
const emptyNotice = document.getElementById("tncscEmptyFilterNotice");
function applyFilter(filter) {
let visibleCount = 0;
eventCols.forEach(function(col) {
const status = col.getAttribute("data-status");
if (filter === "all" || status === filter) {
col.style.display = "";
visibleCount++;
} else {
col.style.display = "none";
}
});
if (emptyNotice) {
emptyNotice.style.display = visibleCount === 0 ? "block" : "none";
}
}
filterBtns.forEach(function(btn) {
btn.addEventListener("click", function() {
filterBtns.forEach(function(b) { b.classList.remove("active"); });
this.classList.add("active");
const filter = this.getAttribute("data-filter");
applyFilter(filter);
});
});
// Check URL parameter ?filter=open or ?filter=closed
try {
const urlParams = new URLSearchParams(window.location.search);
const filterParam = urlParams.get('filter');
if (filterParam &amp;&amp; ["open", "closed"].includes(filterParam)) {
const targetBtn = document.querySelector('.tncsc-filter-btn[data-filter="' + filterParam + '"]');
if (targetBtn) {
targetBtn.click();
}
}
} catch(e) {}
// Simple Intersection Observer for scroll animations // Simple Intersection Observer for scroll animations
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => { entries.forEach(entry => {