diff --git a/addons_extensions/hr_recruitment_web_app/static/src/css/ats.css b/addons_extensions/hr_recruitment_web_app/static/src/css/ats.css
new file mode 100644
index 000000000..75c9b03c5
--- /dev/null
+++ b/addons_extensions/hr_recruitment_web_app/static/src/css/ats.css
@@ -0,0 +1,450 @@
+@import url('colors.css');
+
+/* ===== ATS Custom Layout Reset ===== */
+/* ===== Theme Toggle Styles ===== */
+.theme-toggle-container {
+ padding: 1rem;
+ margin-top: auto; /* Push to bottom */
+ border-top: 1px solid var(--border-color);
+}
+
+.theme-toggle {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border-radius: 8px;
+ background: transparent;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.theme-toggle:hover {
+ background-color: var(--primary-light);
+ color: var(--primary-color);
+}
+
+.light-icon {
+ display: none;
+ color: #fbbf24; /* Amber color for sun */
+}
+
+.dark-icon {
+ color: #cbd5e1; /* Light gray for moon */
+}
+
+[data-theme="dark"] .light-icon {
+ display: block;
+}
+
+[data-theme="dark"] .dark-icon {
+ display: none;
+}
+
+.theme-text {
+ transition: opacity 0.2s ease;
+}
+
+/* Collapsed state styles */
+.sidebar.collapsed .theme-text {
+ display: none;
+}
+
+.sidebar.collapsed .theme-toggle {
+ justify-content: center;
+ padding: 0.75rem 0;
+}
+
+.sidebar.collapsed .theme-toggle i {
+ margin-right: 0;
+ font-size: 1.25rem;
+}
+
+body.ats-app {
+ margin: 0;
+ padding: 0;
+ font-family: "Segoe UI", sans-serif;
+ background-color: var(--body-bg);
+ color: var(--text-primary);
+ height: 100vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ line-height: 1.5;
+}
+
+/* ===== Main Layout ===== */
+.ats-app .layout-container {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+/* ===== Header ===== */
+.ats-app .main-header {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 1rem;
+ font-size: 1.8rem;
+ font-weight: 600;
+ padding: 1rem;
+ position: relative; /* For absolute positioning of toggle button */
+}
+
+/* Logo and title alignment */
+.ats-app .main-header img {
+ height: 40px;
+ width: auto;
+}
+
+.ats-app .main-header span {
+ display: flex;
+ align-items: center;
+ height: 40px; /* Match logo height */
+}
+
+/* ===== Layout Container ===== */
+.ats-app .layout-container {
+ display: flex;
+ height: 100%; /* Adjust for header height */
+ overflow: hidden;
+}
+
+/* ===== Sidebar ===== */
+.ats-app .sidebar {
+ background-color: var(--sidebar-bg);
+ color: var(--sidebar-text);
+ padding: 1rem 1rem 2rem;
+ box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
+ transition: all 0.4s ease-in-out;
+ position: relative;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Sidebar States */
+.ats-app .sidebar.expanded {
+ width: 240px;
+ min-width: 240px;
+}
+
+.ats-app .sidebar.collapsed {
+ width: 60px;
+ min-width: 60px;
+ padding: 1rem 0.4rem 2rem;
+}
+
+.ats-app .sidebar.collapsed .menu-item-text,
+.ats-app .sidebar.collapsed .list-title,
+.ats-app .sidebar.collapsed .main-header span {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+ position: absolute;
+}
+
+/* ===== Sidebar Menu ===== */
+.ats-app .menu-list {
+ list-style: none;
+ padding-top: 1rem;
+ margin: 2rem 0;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.ats-app .list-title {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05rem;
+ margin: 1.5rem 1.5rem 0.5rem;
+ transition: all 0.3s ease;
+}
+
+.ats-app .menu-list a {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ color: var(--text-secondary);
+ text-decoration: none;
+ padding: 0.7rem 1rem;
+ border-radius: var(--radius-md);
+ transition: all 0.2s ease;
+ font-size: 0.95rem;
+}
+
+.ats-app .menu-list a i {
+ font-size: 1.25rem;
+ min-width: 24px;
+ text-align: center;
+}
+
+.ats-app .menu-list a:hover {
+ background-color: #DBEDFE;
+ color: #0061FF;
+ transform: translateX(6px);
+ box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2);
+ font-weight: 600;
+ letter-spacing: 0.5px;
+}
+
+.ats-app .menu-list a.active {
+ background-color: #DBEDFE;
+ color: #0061FF;
+ font-weight: 500;
+}
+
+.ats-app .menu-list a.active::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 4px;
+ background-color: #DBEDFE;
+ border-radius: var(--radius-sm) 0 0 var(--radius-sm);
+}
+
+/* Toggle Button (Improved) */
+.ats-app .toggle-btn {
+ position: absolute;
+ top: 1.5rem;
+ right: -12px;
+ background-color: var(--surface-color);
+ color: var(--primary-color);
+ border: 1px solid var(--border-color);
+ padding: 0.5rem;
+ cursor: pointer;
+ border-radius: 50%;
+ box-shadow: var(--shadow-sm);
+ z-index: 20;
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ transition: all 0.3s ease;
+}
+
+.ats-app .toggle-btn:hover {
+ background-color: var(--primary-color);
+ color: white;
+ transform: translateX(2px);
+}
+
+.ats-app .sidebar.collapsed .toggle-btn {
+ right: -12px;
+ transform: rotate(180deg);
+}
+
+.ats-app .sidebar.collapsed .toggle-btn:hover {
+ transform: rotate(180deg) translateX(2px);
+}
+
+/* ===== Main Content Area ===== */
+.ats-app .content-area {
+ flex: 1;
+ padding: 2rem;
+ background-color: var(--content-bg);
+ overflow-y: auto;
+ box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.02);
+ border-left: 1px solid #dcdde1;
+ min-width: 0;
+}
+
+/* ===== Responsive Design ===== */
+@media (max-width: 1024px) {
+ .ats-app .layout-container {
+ flex-direction: column;
+ }
+
+ .ats-app .sidebar {
+ width: 100%;
+ height: auto;
+ padding: 0;
+ border-right: none;
+ border-bottom: 1px solid var(--border-color);
+ }
+
+ .ats-app .sidebar.expanded {
+ height: auto;
+ }
+
+ .ats-app .sidebar.collapsed {
+ height: 60px;
+ overflow: hidden;
+ }
+
+ .ats-app .menu-list {
+ flex-direction: row;
+ flex-wrap: wrap;
+ margin: 0;
+ padding: 0.5rem;
+ gap: 0.25rem;
+ }
+
+ .ats-app .menu-list a {
+ flex-direction: column;
+ padding: 0.75rem;
+ gap: 0.25rem;
+ font-size: 0.75rem;
+ margin: 0;
+ }
+
+ .ats-app .menu-list a i {
+ font-size: 1.1rem;
+ }
+
+ .ats-app .menu-list a.active::before {
+ width: 100%;
+ height: 3px;
+ top: auto;
+ bottom: 0;
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
+ }
+
+ .ats-app .list-title {
+ display: none;
+ }
+
+ .ats-app .toggle-btn {
+ position: fixed;
+ bottom: 1.5rem;
+ right: 1.5rem;
+ top: auto;
+ left: auto;
+ background-color: var(--primary-color);
+ color: white;
+ box-shadow: var(--shadow-lg);
+ width: 48px;
+ height: 48px;
+ font-size: 1.25rem;
+ }
+
+ .ats-app .content-area {
+ padding: 1.25rem;
+ }
+}
+
+/* Animation for smoother transitions */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(5px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.ats-app .content-area > * {
+ animation: fadeIn 0.3s ease-out;
+}
+
+/* Menu list styles */
+.menu-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.menu-list li {
+ margin-bottom: 4px;
+}
+
+.main-menus {
+ padding-left: 1 rem;
+}
+
+.menu-item {
+ display: flex;
+ align-items: center;
+ padding-left: 12rem;
+ padding: 12px 16px;
+ color: var(--text-primary);
+ text-decoration: none;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+}
+
+.menu-item:hover {
+ background-color: var(--primary-light);
+ color: var(--primary-color);
+}
+
+.menu-item.active {
+ background-color: var(--primary-light);
+ color: var(--primary-color);
+ font-weight: 500;
+}
+
+.menu-icon {
+ font-size: 1.25rem;
+ min-width: 24px;
+ margin-right: 12px;
+}
+
+.menu-item-text {
+ transition: opacity 0.2s ease;
+}
+
+/* Collapsed state styles */
+.sidebar.collapsed .menu-item {
+ justify-content: center;
+ padding: 12px 0;
+}
+
+.sidebar.collapsed .menu-icon {
+ margin-right: 0;
+ font-size: 1.4rem;
+}
+
+.sidebar.collapsed .menu-item-text {
+ display: none;
+}
+
+/* Active state indicator for collapsed */
+.sidebar.collapsed .menu-item.active::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 4px;
+ height: 24px;
+ background-color: var(--primary-color);
+ border-radius: 0 4px 4px 0;
+}
+
+/* Mobile responsive styles */
+@media (max-width: 768px) {
+ .sidebar.collapsed .menu-list {
+ display: flex;
+ justify-content: space-around;
+ }
+
+ .sidebar.collapsed .menu-item {
+ flex-direction: column;
+ padding: 8px 4px;
+ font-size: 0.75rem;
+ }
+
+ .sidebar.collapsed .menu-icon {
+ font-size: 1.2rem;
+ margin-bottom: 4px;
+ }
+
+ .sidebar.collapsed .menu-item.active::after {
+ left: 50%;
+ top: auto;
+ bottom: 0;
+ transform: translateX(-50%);
+ width: 24px;
+ height: 3px;
+ }
+}
diff --git a/addons_extensions/hr_recruitment_web_app/static/src/css/colors.css b/addons_extensions/hr_recruitment_web_app/static/src/css/colors.css
new file mode 100644
index 000000000..676eeaaff
--- /dev/null
+++ b/addons_extensions/hr_recruitment_web_app/static/src/css/colors.css
@@ -0,0 +1,103 @@
+
+ /* Background Colors */
+ --body-bg: #f5f6fa;
+ --sidebar-bg: #FAFCFF;
+ --create-model-bg: #FAFCFF;
+ --content-bg: #E3E9EF;
+ --active-search-bg: #ffffff;
+ --active-search-hover-bg: #f0f0f0;
+ --add-btn-bg: #3498db;
+ --add-btn-hover-bg: #2980b9;
+ --side-panel-bg: #FAFCFF;
+ --side-panel-item-hover: #f0f8ff;
+ --side-panel-item-selected: #d7eaff;
+
+ /* Text Colors */
+ --text-primary: #2f3542;
+ --text-secondary: #4B5865;
+ --text-muted: #6c757d;
+ --sidebar-text: #0F1419;
+ --active-search-text: #333;
+ --add-btn-color: #ffffff;
+
+ /* Border Colors */
+ --border-color: #dcdde1;
+ --border-light: #e0e0e0;
+
+ /* Shadow Colors */
+ --shadow-color: rgba(0, 0, 0, 0.1);
+ --shadow-dark: rgba(0, 0, 0, 0.2);
+
+ /* Status Colors */
+ --status-new: #3498db;
+ --status-interview: #f39c12;
+ --status-hired: #2ecc71;
+ --status-rejected: #e74c3c;
+}
+
+[data-theme="dark"] {
+ /* Primary Colors - Professional Blue Tones */
+ --primary-blue: #4A90E2;
+ --primary-blue-dark: #357ABD;
+ --primary-blue-light: #5BA0F2;
+
+ /* Secondary Colors - Subtle & Professional */
+ --secondary-purple: #8E44AD;
+ --secondary-green: #27AE60;
+ --secondary-red: #C0392B;
+ --secondary-yellow: #F39C12;
+
+ /* Grayscale - Sophisticated Dark Grays */
+ --white: #1E1E1E;
+ --gray-100: #2A2A2A;
+ --gray-200: #333333;
+ --gray-300: #3D3D3D;
+ --gray-400: #474747;
+ --gray-500: #525252;
+ --gray-600: #959595;
+ --gray-700: #B0B0B0;
+ --gray-800: #CACACA;
+ --gray-900: #E5E5E5;
+ --black: #F5F5F5;
+
+ /* Semantic Colors - Balanced Visibility */
+ --success: #27AE60;
+ --info: #3498DB;
+ --warning: #F39C12;
+ --danger: #E74C3C;
+
+ /* Background Colors - Layered Depth */
+ --body-bg: #1A1A1A;
+ --sidebar-bg: #232323;
+ --create-model-bg: #232323;
+ --content-bg: #2A2A2A;
+ --active-search-bg: #2A2A2A;
+ --active-search-hover-bg: #333333;
+ --add-btn-bg: #4A90E2;
+ --add-btn-hover-bg: #357ABD;
+ --side-panel-bg: #232323;
+ --side-panel-item-hover: #2A2A2A;
+ --side-panel-item-selected: #2D4158;
+
+ /* Text Colors - High Contrast */
+ --text-primary: #E5E5E5;
+ --text-secondary: #B0B0B0;
+ --text-muted: #959595;
+ --sidebar-text: #E0E0E0;
+ --active-search-text: #F5F5F5;
+ --add-btn-color: #FFFFFF;
+
+ /* Border Colors - Subtle Definition */
+ --border-color: #3D3D3D;
+ --border-light: #333333;
+
+ /* Shadow Colors - Soft Depth */
+ --shadow-color: rgba(0, 0, 0, 0.25);
+ --shadow-dark: rgba(0, 0, 0, 0.4);
+
+ /* Status Colors - Professional Hierarchy */
+ --status-new: #4A90E2;
+ --status-interview: #F39C12;
+ --status-hired: #27AE60;
+ --status-rejected: #E74C3C;
+}
\ No newline at end of file
diff --git a/addons_extensions/hr_recruitment_web_app/static/src/css/content.css b/addons_extensions/hr_recruitment_web_app/static/src/css/content.css
new file mode 100644
index 000000000..db02ad0ae
--- /dev/null
+++ b/addons_extensions/hr_recruitment_web_app/static/src/css/content.css
@@ -0,0 +1,1195 @@
+@import url('colors.css');
+
+/* Main Container */
+#ats-details-container {
+ overflow-y: auto;
+ max-height: 100%;
+ position: relative;
+ background-color: var(--content-bg);
+}
+
+/* Grid Layout */
+.ats-grid {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ grid-auto-rows: minmax(100px, auto);
+ gap: 20px;
+ padding: 20px;
+ font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ color: var(--text-primary);
+ background-color: var(--content-bg);
+ max-width: 100%;
+ margin: 0 auto;
+ transition: all 0.3s ease;
+ overflow: hidden;
+}
+
+/* Card Base */
+.ats-grid .ats-card {
+ overflow: hidden;
+ background-color: var(--sidebar-bg);
+ border-radius: 12px;
+ padding: 25px;
+ margin-bottom: 25px;
+ box-shadow: 0 4px 20px var(--shadow-color);
+ transition: all 0.3s ease;
+ border: 1px solid var(--border-light);
+}
+
+/* Hover Effect */
+.ats-grid .ats-card:hover {
+ box-shadow: 0 8px 30px var(--shadow-dark);
+ transform: translateY(-5px);
+}
+
+/* Width Span Utilities */
+.ats-grid .span-1 { grid-column: span 1; }
+.ats-grid .span-2 { grid-column: span 2; }
+.ats-grid .span-3 { grid-column: span 3; }
+.ats-grid .span-4 { grid-column: span 4; }
+.ats-grid .span-5 { grid-column: span 5; }
+.ats-grid .span-6 { grid-column: span 6; }
+.ats-grid .span-7 { grid-column: span 7; }
+.ats-grid .span-8 { grid-column: span 8; }
+.ats-grid .span-9 { grid-column: span 9; }
+.ats-grid .span-10 { grid-column: span 10; }
+.ats-grid .span-11 { grid-column: span 11; }
+.ats-grid .span-12 { grid-column: span 12; }
+
+/* Navigation Sidebar */
+.ats-grid #ats-overview {
+ background-color: var(--gray-800);
+ color: var(--white);
+ padding: 20px;
+ border-radius: 12px;
+ box-shadow: 0 8px 24px var(--shadow-dark);
+ transition: all 0.3s ease;
+ font-family: 'Segoe UI', Tahoma, sans-serif;
+ width: 220px;
+ border: 1px solid var(--gray-700);
+}
+
+.ats-grid #ats-overview h3,
+.ats-grid #ats-overview .section-title {
+ border-bottom: 1px solid var(--gray-700);
+ margin-bottom: 12px;
+ padding-bottom: 8px;
+ font-weight: 600;
+ color: var(--gray-200);
+}
+
+.ats-grid .overview-nav {
+ padding: 10px 0;
+}
+
+.ats-grid .nav-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.ats-grid .nav-list li {
+ margin-bottom: 8px;
+ position: relative;
+ padding-left: 0px;
+}
+
+.ats-grid .nav-link {
+ display: flex;
+ align-items: center;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ color: var(--gray-300);
+ text-decoration: none;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+ font-size: 14px;
+}
+
+.ats-grid .nav-link:hover {
+ background-color: var(--gray-700);
+ color: var(--white);
+ transform: translateX(6px);
+ box-shadow: 3px 3px 10px var(--shadow-dark);
+ font-weight: 600;
+ letter-spacing: 0.5px;
+}
+
+.ats-grid .nav-link i {
+ width: 20px;
+ text-align: center;
+}
+
+/* Header Section */
+.ats-grid .ats-title {
+ font-size: 28px;
+ font-weight: 600;
+ margin-bottom: 15px;
+ color: var(--text-primary);
+}
+
+.ats-grid .ats-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+ margin-bottom: 20px;
+ color: var(--text-muted);
+}
+
+.ats-grid .meta-item {
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+}
+
+/* Status Bar */
+.ats-grid .status-bar {
+ background-color: var(--gray-100);
+ padding: 15px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+}
+
+.ats-grid .recruitment-status {
+ margin-bottom: 10px;
+}
+
+.ats-grid .status-label {
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+.ats-grid .status-value {
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.ats-grid .recruitment-progress .progress {
+ height: 20px;
+ border-radius: 5px;
+ background-color: var(--gray-200);
+}
+
+.ats-grid .progress-bar {
+ background-color: var(--primary-blue);
+}
+
+/* Section Titles */
+.ats-grid .section-title {
+ margin: 0;
+ padding-bottom: 20px;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ border-bottom: 2px solid var(--gray-200);
+ transition: all 0.3s ease;
+}
+
+.ats-grid .ats-card:hover .section-title {
+ border-color: var(--gray-300);
+}
+
+.ats-grid .section-title i {
+ color: var(--text-muted);
+ transition: all 0.3s ease;
+}
+
+
+.ats-grid .section-title small {
+ margin-left: auto; /* This pushes the small element to the right */
+ font-weight: normal;
+ font-size: 0.85em;
+ color: var(--text-muted);
+}
+
+/* Detail Grid */
+.ats-grid .detail-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 10px;
+ margin-top: 0px;
+}
+
+.ats-grid .detail-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ align-items: center;
+}
+
+.ats-grid .detail-row:hover {
+ background-color: var(--gray-100);
+ border-radius: 3px;
+}
+
+.ats-grid .detail-label {
+ font-weight: 500;
+ color: var(--text-secondary);
+ min-width: 120px;
+ font-size: 15px;
+ flex-shrink: 0;
+}
+
+.ats-grid .span-6 .detail-label {
+ min-width: 160px;
+}
+
+.ats-grid .detail-value {
+ color: var(--text-primary);
+ font-weight: 400;
+ flex: 1;
+ font-size: 13px;
+ word-break: break-word;
+ overflow-wrap: break-word;
+}
+
+/* Skills and Location Badges */
+.ats-grid .skills-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.ats-grid .skill-badge,
+.ats-grid .location-badge {
+ background-color: var(--gray-100);
+ color: var(--primary-blue);
+ padding: 6px 12px;
+ border-radius: 20px;
+ transition: all 0.3s ease;
+}
+
+.ats-grid .skill-badge:hover,
+.ats-grid .location-badge:hover {
+ background-color: var(--gray-200);
+ transform: translateY(-3px);
+ box-shadow: 0 4px 8px rgba(15, 81, 50, 0.1);
+}
+
+/* Team Members */
+.ats-grid .team-member {
+ margin-bottom: 20px;
+}
+
+.ats-grid .member-header {
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 10px;
+ display: flex;
+ align-items: center;
+}
+
+.ats-grid .member-title {
+ margin-left: 5px;
+}
+
+.ats-grid .recruiter-photo {
+ width: 60px;
+ height: 60px;
+ object-fit: cover;
+ border: 2px solid var(--gray-200);
+}
+
+.ats-grid .member-name {
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.ats-grid .member-email {
+ font-size: 13px;
+ color: var(--text-muted);
+}
+
+/* Description Content */
+.ats-grid .description-content {
+ line-height: 1.6;
+ color: var(--text-primary);
+}
+
+.ats-grid .description-content p {
+ margin-bottom: 15px;
+}
+
+.ats-grid .description-content ul,
+.ats-grid .description-content ol {
+ padding-left: 20px;
+ margin-bottom: 15px;
+}
+
+/* Responsive fallback */
+@media (max-width: 768px) {
+ .ats-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ .ats-grid .ats-card {
+ grid-column: span 2 !important;
+ }
+}
+
+@media print {
+ #ats-details-container {
+ max-height: none;
+ overflow-y: visible;
+ }
+}
+
+
+/* Smart Button Container */
+.smart-button-container {
+ position: fixed;
+ top: 20%;
+ right: 1%;
+ z-index: 1000;
+ display: flex;
+ gap: 10px;
+ flex-direction: column;
+ align-items: flex-end;
+}
+
+.smart-button {
+ background-color: var(--primary-blue);
+ color: var(--white);
+ border: none;
+ border-radius: 8px;
+ padding: 10px 16px;
+ font-weight: 500;
+ font-size: 14px;
+ box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 180px;
+ justify-content: center;
+}
+
+.smart-button:hover {
+ background-color: var(--primary-blue-dark);
+ transform: translateY(-3px);
+ box-shadow: 0 6px 16px rgba(52, 152, 219, 0.4);
+}
+
+.smart-button:active {
+ transform: translateY(1px);
+ box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3);
+}
+
+.smart-button i {
+ transition: transform 0.3s ease;
+}
+
+.smart-button:hover i {
+ transform: rotate(15deg);
+}
+
+/* Published Ribbon */
+.status-ribbon {
+ position: absolute;
+ top: 35px;
+ right: -30px;
+ transform: rotate(45deg);
+ padding: 8px 40px;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--white);
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
+ z-index: 100;
+ transition: all 0.3s ease;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.status-ribbon.ribbon-published {
+ background: linear-gradient(45deg, var(--success), #2ecc71);
+}
+
+.status-ribbon.ribbon-not-published {
+ background: linear-gradient(45deg, var(--danger), #e74c3c);
+}
+
+.status-ribbon:hover {
+ transform: rotate(45deg) scale(1.05);
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
+}
+
+/* Professional Typography */
+.ats-grid {
+ font-family: 'Inter', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ letter-spacing: 0.01em;
+}
+
+.ats-grid .ats-title {
+ font-size: 28px;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+ line-height: 1.2;
+ margin-bottom: 16px;
+ color: var(--text-primary);
+}
+
+.ats-grid .section-title {
+ font-size: 18px;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+ margin-bottom: 20px;
+ padding-bottom: 12px;
+ border-bottom: 2px solid var(--gray-200);
+}
+.ats-grid .fa{
+ padding-right:5px;
+}
+.ats-list {
+ background-color:linear-gradient(90deg,rgba(2, 0, 36, 1) 0%, rgba(9, 9, 121, 1) 35%, rgba(0, 212, 255, 1) 100%);
+
+}
+.ats-grid .detail-label {
+ font-weight: 600;
+ color: var(--text-secondary);
+ font-size: 15px;
+}
+
+.ats-grid .detail-value {
+ font-weight: 500;
+ color: var(--text-primary);
+ font-size: 15px;
+}
+
+.ats-grid .meta-item {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-secondary);
+}
+
+.ats-grid .status-label {
+ font-weight: 600;
+ color: var(--text-secondary);
+ font-size: 14px;
+}
+
+.ats-grid .status-value {
+ font-weight: 500;
+ color: var(--text-primary);
+ font-size: 14px;
+}
+
+.ats-grid .skill-badge,
+.ats-grid .location-badge {
+ font-size: 13px;
+ font-weight: 500;
+}
+
+/* Smooth animations for all interactive elements */
+.ats-grid .ats-card {
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+}
+
+.ats-grid .detail-row {
+ transition: background-color 0.2s ease;
+}
+
+.ats-grid .nav-link {
+ transition: all 0.3s ease;
+}
+
+.ats-grid .skill-badge,
+.ats-grid .location-badge {
+ transition: all 0.3s ease;
+}
+
+/* Professional shadows and borders */
+.ats-grid .ats-card {
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ border: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+.ats-grid .ats-card:hover {
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
+ border-color: rgba(0, 0, 0, 0.08);
+}
+
+/* Close button styling */
+.close-detail {
+ position: absolute;
+ top: 15px;
+ left: 15px;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background-color: var(--white);
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ z-index: 100;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.close-detail:hover {
+ background-color: var(--gray-100);
+ transform: rotate(90deg);
+}
+
+.close-detail span {
+ font-size: 20px;
+ color: var(--text-secondary);
+ line-height: 1;
+}
+
+
+/* ===== Work Experience Section ===== */
+.ats-grid .experience-list {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ margin-top: 15px;
+}
+
+.ats-grid .experience-item {
+ background-color: var(--white);
+ border-radius: 10px;
+ padding: 20px;
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
+ border-left: 4px solid var(--primary-blue);
+ transition: all 0.3s ease;
+ position: relative;
+}
+
+.ats-grid .experience-item:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12);
+}
+
+.ats-grid .exp-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 12px;
+}
+
+.ats-grid .exp-header h5 {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.ats-grid .exp-company {
+ font-size: 16px;
+ color: var(--primary-blue);
+ font-weight: 500;
+ background-color: rgba(52, 152, 219, 0.1);
+ padding: 4px 12px;
+ border-radius: 20px;
+}
+
+.ats-grid .exp-duration {
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+}
+
+.ats-grid .exp-duration i {
+ color: var(--primary-blue);
+ margin-right: 8px;
+}
+
+.ats-grid .exp-ctc {
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ color: var(--text-primary);
+ font-weight: 500;
+}
+
+.ats-grid .exp-ctc i {
+ color: var(--success);
+ margin-right: 8px;
+}
+
+/* ===== Education Section ===== */
+.ats-grid .education-list {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ margin-top: 15px;
+}
+
+.ats-grid .education-item {
+ background-color: var(--white);
+ border-radius: 10px;
+ padding: 20px;
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
+ border-left: 4px solid var(--success);
+ transition: all 0.3s ease;
+ position: relative;
+}
+
+.ats-grid .education-item:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12);
+}
+
+.ats-grid .edu-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 12px;
+}
+
+.ats-grid .edu-header h5 {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.ats-grid .edu-type {
+ font-size: 14px;
+ color: var(--white);
+ font-weight: 500;
+ background-color: var(--success);
+ padding: 4px 12px;
+ border-radius: 20px;
+}
+
+.ats-grid .edu-university {
+ font-size: 15px;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+ font-style: italic;
+}
+
+.ats-grid .edu-duration {
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+}
+
+.ats-grid .edu-duration i {
+ color: var(--success);
+ margin-right: 8px;
+}
+
+.ats-grid .edu-marks {
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ color: var(--text-primary);
+ font-weight: 500;
+}
+
+.ats-grid .edu-marks i {
+ color: var(--warning);
+ margin-right: 8px;
+}
+
+/* ===== Skills Section ===== */
+.ats-grid .skills-container {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ margin-top: 15px;
+}
+
+.ats-grid .skill-item {
+ background-color: var(--white);
+ border-radius: 10px;
+ padding: 15px 20px;
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+}
+
+.ats-grid .skill-item:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12);
+}
+
+.ats-grid .skill-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.ats-grid .skill-name {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.ats-grid .skill-type {
+ font-size: 13px;
+ color: var(--white);
+ background-color: var(--primary-blue);
+ padding: 3px 10px;
+ border-radius: 15px;
+}
+
+.ats-grid .skill-level {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.ats-grid .skill-level-name {
+ font-size: 13px;
+ color: var(--text-secondary);
+ text-align: right;
+}
+
+.ats-grid .progress {
+ height: 8px;
+ border-radius: 4px;
+ background-color: var(--gray-200);
+ overflow: hidden;
+}
+
+.ats-grid .progress-bar {
+ height: 100%;
+ border-radius: 4px;
+ background: linear-gradient(90deg, var(--primary-blue), #3498db);
+ transition: width 1s ease-in-out;
+}
+
+/* ===== Attachments Section ===== */
+.ats-grid .attachments-list {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ margin-top: 15px;
+}
+
+.ats-grid .attachment-card {
+ background-color: var(--white);
+ border-radius: 10px;
+ padding: 15px 20px;
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+}
+
+.ats-grid .attachment-card:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12);
+}
+
+.ats-grid .attachment-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.ats-grid .attachment-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+ word-break: break-word;
+ max-width: 70%;
+}
+
+.ats-grid .attachment-badge {
+ font-size: 12px;
+ color: var(--white);
+ padding: 3px 10px;
+ border-radius: 15px;
+}
+
+.ats-grid .attachment-badge.bg-success {
+ background-color: var(--success);
+}
+
+.ats-grid .attachment-badge.bg-danger {
+ background-color: var(--danger);
+}
+
+.ats-grid .attachment-badge.bg-secondary {
+ background-color: var(--gray-500);
+}
+
+.ats-grid .attachment-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.ats-grid .document-action-btn {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 13px;
+ padding: 6px 12px;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+}
+
+.ats-grid .document-action-btn:hover {
+ transform: translateY(-2px);
+}
+
+.ats-grid .preview-btn {
+ background-color: var(--primary-blue);
+ color: var(--white);
+ border: 1px solid var(--primary-blue);
+}
+
+.ats-grid .preview-btn:hover {
+ background-color: var(--white);
+ color: var(--primary-blue);
+}
+
+.ats-grid .download-btn {
+ background-color: var(--success);
+ color: var(--white);
+ border: 1px solid var(--success);
+}
+
+.ats-grid .download-btn:hover {
+ background-color: var(--white);
+ color: var(--success);
+}
+
+/* ===== Timeline Section ===== */
+.ats-grid .timeline {
+ position: relative;
+ margin-top: 20px;
+ padding-left: 30px;
+}
+
+.ats-grid .timeline::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 8px;
+ height: 100%;
+ width: 2px;
+ background-color: var(--gray-300);
+}
+
+.ats-grid .timeline-item {
+ position: relative;
+ margin-bottom: 25px;
+}
+
+.ats-grid .timeline-point {
+ position: absolute;
+ left: -30px;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background-color: var(--primary-blue);
+ border: 3px solid var(--white);
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
+}
+
+.ats-grid .timeline-point.point-interview {
+ background-color: var(--warning);
+ box-shadow: 0 0 0 3px rgba(243, 156, 18, 0.3);
+}
+
+.ats-grid .timeline-content {
+ background-color: var(--white);
+ border-radius: 8px;
+ padding: 15px 20px;
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+}
+
+.ats-grid .timeline-content:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12);
+}
+
+.ats-grid .timeline-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.ats-grid .timeline-header h5 {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.ats-grid .timeline-date {
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+.ats-grid .timeline-body {
+ font-size: 14px;
+ color: var(--text-secondary);
+ line-height: 1.5;
+}
+
+.ats-grid .timeline-body p {
+ margin: 0;
+}
+
+/* ===== Alert Messages ===== */
+.ats-grid .alert {
+ border-radius: 8px;
+ padding: 15px 20px;
+ border: none;
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
+}
+
+.ats-grid .alert-info {
+ background-color: rgba(23, 162, 184, 0.1);
+ color: var(--info);
+ border-left: 4px solid var(--info);
+}
+
+/* ===== Responsive Design ===== */
+@media (max-width: 768px) {
+ .ats-grid .exp-header, .ats-grid .edu-header {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .ats-grid .skill-info {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ }
+
+ .ats-grid .attachment-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ }
+
+ .ats-grid .attachment-title {
+ max-width: 100%;
+ }
+
+ .ats-grid .timeline-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 5px;
+ }
+}
+
+/* ===== Avatar Styling ===== */
+.ats-grid .avatar-wrapper {
+ position: relative;
+ width: 120px;
+ height: 120px;
+ margin-left: auto;
+}
+
+.ats-grid .avatar-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border: 3px solid var(--primary-blue);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.ats-grid .avatar-placeholder {
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(135deg, var(--primary-blue), #3498db);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 3px solid var(--primary-blue);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ position: relative;
+ overflow: hidden;
+}
+
+.ats-grid .avatar-placeholder::before {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: linear-gradient(
+ to right,
+ rgba(255, 255, 255, 0) 0%,
+ rgba(255, 255, 255, 0.1) 50%,
+ rgba(255, 255, 255, 0) 100%
+ );
+ transform: rotate(30deg);
+}
+
+.ats-grid .applicant-img-placeholder {
+ font-size: 48px;
+ font-weight: 700;
+ color: white;
+ text-transform: uppercase;
+ position: relative;
+ z-index: 1;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.ats-grid .avatar-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ cursor: pointer;
+}
+
+.ats-grid .avatar-wrapper:hover .avatar-overlay {
+ opacity: 1;
+}
+
+.ats-grid .avatar-overlay i {
+ color: white;
+ font-size: 24px;
+}
+
+
+
+.ats-card .edit-card-btn {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background-color: var(--primary-blue);
+ color: white;
+ border: none;
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.3s ease, transform 0.2s ease;
+ z-index: 10;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.ats-card:hover .edit-card-btn{
+ opacity: 1;
+}
+
+.ats-card .edit-card-btn:hover {
+ transform: scale(1.1);
+ background-color: var(--primary-blue-dark);
+}
+
+/* Edit Form Container */
+.edit-form-container {
+ padding: 20px;
+ background-color: var(--sidebar-bg);
+ border-radius: 12px;
+ box-shadow: 0 4px 20px var(--shadow-color);
+}
+
+.edit-form-container .form-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid var(--border-light);
+}
+
+.edit-form-container .form-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.edit-form-container .form-content {
+ margin-top: 15px;
+}
+
+/* Form Styles in Edit Mode */
+.edit-form-container .form-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 15px;
+}
+
+.edit-form-container .form-group {
+ margin-bottom: 15px;
+}
+
+.edit-form-container label {
+ display: block;
+ margin-bottom: 5px;
+ font-weight: 500;
+ color: var(--text-secondary);
+}
+
+.edit-form-container .form-input,
+.edit-form-container .form-select {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background-color: var(--white);
+ color: var(--text-primary);
+ transition: border-color 0.2s ease;
+}
+
+.edit-form-container .form-input:focus,
+.edit-form-container .form-select:focus {
+ border-color: var(--primary-blue);
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
+}
+
+.edit-form-container .full-width {
+ grid-column: 1 / -1;
+}
+
+/* Editor Styles */
+.edit-form-container .oe_editor {
+ min-height: 300px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ padding: 10px;
+ background-color: var(--white);
+}
+
+/* Save/Cancel Button Styles */
+.edit-form-container .save-edit-btn,
+.edit-form-container .cancel-edit-btn {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: transform 0.2s ease;
+}
+
+.edit-form-container .save-edit-btn {
+ background-color: var(--success);
+ color: white;
+}
+
+.edit-form-container .save-edit-btn:hover , .edit-form-container .cancel-edit-btn:hover {
+ transform: scale(1.1);
+}
+
+.edit-form-container .cancel-edit-btn {
+ background-color: var(--danger);
+ color: white;
+}
\ No newline at end of file
diff --git a/addons_extensions/hr_recruitment_web_app/static/src/js/candidates.js b/addons_extensions/hr_recruitment_web_app/static/src/js/candidates.js
new file mode 100644
index 000000000..4e432a74e
--- /dev/null
+++ b/addons_extensions/hr_recruitment_web_app/static/src/js/candidates.js
@@ -0,0 +1,630 @@
+/** @odoo-module **/
+function initCandidatesPage() {
+ console.log("candidates Page Loaded");
+ const candidateDetailArea = document.getElementById("candidates-detail");
+ const container = document.querySelector('.ats-list-container'); // Added this line
+ const toggleBtn = document.getElementById("candidates-list-sidebar-toggle-btn"); // Added this line
+ const sidebar = document.getElementById("candidates-list-panel"); // Added this line
+
+ document.querySelectorAll(".ats-item.candidates-item").forEach(item => {
+ item.addEventListener("click", function() {
+ document.querySelectorAll(".candidates-item.selected").forEach(el => el.classList.remove("selected"));
+ this.classList.add("selected");
+
+ // Show the detail panel and add necessary classes
+ candidateDetailArea.style.display = 'block'; // Added this line
+ container.classList.add('ats-selected'); // Added this line
+ sidebar.classList.remove('collapsed'); // Added this line
+ toggleBtn.style.display = 'flex'; // Added this line
+
+ const candidateId = this.dataset.id;
+ console.log("Candidate ID:", candidateId); // Added for debugging
+
+ // Show loading state
+ if (candidateDetailArea) {
+ candidateDetailArea.innerHTML = '
Loading candidate details...
';
+ }
+
+ fetch(`/myATS/candidate/detail/${candidateId}`, {
+ headers: { "X-Requested-With": "XMLHttpRequest" }
+ })
+ .then(res => {
+ if (!res.ok) throw new Error('Network response was not ok');
+ return res.text();
+ })
+ .then(html => {
+ console.log("Response received"); // Added for debugging
+ if (candidateDetailArea) {
+ candidateDetailArea.innerHTML = html;
+ initCandidateDetailEdit();
+
+ // Add close button functionality
+ const closeBtn = candidateDetailArea.querySelector('.close-detail');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', function() {
+ candidateDetailArea.style.display = 'none';
+ container.classList.remove('ats-selected');
+ document.querySelectorAll(".ats-item.candidates-item.selected").forEach(el => el.classList.remove("selected"));
+ });
+ }
+ }
+ })
+ .catch(error => {
+ console.error('Error loading candidate details:', error);
+ if (candidateDetailArea) {
+ candidateDetailArea.innerHTML = 'Error loading candidate details. Please try again.
';
+ }
+ });
+ });
+ });
+
+ // Search functionality
+ const search = document.getElementById("candidates-search");
+ if (search) {
+ search.addEventListener("input", function() {
+ const query = this.value.toLowerCase();
+ let visibleCount = 0;
+ document.querySelectorAll(".ats-item.candidates-item").forEach(item => {
+ const match = item.textContent.toLowerCase().includes(query);
+ item.style.display = match ? "" : "none";
+ if (match) visibleCount++;
+ });
+ const countElement = document.getElementById("active-records-count");
+ if (countElement) {
+ countElement.textContent = visibleCount;
+ }
+ });
+ }
+
+ // Sidebar Toggle
+ if (toggleBtn && sidebar) { // Added this check
+ toggleBtn.addEventListener("click", function(e) {
+ e.stopPropagation();
+ sidebar.classList.toggle("collapsed");
+ });
+ }
+
+ // Rest of your code remains the same...
+ const createCandidate = document.getElementById('add-candidate-create-btn');
+ const candidateModal = document.getElementById('candidate-form-modal');
+ const form = document.getElementById('candidate-form');
+ const closeModal = document.querySelectorAll('.candidate-form-close, .btn-cancel');
+ const avatarUpload = document.getElementById('avatar-upload');
+ const candidateImage = document.getElementById('candidate-image');
+ const avatarUploadIcon = document.querySelector('.avatar-upload i');
+
+ if (avatarUpload && candidateImage) {
+ // Make the entire avatar clickable
+ candidateImage.parentElement.style.cursor = 'pointer';
+ // Handle click on avatar
+ candidateImage.parentElement.addEventListener('click', function(e) {
+ if (e.target !== avatarUpload && e.target !== avatarUploadIcon) {
+ avatarUpload.click();
+ }
+ });
+ // Handle file selection
+ avatarUpload.addEventListener('change', function(e) {
+ const file = e.target.files[0];
+ if (file && file.type.match('image.*')) {
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ candidateImage.src = e.target.result;
+ };
+ reader.readAsDataURL(file);
+ }
+ });
+ }
+
+ // Resume Upload Elements
+ const resumeUpload = document.getElementById('resume-upload');
+ const resumeDropzone = document.getElementById('resume-dropzone');
+ const resumePreview = document.getElementById('resume-preview');
+ const resumePlaceholder = document.querySelector('.resume-preview-placeholder');
+ const resumeIframe = document.getElementById('resume-iframe');
+ const resumeImage = document.getElementById('resume-image');
+ const unsupportedFormat = document.getElementById('unsupported-format');
+ const downloadResume = document.getElementById('download-resume');
+ const uploadResumeBtn = document.getElementById('upload-applicant-resume');
+ let currentResumeFile = null;
+
+ const saveBtn = document.getElementById('save-candidate');
+ if (saveBtn) {
+ saveBtn.addEventListener('click', function (e) {
+ e.preventDefault();
+ createNewCandidate(form, candidateModal);
+ });
+ }
+
+ if (createCandidate) {
+ createCandidate.addEventListener('click', function(e) {
+ e.preventDefault();
+ candidateModal.style.display = 'flex';
+ setTimeout(() => {
+ candidateModal.classList.add('show');
+ }, 10);
+ document.body.style.overflow = 'hidden';
+ setTimeout(() => {
+ initSelect2();
+ initResumeUploadHandlers();
+ }, 100);
+ });
+ }
+
+ if (closeModal) {
+ closeModal.forEach(btn => {
+ btn.addEventListener('click', function() {
+ candidateModal.classList.remove('show');
+ setTimeout(() => {
+ candidateModal.style.display = 'none';
+ }, 300);
+ document.body.style.overflow = '';
+ });
+ });
+ }
+
+// candidateModal.addEventListener('click', function(e) {
+// if (e.target === candidateModal) {
+// candidateModal.classList.remove('show');
+// setTimeout(() => {
+// candidateModal.style.display = 'none';
+// }, 300);
+// document.body.style.overflow = '';
+// }
+// });
+
+ function formatUserOption(user) {
+ if (!user.id) return user.text;
+ var imageUrl = $(user.element).data('image') || '/web/static/img/placeholder.png';
+ var $container = $(
+ '' +
+ ' ' +
+ '' + user.text + ' ' +
+ ' '
+ );
+ return $container;
+ }
+
+ function formatUserSelection(user) {
+ if (!user.id) return user.text;
+ var imageUrl = $(user.element).data('image') || '/web/static/img/placeholder.png';
+ var $container = $(
+ '' +
+ ' ' +
+ '' + user.text + ' ' +
+ ' '
+ );
+ return $container;
+ }
+
+ function initSelect2() {
+ const candidateSkills = document.getElementById('candidate-skills');
+ if (candidateSkills) {
+ $(candidateSkills).select2({
+ placeholder: 'Select skills',
+ allowClear: true,
+ dropdownParent: $('.candidate-form-modal'),
+ width: '100%',
+ escapeMarkup: function(m) { return m; }
+ });
+ }
+
+ const managerSelect = document.getElementById('manager');
+ if (managerSelect) {
+ $(managerSelect).select2({
+ placeholder: 'Select Manager',
+ allowClear: true,
+ templateResult: formatUserOption,
+ templateSelection: formatUserSelection,
+ escapeMarkup: function(m) { return m; }
+ });
+ }
+ }
+
+ function initResumeUploadHandlers() {
+ // Create remove button
+ const removeResumeBtn = document.createElement('button');
+ removeResumeBtn.innerHTML = ' Remove Resume';
+ removeResumeBtn.className = 'btn btn-danger btn-sm mt-2';
+ removeResumeBtn.style.display = 'none';
+ resumePreview.appendChild(removeResumeBtn);
+
+ // Handle remove resume
+ removeResumeBtn.addEventListener('click', function() {
+ resetResumePreview();
+ });
+
+ function resetResumePreview() {
+ // Clear file input
+ resumeUpload.value = '';
+ currentResumeFile = null;
+ // Reset preview
+ resumePlaceholder.style.display = 'flex';
+ resumeIframe.style.display = 'none';
+ resumeImage.style.display = 'none';
+ unsupportedFormat.style.display = 'none';
+ removeResumeBtn.style.display = 'none';
+ // Reset iframe/src to prevent memory leaks
+ if (resumeIframe.src) {
+ URL.revokeObjectURL(resumeIframe.src);
+ resumeIframe.src = '';
+ }
+ if (resumeImage.src) {
+ URL.revokeObjectURL(resumeImage.src);
+ resumeImage.src = '';
+ }
+ if (downloadResume.href) {
+ URL.revokeObjectURL(downloadResume.href);
+ downloadResume.href = '#';
+ }
+ }
+
+ // Unified upload handler for both preview and parsing
+ uploadResumeBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.accept = '.pdf,.doc,.docx,.txt,.jpg,.jpeg,.png';
+ fileInput.onchange = async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ // First handle the preview
+ handleResumeFile(file);
+
+ // Then try to parse the resume if it's a parseable type
+ if (file.type.match(/pdf|msword|openxmlformats|text/)) {
+ // Show loading state
+ const button = uploadResumeBtn;
+ const originalText = button.innerHTML;
+ button.innerHTML = ' Processing Resume...';
+ button.disabled = true;
+
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('type', 'candidate');
+
+ const response = await fetch('/resume/upload', {
+ method: 'POST',
+ body: formData,
+ credentials: 'same-origin'
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+ populateCandidateForm(result);
+ } catch (error) {
+ console.error('Error parsing Resume:', error);
+ showNotification('Failed to parse Resume. Please try again or enter manually.', 'danger');
+ } finally {
+ button.innerHTML = ' Upload Resume';
+ button.disabled = false;
+ }
+ }
+ };
+ fileInput.click();
+ });
+
+ // Handle click on dropzone
+ resumeDropzone.addEventListener('click', function(e) {
+ if (e.target === this || e.target.classList.contains('upload-icon') ||
+ e.target.tagName === 'H5' || e.target.tagName === 'P') {
+ resumeUpload.click();
+ }
+ });
+
+ // Handle drag and drop
+ resumeDropzone.addEventListener('dragover', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.classList.add('dragover');
+ this.style.borderColor = '#3498db';
+ this.style.backgroundColor = 'rgba(52, 152, 219, 0.1)';
+ });
+
+ resumeDropzone.addEventListener('dragleave', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.classList.remove('dragover');
+ this.style.borderColor = '';
+ this.style.backgroundColor = '';
+ });
+
+ resumeDropzone.addEventListener('drop', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.classList.remove('dragover');
+ this.style.borderColor = '';
+ this.style.backgroundColor = '';
+
+ if (e.dataTransfer.files.length) {
+ const file = e.dataTransfer.files[0];
+ handleResumeFile(file);
+ }
+ });
+
+ // Handle file selection from the regular input
+ resumeUpload.addEventListener('change', function(e) {
+ if (this.files.length) {
+ handleResumeFile(this.files[0]);
+ }
+ });
+
+ function handleResumeFile(file) {
+ const validTypes = [
+ 'application/pdf',
+ 'application/msword',
+ 'application/wps-office.docx',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'image/jpeg',
+ 'image/png',
+ 'text/plain'
+ ];
+
+ if (!validTypes.includes(file.type)) {
+ alert('Please upload a valid file type (PDF, Word, Image, or Text)');
+ return;
+ }
+
+ currentResumeFile = file;
+
+ // Hide placeholder
+ resumePlaceholder.style.display = 'none';
+
+ // Set up download link
+ const fileURL = URL.createObjectURL(file);
+ downloadResume.href = fileURL;
+ downloadResume.download = file.name;
+ removeResumeBtn.style.display = 'block';
+
+ // Check file type and show appropriate preview
+ if (file.type === 'application/pdf') {
+ // PDF preview
+ resumeIframe.src = fileURL;
+ resumeIframe.style.display = 'block';
+ resumeImage.style.display = 'none';
+ unsupportedFormat.style.display = 'none';
+ } else if (file.type.match('image.*')) {
+ // Image preview
+ resumeImage.src = fileURL;
+ resumeImage.style.display = 'block';
+ resumeIframe.style.display = 'none';
+ unsupportedFormat.style.display = 'none';
+ } else {
+ // Unsupported format for preview
+ unsupportedFormat.style.display = 'flex';
+ resumeIframe.style.display = 'none';
+ resumeImage.style.display = 'none';
+ }
+
+ // Update the actual resume-upload input
+ const dataTransfer = new DataTransfer();
+ dataTransfer.items.add(file);
+ resumeUpload.files = dataTransfer.files;
+ }
+ }
+
+ function populateCandidateForm(resumeData) {
+ // Add CSS for visual feedback
+ const style = document.createElement('style');
+ style.textContent = `
+ .populated-field {
+ background-color: #f5f5f5 !important;
+ transition: background-color 0.3s ease;
+ }
+ .select2-populated .select2-selection--multiple {
+ background-color: #f5f5f5 !important;
+ }
+ .select2-populated .select2-selection__choice {
+ background-color: #e8f5e9 !important;
+ border-color: #c8e6c9 !important;
+ }
+ `;
+ document.head.appendChild(style);
+
+ // Helper function to set value with visual feedback
+ function setValueWithFeedback(elementId, value) {
+ const element = document.getElementById(elementId);
+ if (element && value) {
+ element.value = value;
+ element.classList.add('populated-field');
+
+ // Special handling for Select2 if this element uses it
+ if ($(element).hasClass('select2-hidden-accessible')) {
+ $(element).next('.select2-container')
+ .find('.select2-selection')
+ .addClass('populated-field');
+ }
+ return true;
+ }
+ return false;
+ }
+
+ // Section 1: Basic Information
+ if (resumeData.personal_info) {
+ const personal = resumeData.personal_info;
+
+ // Set values with visual feedback
+ setValueWithFeedback('partner-name', personal.name);
+ setValueWithFeedback('email', personal.email);
+ setValueWithFeedback('phone', personal.phone);
+ setValueWithFeedback('linkedin', personal.linkedin);
+
+ // If any of these fields are Select2 elements, ensure they get styled
+ ['partner-name', 'email', 'phone', 'linkedin'].forEach(id => {
+ const el = document.getElementById(id);
+ if (el && el.value && $(el).hasClass('select2-hidden-accessible')) {
+ $(el).next('.select2-container')
+ .find('.select2-selection')
+ .addClass('populated-field');
+ }
+ });
+ }
+
+ // Skills - Handle Select2 with background color change
+ if (resumeData.skills?.length) {
+ const skillValues = resumeData.skills.map(skill => skill.id);
+ $('#candidate-skills')
+ .val(skillValues)
+ .trigger('change')
+ .addClass('populated-field');
+
+ // Add class to Select2 container for styling
+ $('#candidate-skills').next('.select2-container')
+ .find('.select2-selection--multiple')
+ .addClass('select2-populated');
+ }
+
+ // Show notification
+ showNotification('Resume uploaded and fields populated successfully!', 'success');
+ }
+
+ function showNotification(message, type) {
+ // Check if notification container exists, create if not
+ let notificationContainer = document.getElementById('notification-container');
+ if (!notificationContainer) {
+ notificationContainer = document.createElement('div');
+ notificationContainer.id = 'notification-container';
+ notificationContainer.style.position = 'fixed';
+ notificationContainer.style.top = '20px';
+ notificationContainer.style.right = '20px';
+ notificationContainer.style.zIndex = '9999';
+ document.body.appendChild(notificationContainer);
+ }
+
+ // Create notification element
+ const notification = document.createElement('div');
+ notification.className = `alert alert-${type} alert-dismissible fade show`;
+ notification.innerHTML = `
+ ${message}
+
+ `;
+
+ // Add to container
+ notificationContainer.appendChild(notification);
+
+ // Auto remove after 5 seconds
+ setTimeout(() => {
+ notification.classList.remove('show');
+ setTimeout(() => {
+ notification.remove();
+ }, 300);
+ }, 5000);
+ }
+}
+
+function createNewCandidate(form, modal) {
+ if (!form.checkValidity()) {
+ form.reportValidity();
+ return;
+ }
+
+ const formData = new FormData();
+
+ // Basic Information
+ formData.append('sequence', document.getElementById('candidate-sequence').value);
+ formData.append('partner_name', document.getElementById('partner-name').value);
+
+ // Add image file if selected
+ const avatarUpload = document.getElementById('avatar-upload');
+ if (avatarUpload.files.length > 0) {
+ formData.append('image_1920', avatarUpload.files[0]);
+ }
+
+ // Contact Information
+ formData.append('email', document.getElementById('email').value);
+ formData.append('phone', document.getElementById('phone').value);
+ formData.append('mobile', document.getElementById('alternate-phone').value);
+ formData.append('linkedin_profile', document.getElementById('linkedin').value);
+ formData.append('type_id', document.getElementById('type').value);
+ formData.append('user_id', document.getElementById('manager').value);
+ formData.append('availability', document.getElementById('availability').value);
+
+ // Skills
+ const skillOptions = document.getElementById('candidate-skills').selectedOptions;
+ for (let i = 0; i < skillOptions.length; i++) {
+ formData.append('skill_ids', skillOptions[i].value);
+ }
+
+ // Add resume file if selected
+ const resumeUpload = document.getElementById('resume-upload');
+ if (resumeUpload.files.length > 0) {
+ formData.append('resume_file', resumeUpload.files[0]);
+ }
+
+ // Additional fields
+ formData.append('active', 'true');
+ formData.append('color', '0');
+
+ fetch('/myATS/candidate/create', {
+ method: 'POST',
+ body: formData,
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest',
+ }
+ })
+ .then(async (response) => {
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.error || response.statusText);
+ }
+ return response.json();
+ })
+ .then(data => {
+ if (data.success) {
+ modal.classList.remove('show');
+ document.body.style.overflow = '';
+ form.reset();
+ alert('Candidate created successfully!');
+
+ // Refresh the candidates list
+ fetch('/myATS/page/candidates', {
+ headers: { "X-Requested-With": "XMLHttpRequest" }
+ })
+ .then(res => res.text())
+ .then(html => {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const newCandidate = doc.querySelector('.candidates-list-left');
+ const existingCandidatesList = document.querySelector('.candidates-list-left');
+ if (newCandidate && existingCandidatesList) {
+ existingCandidatesList.innerHTML = newCandidate.innerHTML;
+ initCandidatesPage();
+ }
+ });
+ } else {
+ throw new Error(data.error || 'Failed to save Candidate');
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ alert("Error saving changes: " + error.message);
+ });
+}
+
+function initCandidateDetailEdit() {
+
+ // Improved smooth scroll navigation
+ document.querySelectorAll('.nav-link').forEach(link => {
+ link.addEventListener('click', function(e) {
+ e.preventDefault();
+ const targetId = this.getAttribute('href');
+ const targetElement = document.querySelector(targetId);
+ if (targetElement) {
+ scrollToTarget(targetElement);
+ // Highlight effect
+ targetElement.style.boxShadow = '0 0 0 3px rgba(13, 110, 253, 0.5)';
+ targetElement.style.transition = 'box-shadow 0.3s ease';
+ setTimeout(() => {
+ targetElement.style.boxShadow = 'none';
+ }, 2000);
+ }
+ });
+ });
+}
+
+// Initialize the page when DOM is ready
+document.addEventListener('DOMContentLoaded', initCandidatesPage);
\ No newline at end of file
diff --git a/addons_extensions/hr_recruitment_web_app/static/src/js/job_requests.js b/addons_extensions/hr_recruitment_web_app/static/src/js/job_requests.js
new file mode 100644
index 000000000..357407470
--- /dev/null
+++ b/addons_extensions/hr_recruitment_web_app/static/src/js/job_requests.js
@@ -0,0 +1,2005 @@
+// Define all your page initialization functions first
+function initJobListPage() {
+ console.log("Job List Page JS Loaded");
+ const jobDetailArea = document.getElementById("job-detail");
+ const headerBadges = document.querySelectorAll(".stat-toggle");
+ const container = document.querySelector('.ats-list-container');
+ const sidebar = document.getElementById("job-list-panel");
+ const toggleBtn = document.getElementById("job-list-sidebar-toggle-btn");
+ let originalContent;
+
+ // Initially hide the job detail panel
+ jobDetailArea.style.display = 'none';
+
+ // Badge click handler (unchanged)
+ headerBadges.forEach(badge => {
+ badge.addEventListener("click", function() {
+ this.classList.toggle("active");
+ this.classList.toggle("crossed");
+ const type = this.dataset.type;
+ document.querySelectorAll(`.job-badge[data-type="${type}"]`).forEach(jobBadge => {
+ jobBadge.style.display = this.classList.contains("active") ? "inline-block" : "none";
+ });
+ const activeJob = document.querySelector(".job-item.selected");
+ if (!this.classList.contains("active")) {
+ // Handle inactive state
+ } else if (activeJob) {
+ // Handle active state
+ }
+ });
+ });
+
+ // Job item click logic
+ document.querySelectorAll(".job-item").forEach(item => {
+ item.addEventListener("click", function() {
+ document.querySelectorAll(".job-item.selected").forEach(el => el.classList.remove("selected"));
+ this.classList.add("selected");
+
+ // Show job detail panel and adjust layout
+ jobDetailArea.style.display = 'block';
+ container.classList.add('ats-selected');
+ sidebar.classList.remove('collapsed');
+ toggleBtn.style.display = 'flex';
+
+ const jobId = this.dataset.id;
+ fetch(`/myATS/job/detail/${jobId}`, {
+ headers: { "X-Requested-With": "XMLHttpRequest" }
+ })
+ .then(res => res.text())
+ .then(html => {
+ if (jobDetailArea) {
+ jobDetailArea.innerHTML = html;
+ initSmartButtons();
+
+// initJobDetailEdit(); // Initialize edit functionality
+ initEditCardButtons();
+ // Add close button functionality
+ const closeBtn = jobDetailArea.querySelector('.close-detail');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', function() {
+ jobDetailArea.style.display = 'none';
+ container.classList.remove('ats-selected');
+ document.querySelectorAll(".job-item.selected").forEach(el => el.classList.remove("selected"));
+ });
+ }
+ }
+ });
+ });
+ });
+
+ // Search functionality (unchanged)
+ const search = document.getElementById("ats-search");
+ if (search) {
+ search.addEventListener("input", function () {
+ const query = this.value.toLowerCase();
+ let visibleCount = 0;
+ document.querySelectorAll(".ats-item").forEach(item => {
+ const match = item.textContent.toLowerCase().includes(query);
+ item.style.display = match ? "" : "none";
+ if (match) visibleCount++;
+ });
+ const countElement = document.getElementById("active-records-count");
+ if (countElement) {
+ countElement.textContent = visibleCount;
+ }
+ });
+ }
+
+ // Sidebar Toggle
+ if (toggleBtn && sidebar) {
+ toggleBtn.addEventListener("click", function () {
+ sidebar.classList.toggle("collapsed");
+ // Change the button icon based on state
+ if (sidebar.classList.contains("collapsed")) {
+ this.textContent = "☰";
+ this.style.right = "-12px";
+ this.style.transform = "translateY(-50%)";
+ } else {
+ this.textContent = "☰";
+ this.style.right = "-12px";
+ }
+ });
+ }
+
+ initJDModal();
+}
+
+let editor = null;
+
+// Initialize edit functionality
+function initEditCardButtons() {
+ document.querySelectorAll('.edit-card-btn').forEach(btn => {
+ btn.addEventListener('click', function() {
+ const cardId = this.dataset.card;
+ const card = document.getElementById(`ats-${cardId}`);
+ if (!card) return;
+
+ // Store original content
+ originalContent = card.innerHTML;
+
+ // Get the appropriate edit form template
+ let formTemplateId = '';
+ let formTitle = '';
+ switch(cardId) {
+ case 'basic-info':
+ formTemplateId = 'basic_info_edit_form';
+ formTitle = 'Basic Information';
+ break;
+ case 'requirements':
+ formTemplateId = 'requirements_edit_form';
+ formTitle = 'Requirements';
+ break;
+ case 'request-info':
+ formTemplateId = 'request_info_edit_form';
+ formTitle = 'Request Information';
+ break;
+ case 'team':
+ formTemplateId = 'team_edit_form';
+ formTitle = 'Recruitment Team';
+ break;
+ case 'description':
+ formTemplateId = 'description_edit_form';
+ formTitle = 'Job Description';
+ break;
+ default:
+ return;
+ }
+
+ // Get the template content
+ const template = document.getElementById(formTemplateId);
+ if (!template) return;
+
+ // Clone the template content
+ const formContent = template.content.cloneNode(true);
+
+ // Create the edit form container
+ const editFormContainer = document.createElement('div');
+ editFormContainer.className = 'edit-form-container';
+
+ // Create form header
+ const formHeader = document.createElement('div');
+ formHeader.className = 'form-header';
+ formHeader.innerHTML = `
+ Edit ${formTitle}
+
+
+
+
+
+
+
+
+ `;
+
+ // Create form content container
+ const formContentContainer = document.createElement('div');
+ formContentContainer.className = 'form-content';
+ formContentContainer.appendChild(formContent);
+
+ // Assemble the edit form
+ editFormContainer.appendChild(formHeader);
+ editFormContainer.appendChild(formContentContainer);
+
+ // Replace card content with edit form
+ card.innerHTML = '';
+ card.appendChild(editFormContainer);
+
+ // Store reference to the button for later use
+ const editButton = this;
+
+ // Initialize Select2 and CKEditor first
+ setTimeout(() => {
+ initSelect2ForEditForm(card);
+
+ // Initialize CKEditor for description
+ if (cardId === 'description') {
+ initDescriptionEditor(card);
+ }
+
+ // Now populate the form fields after initialization
+ populateFormFields(card, editButton);
+ }, 100);
+
+ // Add event listeners for save and cancel buttons
+ card.querySelector('.save-edit-btn').addEventListener('click', function() {
+ saveCardEdit(cardId, card, originalContent);
+ });
+
+ card.querySelector('.cancel-edit-btn').addEventListener('click', function() {
+ debugger;
+ card.innerHTML = originalContent;
+ // Re-initialize edit button
+ initEditCardButtons();
+ originalContent = null;
+ });
+ });
+ });
+}
+
+// New function to populate form fields with data from button data attributes
+// New function to populate form fields with data from button data attributes
+function populateFormFields(card, button) {
+ const cardId = button.dataset.card;
+ switch(cardId) {
+ case 'basic-info':
+ // Populate basic info fields
+ const employmentType = card.querySelector('#edit-employment-type');
+ if (employmentType && button.dataset.employmentType) {
+ employmentType.value = button.dataset.employmentType;
+ $(employmentType).trigger('change'); // Trigger change for Select2
+ }
+ const budget = card.querySelector('#edit-budget');
+ if (budget && button.dataset.budget) {
+ budget.value = button.dataset.budget;
+ }
+ const targetFrom = card.querySelector('#edit-target-from');
+ if (targetFrom && button.dataset.targetFrom) {
+ targetFrom.value = button.dataset.targetFrom;
+ }
+ const targetTo = card.querySelector('#edit-target-to');
+ if (targetTo && button.dataset.targetTo) {
+ targetTo.value = button.dataset.targetTo;
+ }
+ const noOfPositions = card.querySelector('#edit-no-of-positions');
+ if (noOfPositions && button.dataset.noOfPositions) {
+ noOfPositions.value = button.dataset.noOfPositions;
+ }
+ const eligibleSubmissions = card.querySelector('#edit-eligible-submissions');
+ if (eligibleSubmissions && button.dataset.eligibleSubmissions) {
+ eligibleSubmissions.value = button.dataset.eligibleSubmissions;
+ }
+ break;
+ case 'requirements':
+ // Populate requirements fields
+ const experience = card.querySelector('#edit-experience');
+ if (experience && button.dataset.experience) {
+ experience.value = button.dataset.experience;
+ $(experience).trigger('change'); // Trigger change for Select2
+ }
+ const primarySkills = card.querySelector('#edit-primary-skills');
+ if (primarySkills && button.dataset.primarySkills) {
+ const skills = button.dataset.primarySkills.split(',');
+ $(primarySkills).val(skills).trigger('change'); // Set values and trigger change for Select2
+ }
+ const secondarySkills = card.querySelector('#edit-secondary-skills');
+ if (secondarySkills && button.dataset.secondarySkills) {
+ const skills = button.dataset.secondarySkills.split(',');
+ $(secondarySkills).val(skills).trigger('change'); // Set values and trigger change for Select2
+ }
+ const locations = card.querySelector('#edit-locations');
+ if (locations && button.dataset.locations) {
+ const locationIds = button.dataset.locations.split(',');
+ $(locations).val(locationIds).trigger('change'); // Set values and trigger change for Select2
+ }
+ break;
+ case 'request-info':
+ // Populate request info fields
+ const requestedBy = card.querySelector('#edit-requested-by');
+ if (requestedBy && button.dataset.requestedBy) {
+ requestedBy.value = button.dataset.requestedBy;
+ $(requestedBy).trigger('change'); // Trigger change for Select2
+ }
+ const department = card.querySelector('#edit-department');
+ if (department && button.dataset.department) {
+ department.value = button.dataset.department;
+ $(department).trigger('change'); // Trigger change for Select2
+ }
+ const company = card.querySelector('#edit-company');
+ if (company && button.dataset.company) {
+ company.value = button.dataset.company;
+ $(company).trigger('change'); // Trigger change for Select2
+ }
+ break;
+ case 'team':
+ // Populate team fields
+ const primaryRecruiter = card.querySelector('#edit-primary-recruiter');
+ if (primaryRecruiter && button.dataset.primaryRecruiter) {
+ primaryRecruiter.value = button.dataset.primaryRecruiter;
+ $(primaryRecruiter).trigger('change'); // Trigger change for Select2
+ }
+ const secondaryRecruiters = card.querySelector('#edit-secondary-recruiters');
+ if (secondaryRecruiters && button.dataset.secondaryRecruiters) {
+ const recruiters = button.dataset.secondaryRecruiters.split(',');
+ $(secondaryRecruiters).val(recruiters).trigger('change'); // Set values and trigger change for Select2
+ }
+ break;
+ case 'description':
+ // Populate description field
+ const descriptionEditor = card.querySelector('#edit-description-editor');
+ const descriptionTextarea = card.querySelector('#edit-description');
+ if (descriptionEditor && descriptionTextarea && button.dataset.description) {
+ descriptionTextarea.value = button.dataset.description;
+ // Set content after CKEditor is initialized
+ setTimeout(() => {
+ if (descriptionEditor.editorInstance) {
+ descriptionEditor.editorInstance.setData(button.dataset.description);
+ } else {
+ descriptionEditor.innerHTML = button.dataset.description;
+ }
+ }, 200);
+ }
+ break;
+ }
+}
+
+// Initialize Select2 for edit form fields
+function initSelect2ForEditForm(card) {
+ // Initialize all select elements with Select2
+ card.querySelectorAll('select').forEach(select => {
+ if (!select.classList.contains('select2-hidden-accessible')) {
+ $(select).select2({
+ width: '100%',
+ dropdownParent: $(card),
+ placeholder: select.getAttribute('data-placeholder') || 'Select an option'
+ });
+ }
+ });
+}
+
+// Initialize CKEditor for description field
+function initDescriptionEditor(card) {
+ const editorElement = card.querySelector('#edit-description-editor');
+ if (!editorElement) return;
+
+ ClassicEditor
+ .create(editorElement, {
+ toolbar: {
+ items: [
+ 'heading', '|',
+ 'bold', 'italic', 'link', 'bulletedList', 'numberedList', '|',
+ 'blockQuote', '|',
+ 'undo', 'redo'
+ ]
+ }
+ })
+ .then(editor => {
+ // Store editor instance
+ editorElement.editorInstance = editor;
+
+ // Update hidden textarea when editor content changes
+ editor.model.document.on('change:data', () => {
+ const textarea = card.querySelector('#edit-description');
+ if (textarea) {
+ textarea.value = editor.getData();
+ }
+ });
+ })
+ .catch(error => {
+ console.error('Error initializing CKEditor:', error);
+ });
+}
+
+// Save card edit
+function saveCardEdit(cardId, card, originalContent) {
+ const jobId = document.querySelector('.job-detail-container').dataset.jobId;
+ if (!jobId) return;
+
+ // Collect form data based on card type
+ let formData = {
+ id: jobId,
+ card_type: cardId
+ };
+
+ switch(cardId) {
+ case 'basic-info':
+ formData.employment_type = card.querySelector('#edit-employment-type').value;
+ formData.budget = card.querySelector('#edit-budget').value;
+ formData.target_from = card.querySelector('#edit-target-from').value;
+ formData.target_to = card.querySelector('#edit-target-to').value;
+ formData.no_of_positions = card.querySelector('#edit-no-of-positions').value;
+ formData.eligible_submissions = card.querySelector('#edit-eligible-submissions').value;
+ break;
+
+ case 'requirements':
+ formData.experience = card.querySelector('#edit-experience').value;
+ formData.primary_skills = Array.from(card.querySelector('#edit-primary-skills').selectedOptions).map(option => option.value);
+ formData.secondary_skills = Array.from(card.querySelector('#edit-secondary-skills').selectedOptions).map(option => option.value);
+ formData.locations = Array.from(card.querySelector('#edit-locations').selectedOptions).map(option => option.value);
+ break;
+
+ case 'request-info':
+ formData.requested_by = card.querySelector('#edit-requested-by').value;
+ formData.department = card.querySelector('#edit-department').value;
+ formData.company = card.querySelector('#edit-company').value;
+ break;
+
+ case 'team':
+ formData.primary_recruiter = card.querySelector('#edit-primary-recruiter').value;
+ formData.secondary_recruiters = Array.from(card.querySelector('#edit-secondary-recruiters').selectedOptions).map(option => option.value);
+ break;
+
+ case 'description':
+ const editorElement = card.querySelector('#edit-description-editor');
+ if (editorElement.editorInstance) {
+ formData.description = editorElement.editorInstance.getData();
+ } else {
+ formData.description = card.querySelector('#edit-description').value;
+ }
+ break;
+ }
+
+ // Send data to server
+ fetch('/myATS/job/update_card', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ body: JSON.stringify(formData)
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ // Refresh the job detail view
+ fetch(`/myATS/job/detail/${jobId}`, {
+ headers: { "X-Requested-With": "XMLHttpRequest" }
+ })
+ .then(res => res.text())
+ .then(html => {
+ const jobDetailArea = document.getElementById('job-detail');
+ if (jobDetailArea) {
+ jobDetailArea.innerHTML = html;
+ initSmartButtons();
+
+ // Re-initialize all functionality
+// initJobDetailEdit();
+ initEditCardButtons();
+ initSmartButtons();
+ }
+ });
+ } else {
+ alert('Error updating card: ' + (data.error || 'Unknown error'));
+ // Restore original content
+ card.innerHTML = originalContent;
+ initEditCardButtons();
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ alert('Error updating card');
+ // Restore original content
+ card.innerHTML = originalContent;
+ initEditCardButtons();
+ });
+}
+
+function initJobDetailEdit() {
+ const editBtn = document.getElementById('edit-job-btn');
+ const cancelBtn = document.getElementById('cancel-edit');
+ const editForm = document.getElementById('job-edit-form');
+ const viewSection = document.getElementById('job-detail-view');
+ const editSection = document.getElementById('job-detail-edit');
+
+ initEditCardButtons();
+
+ if (editBtn) {
+ editBtn.addEventListener('click', function() {
+ viewSection.style.display = 'none';
+ editSection.style.display = 'block';
+
+ // Initialize CKEditor
+ if (typeof CKEDITOR !== 'undefined') {
+ if (editor) {
+ editor.destroy();
+ }
+ editor = CKEDITOR.replace('job-description-edit', {
+ toolbar: [
+ { name: 'basicstyles', items: ['Bold', 'Italic', 'Underline', 'Strike', '-', 'RemoveFormat'] },
+ { name: 'paragraph', items: ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent'] },
+ { name: 'links', items: ['Link', 'Unlink'] },
+ { name: 'document', items: ['Source'] }
+ ],
+ height: 300
+ });
+ }
+ });
+ }
+
+ if (cancelBtn) {
+ cancelBtn.addEventListener('click', function () {
+ const jobId = document.getElementById('job-edit-form').dataset.id;
+
+ fetch(`/myATS/job/detail/${jobId}`, {
+ headers: { "X-Requested-With": "XMLHttpRequest" }
+ })
+ .then(res => res.text())
+ .then(html => {
+ const jobDetailArea = document.getElementById('job-detail');
+ if (jobDetailArea) {
+ jobDetailArea.innerHTML = html;
+// initJobDetailEdit(); // re-init the edit handlers
+ }
+ });
+
+ if (editor) {
+ editor.destroy();
+ editor = null;
+ }
+ });
+ }
+
+
+ if (editForm) {
+ editForm.addEventListener('submit', function(e) {
+ e.preventDefault();
+ saveJobDetails(this);
+ });
+ }
+
+ // Improved smooth scroll navigation
+ document.querySelectorAll('.nav-link').forEach(link => {
+ link.addEventListener('click', function(e) {
+ e.preventDefault();
+ const targetId = this.getAttribute('href');
+ const targetElement = document.querySelector(targetId);
+ if (targetElement) {
+ scrollToTarget(targetElement);
+ // Highlight effect
+ targetElement.style.boxShadow = '0 0 0 3px rgba(13, 110, 253, 0.5)';
+ targetElement.style.transition = 'box-shadow 0.3s ease';
+ setTimeout(() => {
+ targetElement.style.boxShadow = 'none';
+ }, 2000);
+ }
+ });
+ });
+
+ initSmartButtons();
+}
+
+function saveJobDetails(form) {
+ // Get CKEditor data properly
+ let description = '';
+ if (editor && CKEDITOR.instances['job-description-edit']) {
+ description = CKEDITOR.instances['job-description-edit'].getData();
+ } else {
+ description = document.getElementById('job-description-edit')?.innerHTML || '';
+ }
+
+ const jobId = form.dataset.id;
+ const formData = {
+ 'id': jobId,
+ 'job_id': form.querySelector('#job-id-edit').value,
+ 'job_sequence': form.querySelector('#job-sequence-edit').value,
+ 'category': form.querySelector('#job-category-edit').value,
+ 'description': description,
+ };
+
+ fetch('/myATS/job/save', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ body: JSON.stringify(formData)
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ return response.json();
+ })
+ .then(data => {
+ if (data.success) {
+ // Update view with new data
+ document.getElementById('job-name-view').textContent = formData.name;
+ document.getElementById('job-description-view').innerHTML = formData.description;
+
+ const selectedOption = document.querySelector('#job-category-edit option:checked');
+ if (selectedOption) {
+ document.getElementById('job-category-view').textContent = selectedOption.textContent;
+ }
+
+ document.getElementById('job-detail-view').style.display = 'block';
+ document.getElementById('job-detail-edit').style.display = 'none';
+
+ if (editor) {
+ editor.destroy();
+ editor = null;
+ }
+
+ alert("Changes saved successfully");
+ fetch('/myATS/page/job_requests', {
+ headers: { "X-Requested-With": "XMLHttpRequest" }
+ })
+ .then(res => res.text())
+ .then(html => {
+ // Replace only the left panel to avoid wiping out the whole page
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const newJobList = doc.querySelector('.job-list-left');
+ const existingJobList = document.querySelector('.job-list-left');
+
+ if (newJobList && existingJobList) {
+ existingJobList.innerHTML = newJobList.innerHTML;
+ initJobListPage(); // re-bind event listeners
+ }
+ });
+ } else {
+ throw new Error(data.error || 'Unknown error occurred');
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ alert("Error saving changes: " + error.message);
+ });
+}
+
+let wysiwyg = null;
+
+
+function createJobDetails(form, modal) {
+
+ if (!form.checkValidity()) {
+ form.reportValidity();
+ return;
+ }
+
+ // Get editor content
+ let description = '';
+ if (wysiwyg) {
+ description = wysiwyg.getData();
+ } else {
+ description = null
+// description = document.getElementById('job-description').value || '';
+ }
+
+ // Collect form data
+ const formData = {
+ // Basic Information
+ sequence: document.getElementById('job-sequence').value,
+ position_id: document.getElementById('job-position').value,
+ category: document.getElementById('job-category').value,
+ priority: document.getElementById('job-priority').value,
+ work_type: document.querySelector('input[name="work-type"]:checked').value,
+ client_company_id: document.getElementById('client-company').value,
+ client_id: document.getElementById('client-id').value,
+
+ // Employment Details
+ employment_type_id: document.getElementById('employment-type').value,
+ budget: document.getElementById('job-budget').value,
+ no_of_positions: document.getElementById('job-no-of-positions').value,
+ eligible_submissions: document.getElementById('job-eligible-submissions').value,
+ target_from: document.getElementById('target-from').value,
+ target_to: document.getElementById('target-to').value,
+
+ // Skills & Requirements
+ primary_skill_ids: Array.from(document.getElementById('job-primary-skills').selectedOptions)
+ .map(option => option.value),
+ secondary_skill_ids: Array.from(document.getElementById('job-secondary-skills').selectedOptions)
+ .map(option => option.value),
+ experience_id: document.getElementById('job-experience').value,
+
+ // Recruitment Team
+ primary_recruiter_id: document.getElementById('primary-recruiter').value,
+ secondary_recruiter_ids: Array.from(document.getElementById('secondary-recruiter').selectedOptions)
+ .map(option => option.value),
+
+ // Additional Information
+ location_ids: Array.from(document.getElementById('job-locations').selectedOptions)
+ .map(option => option.value),
+ stage_ids: Array.from(document.getElementById('recruitment-stages').selectedOptions)
+ .map(option => option.value),
+
+ // Description
+ description: description,
+
+ // Attachments (if any)
+ attachment_ids: []
+ };
+
+ fetch('/myATS/job/create', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'Accept': 'application/json'
+ },
+ body: JSON.stringify(formData)
+ }).then(async (response) => {
+ const contentType = response.headers.get("content-type");
+ const responseData = contentType && contentType.includes("application/json")
+ ? await response.json()
+ : await response.text();
+
+ if (!response.ok) {
+ // For 500 errors, check if we have a JSON response with error details
+ if (response.status === 500 && typeof responseData === 'object') {
+ throw new Error(responseData.error || responseData.message || "Internal server error");
+ } else if (typeof responseData === 'string') {
+ throw new Error(responseData);
+ } else {
+ throw new Error(response.statusText);
+ }
+ }
+
+ return responseData;
+ })
+ .then(data => {
+ if (data.success) {
+ modal.classList.remove('show');
+ document.body.style.overflow = '';
+ form.reset();
+
+ if (wysiwyg) {
+ wysiwyg.destroy()
+ .then(() => {
+ wysiwyg = null;
+ })
+ .catch(error => {
+ console.error('Error destroying editor:', error);
+ });
+ }
+
+ alert('Job created successfully!');
+ fetch('/myATS/page/job_requests', {
+ headers: { "X-Requested-With": "XMLHttpRequest" }
+ })
+ .then(res => res.text())
+ .then(html => {
+ // Replace only the left panel to avoid wiping out the whole page
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const newJobList = doc.querySelector('.job-list-left');
+ const existingJobList = document.querySelector('.job-list-left');
+
+ if (newJobList && existingJobList) {
+ existingJobList.innerHTML = newJobList.innerHTML;
+ initJobListPage(); // re-bind event listeners
+ }
+ });
+ } else {
+ throw new Error(data.error || 'Failed to save job');
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ alert("Error saving changes: " + error.message);
+ });
+}
+
+
+// Add this function to your existing JS
+async function initJDModal() {
+ console.log('Initializing JD modal');
+ const modal = document.getElementById('add-jd-modal');
+ const addJdBtn = document.getElementById('add-jd-create-btn');
+ const closeBtns = document.querySelectorAll('.jd-modal-close, .btn-cancel');
+ const saveBtn = document.getElementById('save-jd');
+ const form = document.getElementById('jd-form');
+ const uploadBtn = document.getElementById('upload-jd')
+
+ if (!modal) return;
+
+ if (addJdBtn) {
+ addJdBtn.addEventListener('click', async function(e) {
+ e.preventDefault();
+ modal.classList.add('show');
+ document.body.style.overflow = 'hidden';
+
+ // Initialize WYSIWYG editor
+ const editorElement = document.getElementById('job-description-editor');
+
+// editorElement.innerHTML = document.getElementById('job-description').value || '';
+
+ // Replace your current CKEditor initialization with this:
+ // Replace your CKEditor initialization with this:
+ if (editorElement && !wysiwyg) {
+ try {
+ await loadAssets([
+ '/hr_recruitment_web_app/static/src/libs/ckeditor/build/ckeditor.js'
+ ]);
+
+ ClassicEditor
+ .create(editorElement, {
+ toolbar: {
+ items: [
+ 'heading', '|',
+ 'bold', 'italic', 'link', 'bulletedList', 'numberedList', '|',
+ 'blockQuote', 'uploadImage', '|',
+ 'undo', 'redo'
+ ]
+ },
+ image: {
+ toolbar: [
+ 'imageTextAlternative',
+ 'toggleImageCaption',
+ 'imageStyle:inline',
+ 'imageStyle:block',
+ 'imageStyle:side'
+ ],
+ upload: {
+ types: ['jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff']
+ }
+ },
+ simpleUpload: {
+ uploadUrl: '/web_editor/attachment/add',
+ withCredentials: true,
+ headers: {
+ 'X-CSRF-TOKEN': getCookie('csrftoken') || '',
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ additionalRequestData: {
+ 'res_model': 'hr.job',
+ 'res_id': 0,
+ 'callback': 'window.top'
+ }
+ },
+ typing: {
+ undo: true
+ },
+ keystrokes: [
+ [ 32, 'insertText', ' ' ] // Handle spacebar
+ ]
+ })
+ .then(editor => {
+ wysiwyg = editor;
+ const textarea = document.getElementById('job-description');
+
+ editor.model.document.on('change:data', () => {
+ document.getElementById('job-description').value = editor.getData();
+ });
+
+ if (textarea) {
+ textarea.addEventListener('input', () => {
+ if (wysiwyg) {
+ wysiwyg.setData(textarea.value);
+ }
+ });
+ }
+ })
+ .catch(error => {
+ console.error('Error initializing CKEditor:', error);
+ });
+ } catch (error) {
+ console.error('Error loading CKEditor:', error);
+ }
+ }
+
+ setTimeout(initSelect2, 100);
+ });
+ }
+
+ // Close modal handlers
+ closeBtns.forEach(btn => {
+ btn.addEventListener('click', function(e) {
+ e.preventDefault();
+ modal.classList.remove('show');
+ document.body.style.overflow = '';
+ form.reset();
+
+ if (wysiwyg) {
+ wysiwyg.destroy()
+ .then(() => {
+ wysiwyg = null;
+ })
+ .catch(error => {
+ console.error('Error destroying editor:', error);
+ });
+ }
+ });
+ });
+
+ modal.addEventListener('click', function(e) {
+ if (e.target === modal) {
+ modal.classList.remove('show');
+ document.body.style.overflow = '';
+ form.reset();
+ if (wysiwyg) {
+ wysiwyg.destroy()
+ .then(() => {
+ wysiwyg = null;
+ })
+ .catch(error => {
+ console.error('Error destroying editor:', error);
+ });
+ }
+ }
+ });
+
+ // Save JD
+ if (saveBtn) {
+ saveBtn.addEventListener('click', function (e) {
+ e.preventDefault();
+ createJobDetails(form, modal);
+ });
+ }
+
+ if (uploadBtn) {
+ uploadBtn.addEventListener('click', function (e) {
+ e.preventDefault();
+ // Create file input element
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.accept = '.pdf,.doc,.docx,.txt';
+
+ fileInput.onchange = async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ // Show loading state
+ const button = document.getElementById('upload-jd');
+ const originalText = button.innerHTML;
+ button.innerHTML = ' Processing JD...';
+ button.disabled = true;
+
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ // Call your Odoo endpoint
+ const response = await fetch('/jd/upload', {
+ method: 'POST',
+ body: formData,
+ credentials: 'same-origin'
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ // Map all fields from the parsed result to the form
+ populateJDForm(result);
+
+ // Show notification
+ showNotification('JD uploaded and fields populated successfully!', 'success');
+
+ } catch (error) {
+ console.error('Error parsing JD:', error);
+ showNotification('Failed to parse JD. Please try again or enter manually.', 'danger');
+ } finally {
+ button.innerHTML = originalText;
+ button.disabled = false;
+ }
+ };
+
+ fileInput.click();
+ });
+ }
+
+ function populateJDForm(jdData) {
+ // Section 1: Basic Information
+ if (jdData.sequence) {
+ document.getElementById('job-sequence').value = jdData.sequence;
+ }
+
+ // Job Position - try to match with existing options
+ if (jdData.job_title) {
+ const jobSelect = document.getElementById('job-position');
+ const options = jobSelect.options;
+ let found = false;
+
+ // First try exact match
+ for (let i = 0; i < options.length; i++) {
+ if (options[i].text.toLowerCase() === jdData.job_title.toLowerCase()) {
+ jobSelect.value = options[i].value;
+ $(jobSelect).trigger('change');
+ found = true;
+ break;
+ }
+ }
+
+ // If not found, try partial match
+ if (!found) {
+ for (let i = 0; i < options.length; i++) {
+ if (options[i].text.toLowerCase().includes(jdData.job_title.toLowerCase()) ||
+ jdData.job_title.toLowerCase().includes(options[i].text.toLowerCase())) {
+ jobSelect.value = options[i].value;
+ $(jobSelect).trigger('change');
+ break;
+ }
+ }
+ }
+ }
+
+ // Set work type based on employment type
+ if (jdData.employment_type) {
+ const employmentType = jdData.employment_type.toLowerCase();
+ if (employmentType.includes('client') || employmentType.includes('external')) {
+ document.getElementById('work-type-external').checked = true;
+ } else {
+ document.getElementById('work-type-internal').checked = true;
+ }
+ }
+
+ // Section 2: Employment Details
+ if (jdData.target_start_date) {
+ document.getElementById('target-from').value = jdData.target_start_date;
+ }
+ if (jdData.target_end_date) {
+ document.getElementById('target-to').value = jdData.target_end_date;
+ }
+
+ // Set employment type dropdown
+ if (jdData.employment_type) {
+ const empTypeSelect = document.getElementById('employment-type');
+ const options = empTypeSelect.options;
+ for (let i = 0; i < options.length; i++) {
+ if (options[i].text.toLowerCase().includes(jdData.employment_type.toLowerCase())) {
+ empTypeSelect.value = options[i].value;
+ break;
+ }
+ }
+ }
+
+ // Set experience dropdown
+ if (jdData.experience) {
+ document.getElementById('job-experience').value = jdData.experience.id;
+ $(document.getElementById('job-experience')).trigger('change');
+ }
+
+ // Section 3: Skills & Requirements
+ if (jdData.primary_skills && jdData.primary_skills.length) {
+ const skillValues = jdData.primary_skills.map(skill => skill.id);
+ $('#job-primary-skills').val(skillValues).trigger('change');
+ }
+
+ if (jdData.secondary_skills && jdData.secondary_skills.length) {
+ const skillValues = jdData.secondary_skills.map(skill => skill.id);
+ $('#job-secondary-skills').val(skillValues).trigger('change');
+ }
+
+ // Section 5: Additional Information
+ if (jdData.locations && jdData.locations.length) {
+ const locationValues = jdData.locations.map(loc => loc.id);
+ $('#job-locations').val(locationValues).trigger('change');
+ }
+
+ if (jdData.description) {
+ setTimeout(() => {
+ const editor = document.getElementById('job-description-editor');
+ const textarea = document.getElementById('job-description');
+
+ if (!editor || !textarea) {
+ console.error('Editor or textarea element not found!');
+ return;
+ }
+
+ const lines = jdData.description.split('\n').map(l => l.trim()).filter(l => l);
+ let formatted = '';
+ let inBulletList = false;
+
+ lines.forEach(line => {
+ const numberedMatch = line.match(/^(\d+\.\s+)(.*)/);
+ const bulletMatch = line.match(/^([•\-○]\s*)(.*)/);
+ const titleMatch = line.match(/^(.+?):\s*(.*)/);
+
+ if (numberedMatch) {
+ // Close bullet list if previously opened
+ if (inBulletList) {
+ formatted += '';
+ inBulletList = false;
+ }
+ formatted += `${numberedMatch[1]} ${numberedMatch[2]}
`;
+ } else if (bulletMatch) {
+ if (!inBulletList) {
+ formatted += '';
+ inBulletList = true;
+ }
+ formatted += `${bulletMatch[2]} `;
+ } else if (titleMatch) {
+ if (inBulletList) {
+ formatted += ' ';
+ inBulletList = false;
+ }
+ formatted += `${titleMatch[1]}: ${titleMatch[2]}
`;
+ } else {
+ // Treat as bullet if short or point-like
+ if (line.length < 150 && /^[A-Za-z0-9].*/.test(line)) {
+ if (!inBulletList) {
+ formatted += '';
+ inBulletList = true;
+ }
+ formatted += `${line} `;
+ } else {
+ if (inBulletList) {
+ formatted += ' ';
+ inBulletList = false;
+ }
+ formatted += `${line}
`;
+ }
+ }
+ });
+
+ // Close any open
+ if (inBulletList) {
+ formatted += ' ';
+ }
+
+ editor.innerHTML = formatted;
+ textarea.value = formatted;
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
+
+ console.log('Formatted:', formatted);
+ }, 300);
+ }
+
+ }
+
+ function showNotification(message, type) {
+ // Using Odoo's notification system
+ if (typeof Notification !== 'undefined') {
+ Notification.create({
+ title: type === 'success' ? 'Success' : 'Error',
+ message: message,
+ type: type,
+ sticky: false
+ });
+ } else {
+ // Fallback to alert
+ alert(`${type.toUpperCase()}: ${message}`);
+ }
+ }
+
+ function getCookie(name) {
+ let cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ const cookies = document.cookie.split(';');
+ for (let i = 0; i < cookies.length; i++) {
+ const cookie = cookies[i].trim();
+ if (cookie.substring(0, name.length + 1) === (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+ }
+
+ // Helper function to load WYSIWYG assets
+ function loadAssets(assets) {
+ return Promise.all(assets.map(asset => {
+ return new Promise((resolve, reject) => {
+ const isCss = asset.endsWith('.css');
+ const element = isCss
+ ? document.createElement('link')
+ : document.createElement('script');
+
+ if (isCss) {
+ element.rel = 'stylesheet';
+ element.href = asset;
+ element.type = 'text/css';
+ } else {
+ element.src = asset;
+ element.type = 'text/javascript';
+ }
+
+ element.onload = resolve;
+ element.onerror = () => reject(new Error(`Failed to load ${asset}`));
+ document.head.appendChild(element);
+ });
+ }));
+ }
+
+
+ // Populate categories (example)
+// const categorySelect = document.getElementById('job-category');
+// if (categorySelect) {
+// // You might want to fetch these from your backend
+// const categories = ['IT', 'Marketing', 'Finance', 'HR', 'Operations'];
+// categories.forEach(cat => {
+// const option = document.createElement('option');
+// option.value = cat.toLowerCase();
+// option.textContent = cat;
+// categorySelect.appendChild(option);
+// });
+// }
+
+ const workTypeRadios = document.querySelectorAll("input[name='work-type']");
+ const companySelect = document.getElementById("client-company");
+ const clientSelect = document.getElementById("client-id");
+
+ let clientToCompanyMap = {}; // New map
+
+ function initSelect2() {
+ // Check if Select2 is already initialized
+ const primarySkillsSelect = document.getElementById('job-primary-skills');
+ if (primarySkillsSelect) {
+ $(primarySkillsSelect).select2({
+ placeholder: 'Select skills',
+ allowClear: true,
+ dropdownParent: $('.new-jd-container'),
+ width: '100%',
+ escapeMarkup: function(m) { return m; }
+ });
+ }
+ const jobPosition = document.getElementById('job-position');
+ if (jobPosition) {
+ $(jobPosition).select2({
+ placeholder: 'Select Job Position',
+ dropdownParent: $('.new-jd-container'),
+ width: '100%',
+ escapeMarkup: function(m) { return m; }
+ });
+ }
+
+ const clientCompany = document.getElementById('client-company');
+ if (clientCompany) {
+ $(clientCompany).select2({
+ placeholder: 'Select Client Company',
+ dropdownParent: $('.new-jd-container'),
+ width: '100%',
+ escapeMarkup: function(m) { return m; }
+ });
+ }
+
+ const clientId = document.getElementById('client-id');
+ if (clientId) {
+ $(clientId).select2({
+ placeholder: 'Select client',
+ dropdownParent: $('.new-jd-container'),
+ width: '100%',
+ escapeMarkup: function(m) { return m; }
+ });
+ }
+
+ const secondarySkillSelect = document.getElementById('job-secondary-skills');
+ if (secondarySkillSelect) {
+ $(secondarySkillSelect).select2({
+ placeholder: 'Select skills',
+ allowClear: true,
+ dropdownParent: $('.new-jd-container'),
+ width: '100%',
+ escapeMarkup: function(m) { return m; }
+ });
+ }
+
+ const primaryRecruiterSelect = document.getElementById('primary-recruiter');
+ if (primaryRecruiterSelect) {
+ $(primaryRecruiterSelect).select2({
+ placeholder: 'Select Primary Recruiters',
+ allowClear: true,
+ templateResult: formatUserOption,
+ templateSelection: formatUserSelection,
+ escapeMarkup: function(m) { return m; }
+ });
+ }
+
+ const secondaryRecruiterSelect = document.getElementById('secondary-recruiter');
+ if (secondaryRecruiterSelect) {
+ $(secondaryRecruiterSelect).select2({
+ placeholder: 'Select Secondary Recruiters',
+ allowClear: true,
+ templateResult: formatUserOption,
+ templateSelection: formatUserSelection,
+ dropdownParent: $('.new-jd-container'),
+ width: '100%',
+ escapeMarkup: function(m) { return m; }
+ });
+ }
+
+ const jobLocationsSelect = document.getElementById('job-locations');
+ if (jobLocationsSelect) {
+ $(jobLocationsSelect).select2({
+ placeholder: 'Select Job Locations',
+ allowClear: true,
+ dropdownParent: $('.new-jd-container'),
+ width: '100%',
+ escapeMarkup: function(m) { return m; }
+ });
+ }
+
+ const RecruitmentStagesSelect = document.getElementById('recruitment-stages');
+ if (RecruitmentStagesSelect) {
+ $(RecruitmentStagesSelect).select2({
+ placeholder: 'Select Recruitment Stages',
+ allowClear: true,
+ dropdownParent: $('.new-jd-container'),
+ width: '100%',
+ escapeMarkup: function(m) { return m; }
+ });
+ }
+
+ initSkillsDragAndDrop();
+ }
+
+ function setPrimarySkills(skills, remove = false) {
+ const $select = $('#job-primary-skills');
+ if ($select.hasClass("select2-hidden-accessible")) {
+ let current = $select.val() || [];
+ if (remove) {
+ // Remove the specified skills
+ current = current.filter(id => !skills.includes(id));
+ } else {
+ // Add the specified skills (avoid duplicates)
+ current = Array.from(new Set(current.concat(skills)));
+ }
+ $select.val(current).trigger('change');
+ } else {
+ setTimeout(() => setPrimarySkills(skills, remove), 100);
+ }
+ }
+
+ function setSecondarySkills(skills, remove = false) {
+ const $select = $('#job-secondary-skills');
+ if ($select.hasClass("select2-hidden-accessible")) {
+ let current = $select.val() || [];
+ if (remove) {
+ // Remove the specified skills
+ current = current.filter(id => !skills.includes(id));
+ } else {
+ // Add the specified skills (avoid duplicates)
+ current = Array.from(new Set(current.concat(skills)));
+ }
+ $select.val(current).trigger('change');
+ } else {
+ setTimeout(() => setSecondarySkills(skills, remove), 100);
+ }
+ }
+
+ function isSkillInOptions(skillId, selectId) {
+ return $(`#${selectId} option[value="${skillId}"]`).length > 0;
+ }
+ function isSkillSelected(skillId, selectId) {
+ const selector = `#${selectId}`;
+ const selectedValues = $(selector).val() || [];
+ return selectedValues.includes(skillId.toString());
+ }
+ // Add this new function
+ function initSkillsDragAndDrop() {
+ const skillRequirementsDiv = document.getElementById('skill_requirements');
+ const primarySkills = document.getElementById('job-primary-skills');
+ const secondarySkills = document.getElementById('job-secondary-skills');
+
+ if (!skillRequirementsDiv || !primarySkills || !secondarySkills) return;
+
+ // Make options draggable
+ [primarySkills, secondarySkills].forEach(select => {
+ // Set draggable attribute on all options
+ Array.from(select.options).forEach(option => {
+ option.setAttribute('draggable', 'true');
+ });
+
+ select.addEventListener('dragstart', function(e) {
+ if (e.target.tagName === 'OPTION') {
+ console.log('Drag started for option:', e.target.value);
+ e.dataTransfer.setData('text/plain', e.target.value);
+ e.dataTransfer.effectAllowed = 'move';
+ e.target.classList.add('dragging');
+ }
+ });
+
+ select.addEventListener('dragend', function(e) {
+ if (e.target.tagName === 'OPTION') {
+ console.log('Drag ended for option:', e.target.value);
+ e.target.classList.remove('dragging');
+ }
+ });
+ });
+
+ // Set up drop zones for both selects
+ [primarySkills, secondarySkills].forEach(select => {
+ const select2Container = $(`#${select.id}`).next('.select2-container');
+ const select2Selection = select2Container.find('.select2-selection--multiple');
+ console.log(select2Container);
+ console.log(select2Selection);
+ // Handle dragover for the Select2 container
+ select2Selection.on('dragover', function(e) {
+ e.preventDefault();
+ e.originalEvent.dataTransfer.dropEffect = 'move';
+ $(this).addClass('drag-over');
+ console.log('Dragging over select:', select.id);
+ return false;
+ });
+
+ // Handle dragleave for the Select2 container
+ select2Selection.on('dragleave', function(e) {
+ console.log("drag leave feature");
+ $(this).removeClass('drag-over');
+ return false;
+ });
+
+ // Handle drop for the Select2 container
+ select2Selection.on('drop', function(e) {
+ console.log("dropping the data");
+ e.preventDefault();
+ e.stopPropagation();
+ $(this).removeClass('drag-over');
+
+ const skillId = e.originalEvent.dataTransfer.getData('text/plain');
+ const option = document.querySelector(`option[value="${skillId}"]`);
+ console.log('Moving option from', option.parentElement.id, 'to', select.id);
+
+ console.log('Dropped skill ID:', skillId);
+
+ if (!isSkillInOptions(skillId, select.id)) {
+ console.log('Skill does not exists in the selected dropdown');
+ return;
+ }
+
+
+ // If the option is already in this select, do nothing
+ if (isSkillSelected(skillId, select.id)) {
+ console.log('Skill is already selected.');
+ return;
+ }
+
+
+ // Remove from current select
+ const oldSelect = option.parentElement;
+ if (select.id == 'job-primary-skills'){
+ setSecondarySkills(skillId, remove=true)
+ setPrimarySkills(skillId)
+ } else {
+ setPrimarySkills(skillId, remove=true)
+ setSecondarySkills(skillId)
+ }
+
+
+
+ // Trigger Select2 updates
+ $(oldSelect).trigger('change');
+ $(select).trigger('change');
+
+ console.log('Option moved successfully');
+ return false;
+ });
+ });
+
+ // Prevent default behavior for the parent container
+ skillRequirementsDiv.addEventListener('dragover', function(e) {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'none';
+ return false;
+ });
+
+ skillRequirementsDiv.addEventListener('drop', function(e) {
+ console.log("drop feature");
+ e.preventDefault();
+ return false;
+ });
+
+ // Make Select2 choices draggable
+ $(document).on('mouseenter', '.select2-selection__choice', function() {
+ this.setAttribute('draggable', 'true');
+ this.addEventListener('dragstart', function(e) {
+ console.log("dragstarted");
+ const value = $(this).attr('title') || $(this).text().trim();
+ console.log('Dragging choice:', value);
+ const option = $(primarySkills).find(`option:contains('${value}')`)[0] ||
+ $(secondarySkills).find(`option:contains('${value}')`)[0];
+ if (option) {
+ console.log('Found matching option:', option.value);
+ e.dataTransfer.setData('text/plain', option.value);
+ e.dataTransfer.effectAllowed = 'move';
+ } else {
+ console.log('No matching option found for:', value);
+ }
+ });
+ });
+ }
+
+ function formatUserOption(user) {
+ if (!user.id) return user.text;
+ var imageUrl = $(user.element).data('image') || '/web/static/img/placeholder.png';
+ var $container = $(
+ '' +
+ ' ' +
+ '' + user.text + ' ' +
+ ' '
+ );
+ return $container;
+ }
+
+ function formatUserSelection(user) {
+ if (!user.id) return user.text;
+ var imageUrl = $(user.element).data('image') || '/web/static/img/placeholder.png';
+ var $container = $(
+ '' +
+ ' ' +
+ '' + user.text + ' ' +
+ ' '
+ );
+ return $container;
+ }
+
+ function fetchCompanies(workType) {
+ fetch('/get_client_companies', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ type: workType })
+ })
+ .then(res => res.json())
+ .then(data => {
+ const companies = data.result;
+ if (!Array.isArray(companies)) {
+ console.error("Expected an array, got:", companies);
+ return;
+ }
+
+ companySelect.innerHTML = 'Select Client Company ';
+ companies.forEach(company => {
+ companySelect.innerHTML += `${company.name} `;
+ });
+
+ clientSelect.innerHTML = 'Select Client ';
+ clientToCompanyMap = {}; // Reset map
+ })
+ .catch(err => {
+ console.error("Error fetching companies:", err);
+ });
+ }
+
+ function fetchClients(companyId, workType) {
+ fetch('/get_clients_by_company', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ company_id: companyId, type: workType })
+ })
+ .then(res => res.json())
+ .then(data => {
+ const clients = data.result;
+ if (!Array.isArray(clients)) {
+ console.error("Expected an array, got:", clients);
+ return;
+ }
+
+ clientSelect.innerHTML = 'Select Client ';
+ clientToCompanyMap = {}; // Reset map
+
+ clients.forEach(client => {
+ clientSelect.innerHTML += `${client.name} `;
+ clientToCompanyMap[client.id] = client.company_id; // Assuming `company_id` comes from backend
+ });
+ });
+ }
+
+
+ // Initial fetch on page load
+ const initialType = document.querySelector("input[name='work-type']:checked").value;
+ fetchCompanies(initialType);
+ fetchClients(null, initialType);
+
+
+ // Change on radio click
+ workTypeRadios.forEach(radio => {
+ console.log("radio clicked");
+ radio.addEventListener("change", () => {
+ fetchCompanies(radio.value);
+ fetchClients(companyId = null, workType = radio.value);
+
+ });
+ });
+
+ companySelect.addEventListener("change", () => {
+ let selectedCompanyId = null;
+ if (companySelect.value){
+ selectedCompanyId = companySelect.value;
+ }
+ clientSelect.innerHTML = 'Select Client ';
+ const selectedType = document.querySelector("input[name='work-type']:checked").value;
+ fetchClients(selectedCompanyId, selectedType);
+ });
+
+ // On client selection
+ clientSelect.addEventListener("change", () => {
+ const selectedClientId = clientSelect.value;
+
+ if (selectedClientId && clientToCompanyMap[selectedClientId]) {
+ const companyId = clientToCompanyMap[selectedClientId];
+ companySelect.value = companyId;
+ }
+ });
+}
+
+// Track open popups
+const openPopups = [];
+
+function initSmartButtons() {
+ // Matching Candidates button
+ document.getElementById('matching-jd-candidates')?.addEventListener('click', function() {
+ togglePopup('matchingCandidates', this.dataset.popupTitle || 'Matching Candidates');
+ });
+
+ // Applicants button
+ document.getElementById('jd-applicants')?.addEventListener('click', function() {
+ togglePopup('applicants', this.dataset.popupTitle || 'Applicants');
+ });
+
+ // Close buttons (using event delegation)
+ document.addEventListener('click', function(e) {
+ if (e.target.classList.contains('popup-close')) {
+ const popup = e.target.closest('.popup-container');
+ closePopup(popup.id.replace('Popup', ''));
+ }
+ });
+}
+
+function togglePopup(type, title) {
+ const popupId = `${type}Popup`;
+ let popup = document.getElementById(popupId);
+
+ if (!popup) {
+ // Create popup if it doesn't exist
+ popup = document.createElement('div');
+ popup.id = popupId;
+ popup.className = 'popup-container bottom-right';
+ popup.innerHTML = document.getElementById(`${type}_popup`).innerHTML;
+ document.body.appendChild(popup);
+ }
+
+ if (popup.classList.contains('visible')) {
+ closePopup(type);
+ } else {
+ openPopup(type, title);
+ }
+}
+
+function openPopup(type, title) {
+ const popupId = `${type}Popup`;
+ const popup = document.getElementById(popupId);
+
+ if (!popup) return;
+
+ // Update title
+ const titleEl = popup.querySelector('h5');
+ if (titleEl) titleEl.textContent = title;
+
+ // Show loading state
+ const loadingEl = popup.querySelector('.loading-spinner');
+ const contentEl = popup.querySelector('.popup-content');
+
+ if (loadingEl) {
+ loadingEl.style.display = 'flex';
+ loadingEl.innerHTML = '
';
+ }
+ if (contentEl) {
+ contentEl.style.display = 'none';
+ contentEl.innerHTML = '';
+ }
+
+ // Show popup
+ popup.classList.add('visible');
+
+ // Add to open popups array if not already there
+ if (!openPopups.includes(popupId)) {
+ openPopups.push(popupId);
+ }
+
+ // Position popups
+ positionPopups();
+ // Fetch data
+ const jobId = document.querySelector('.job-detail-container')?.dataset?.jobId;
+ if (!jobId) {
+ console.error('No job ID found');
+ return;
+ }
+
+ const endpoint = type === 'matchingCandidates'
+ ? `/myATS/job/matching_candidates/${jobId}`
+ : `/myATS/job/applicants/${jobId}`;
+
+ fetch(endpoint, {
+ headers: { "X-Requested-With": "XMLHttpRequest" }
+ })
+ .then(response => {
+ if (!response.ok) throw new Error('Network response was not ok');
+ return response.text();
+ })
+ .then(html => {
+ if (loadingEl) loadingEl.style.display = 'none';
+ if (contentEl) {
+ contentEl.innerHTML = html;
+ contentEl.style.display = 'block';
+ initMatchingCandidates();
+ }
+ })
+ .catch(error => {
+ console.error('Error loading popup content:', error);
+ if (loadingEl) {
+ loadingEl.innerHTML = `
+
+ Failed to load data. Please try again.
+
+ `;
+ }
+ });
+}
+
+function closePopup(type) {
+ const popupId = `${type}Popup`;
+ const popup = document.getElementById(popupId);
+
+ if (popup) {
+ popup.classList.remove('visible');
+
+ // Remove from open popups array
+ const index = openPopups.indexOf(popupId);
+ if (index > -1) {
+ openPopups.splice(index, 1);
+ }
+
+ // Reposition remaining popups
+ positionPopups();
+ }
+}
+
+function positionPopups() {
+ // Reset all popup positions first
+ document.querySelectorAll('.popup-container').forEach(popup => {
+ popup.style.right = '';
+ popup.style.bottom = '';
+ });
+
+ // Position each visible popup
+ openPopups.forEach((popupId, index) => {
+ const popup = document.getElementById(popupId);
+ if (popup) {
+ popup.style.right = `${20 + (index * 420)}px`; // 400px width + 20px gap
+ popup.style.bottom = '0';
+ }
+ });
+}
+
+function initMatchingCandidates() {
+ // Handle candidate card clicks
+ document.addEventListener('click', function(e) {
+ const card = e.target.closest('.mc-card');
+ if (card) {
+ debugger;
+ const candidateId = parseInt(card.dataset.id);
+ showCandidateDetails(card);
+ }
+
+ // Close modal
+ if (e.target.classList.contains('mc-close-modal')) {
+ closeCandidateModal();
+ }
+ });
+
+ // Close modal when clicking outside
+ window.addEventListener('click', function(e) {
+ const modal = document.getElementById('candidateDetailModal');
+ if (e.target === modal) {
+ closeCandidateModal();
+ }
+ });
+}
+
+function showCandidateDetails(card) {
+ const modalContent = document.getElementById('candidateDetailContent');
+
+ // Parse the applications data
+ let applications = [];
+ try {
+ applications = JSON.parse(card.dataset.applications.replace(/'/g, '"'));
+ } catch (e) {
+ console.error('Error parsing applications:', e);
+ }
+
+ // Parse skills data
+ let primarySkills = [];
+ let secondarySkills = [];
+ try {
+ primarySkills = card.dataset.matchPrimarySkills ?
+ JSON.parse(card.dataset.matchPrimarySkills.replace(/'/g, '"')) : [];
+ secondarySkills = card.dataset.matchSecondarySkills ?
+ JSON.parse(card.dataset.matchSecondarySkills.replace(/'/g, '"')) : [];
+ } catch (e) {
+ console.error('Error parsing skills:', e);
+ }
+
+ // Create the HTML structure
+ modalContent.innerHTML = `
+
+
+
+
+
Contact Information
+
+
+
+ Recruiter:
+
+ ${card.dataset.manager || 'Unassigned'}
+
+
+
+
+
+
Skills Match
+
+
+
Primary Skills (${parseInt(card.dataset.primaryPercent) || 0}% Match)
+ ${primarySkills.length > 0 ?
+ `
+ ${primarySkills.map(skill => `${skill} `).join('')}
+ ` :
+ '
No primary skills matched
'}
+
+
+
Secondary Skills (${parseInt(card.dataset.secondaryPercent) || 0}% Match)
+ ${secondarySkills.length > 0 ?
+ `
+ ${secondarySkills.map(skill => `${skill} `).join('')}
+ ` :
+ '
No secondary skills matched
'}
+
+
+
+
+
+
Applications
+ ${applications.length > 0 ?
+ `
+ ${applications.map(app => `
+
+ ${app[0] || 'Untitled Application'}
+ ${app[1]}
+
+ `).join('')}
+
` :
+ '
No applications found
'}
+
+
+ `;
+
+ // Show modal
+ document.getElementById('candidateDetailModal').style.display = 'block';
+}
+
+function renderCandidateDetail(data) {
+ const detailContainer = document.getElementById('candidateDetailContent');
+ // Here you would create the HTML structure based on your template
+ // and populate it with the data received from the server
+ detailContainer.innerHTML = `
+
+
+
+
+ `;
+ // You would continue with all the other fields from your template
+}
+
+function closeCandidateModal() {
+ document.getElementById('candidateDetailModal').style.display = 'none';
+}
+
+//
+//// Add this function to initialize the popup functionality
+//function initSmartButtons() {
+// document.querySelectorAll('.smart-button').forEach(button => {
+// console.log("event-exist");
+// button.addEventListener('click', function() {
+// console.log("event-clicked");
+// const jobId = document.querySelector('.job-detail-container').getAttribute('data-job-id');
+// const popupType = this.dataset.popupType;
+// const popupTitle = this.dataset.popupTitle;
+// console.log(jobId);
+// console.log(popupType);
+// console.log(popupTitle);
+// showSmartPopup(jobId, popupType, popupTitle);
+// });
+// });
+//}
+//
+//function showSmartPopup(jobId, popupType, popupTitle) {
+// // Create or get the canvas element
+// debugger;
+// let canvasId = popupType + 'Canvas';
+// let canvas = document.getElementById(canvasId);
+//
+// if (!canvas) {
+// // Create the canvas from template if it doesn't exist
+// const template = document.getElementById(popupType + '_popup');
+// if (template) {
+// const clone = template.content.cloneNode(true);
+// document.body.appendChild(clone);
+// canvas = document.getElementById(canvasId);
+// }
+// }
+//
+// if (canvas) {
+// // Initialize the offcanvas if not already initialized
+// if (!canvas._offcanvas) {
+// canvas._offcanvas = new bootstrap.Offcanvas(canvas);
+// }
+//
+// // Show the offcanvas
+// canvas._offcanvas.show();
+//
+// // Load content
+// loadPopupContent(jobId, popupType);
+// }
+//}
+//
+//function loadPopupContent(jobId, popupType) {
+// console.log("loadPopupContent");
+// const canvas = document.getElementById(popupType + 'Canvas');
+// if (!canvas) return;
+//
+// const loadingSpinner = canvas.querySelector('.loading-spinner');
+// const contentArea = canvas.querySelector(`.${popupType.replace('_', '-')}-list`);
+//
+// // Show loading spinner
+// if (loadingSpinner) loadingSpinner.style.display = 'block';
+// if (contentArea) contentArea.innerHTML = '';
+//
+// // Determine the endpoint based on popup type
+// let endpoint = '';
+// if (popupType === 'matchingCandidates') {
+// endpoint = `/myATS/job/${jobId}/matching_candidates`;
+// } else if (popupType === 'applicants') {
+// endpoint = `/myATS/job/${jobId}/applicants`;
+// }
+//
+// if (endpoint) {
+// fetch(endpoint, {
+// headers: { "X-Requested-With": "XMLHttpRequest" }
+// })
+// .then(response => response.text())
+// .then(html => {
+// if (contentArea) {
+// contentArea.innerHTML = html;
+// // Initialize any interactive elements in the popup if needed
+// initPopupInteractiveElements(popupType);
+// }
+// })
+// .catch(error => {
+// console.error(`Error loading ${popupType}:`, error);
+// if (contentArea) {
+// contentArea.innerHTML = `
+//
+// Failed to load ${popupType.replace('_', ' ')}. Please try again.
+//
+// `;
+// }
+// })
+// .finally(() => {
+// if (loadingSpinner) loadingSpinner.style.display = 'none';
+// });
+// }
+//}
+//
+//function initPopupInteractiveElements(popupType) {
+// // Initialize any interactive elements specific to each popup type
+// if (popupType === 'matching_candidates') {
+// // Add event listeners for matching candidates popup
+// document.querySelectorAll('.match-candidate-action').forEach(button => {
+// button.addEventListener('click', function() {
+// const candidateId = this.dataset.candidateId;
+// // Handle candidate action
+// });
+// });
+// } else if (popupType === 'applicants') {
+// // Add event listeners for applicants popup
+// document.querySelectorAll('.applicant-action').forEach(button => {
+// button.addEventListener('click', function() {
+// const applicantId = this.dataset.applicantId;
+// // Handle applicant action
+// });
+// });
+// }
+//}
diff --git a/addons_extensions/hr_recruitment_web_app/views/jd.xml b/addons_extensions/hr_recruitment_web_app/views/jd.xml
new file mode 100644
index 000000000..105b2a6c1
--- /dev/null
+++ b/addons_extensions/hr_recruitment_web_app/views/jd.xml
@@ -0,0 +1,1089 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select a job to view details.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Upload JD
+
+
+
+
+
+
+
+
+
+
+
+
+ ×
+
+
+
+
+ Matching Candidates
+
+
+ Applicants
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Employment Type:
+
+
+
+ Budget:
+
+
+
+ Target From:
+
+
+ -
+
+
+
+
+ Number of Positions:
+
+
+
+ Eligible Submissions:
+
+
+
+
+
+
+
+
+
+
+ Experience:
+
+
+
+
Primary Skills:
+
+
+
+
+ Not specified
+
+
+
+
Secondary Skills:
+
+
+
+
+ Not specified
+
+
+
+
Locations:
+
+
+
+
+ Not specified
+
+
+
+
+
+
+
+
+
+
+ Requested By:
+
+
+
+ Department:
+
+
+
+ Company:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No primary recruiter assigned
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No secondary recruiters assigned
+
+
+
+
+
+
+
+
+
+
+
+
+ No description provided for this job.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🔍
+
+ Showing matches ≥ %
+
+
+
+ Active Records ( )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %
+
+ Primary
+
+
+
+ %
+
+ Secondary
+
+
+
+
+
+
+
+
+
+
+
😕
+
No matching candidates found
+
Try lowering the match threshold or expanding your search criteria
+
+
+
+
+
+
+
+
+
+
+
+
+
Applicants for ( )
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/project_task_timesheet_extended/__manifest__.py b/addons_extensions/project_task_timesheet_extended/__manifest__.py
index 57c964663..bfc57f567 100644
--- a/addons_extensions/project_task_timesheet_extended/__manifest__.py
+++ b/addons_extensions/project_task_timesheet_extended/__manifest__.py
@@ -25,6 +25,8 @@ Key Features:
'base',
'analytic',
'project_gantt',
+ 'hr',
+ 'hr_contract',
],
'data': [
'security/security.xml',
@@ -35,6 +37,7 @@ Key Features:
'wizards/project_stage_update_wizard.xml',
'wizards/task_reject_reason_wizard.xml',
'view/teams.xml',
+ 'view/project_stages.xml',
'view/task_stages.xml',
'view/project.xml',
'view/project_task.xml',
diff --git a/addons_extensions/project_task_timesheet_extended/data/data.xml b/addons_extensions/project_task_timesheet_extended/data/data.xml
index 9bcc53817..8a8e9815e 100644
--- a/addons_extensions/project_task_timesheet_extended/data/data.xml
+++ b/addons_extensions/project_task_timesheet_extended/data/data.xml
@@ -12,6 +12,43 @@
+
+ Show/Hide Chatter
+
+
+ action
+ code
+
+ if records:
+ action = records.action_show_project_chatter()
+
+
+
+
+ Show/Hide Chatter
+
+
+ action
+ code
+
+ if records:
+ action = records.action_show_project_task_chatter()
+
+
+
+
+ Enable/Disable Stage Approvals
+
+
+ action
+ code
+
+ if records:
+ action = records.action_assign_approval_flow()
+
+
+
+
@@ -83,4 +120,103 @@
+
+
+ Initial
+ project_sponsor
+
+ 1
+
+
+
+
+ Planning
+ project_manager
+
+ 2
+
+
+
+
+ Architecture & Design
+ project_manager
+
+ 3
+
+
+
+
+ Sprint Planning
+ project_manager
+
+ 4
+
+
+
+
+ Development
+ project_manager
+
+ 5
+
+
+
+
+ Testing & QA
+ project_manager
+
+ 6
+
+
+
+
+ Deployment
+ project_manager
+
+ 7
+
+
+
+
+ Maintenance & Support
+ project_manager
+
+ 8
+
+
+
+
+ Closer
+ project_sponsor
+ 9
+
+
+
+
+ 10
+ To Do
+ False
+
+
+
+ 15
+ In Progress
+ False
+
+
+
+ 20
+ Done
+
+ False
+
+
+
+ 25
+ Cancelled
+
+ project_manager
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/models/__init__.py b/addons_extensions/project_task_timesheet_extended/models/__init__.py
index 7f9bdc21e..cb910db8e 100644
--- a/addons_extensions/project_task_timesheet_extended/models/__init__.py
+++ b/addons_extensions/project_task_timesheet_extended/models/__init__.py
@@ -1,7 +1,15 @@
from . import teams
+from . import project_sprint
+from . import task_documents
+from . import project_architecture_design
+from . import project_risk
+from . import project_resource_cost
+from . import project_costings
+from . import project_code_commit
+from . import project_stages
from . import task_stages
from . import project
from . import project_task
from . import timesheets
# from . import project_task_gantt
-from . import user_availability
\ No newline at end of file
+from . import user_availability
diff --git a/addons_extensions/project_task_timesheet_extended/models/project_architecture_design.py b/addons_extensions/project_task_timesheet_extended/models/project_architecture_design.py
new file mode 100644
index 000000000..7a455934b
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/project_architecture_design.py
@@ -0,0 +1,29 @@
+from odoo import api, Command, fields, models
+
+class ProjectArchitectureDesign(models.Model):
+ _name = "project.architecture.design"
+ _description = "Architecture & Design"
+
+ project_id = fields.Many2one("project.project", required=True)
+
+ # System Architecture
+ architecture_diagram = fields.Binary(string="Architecture Diagram")
+ tech_stack = fields.Text(string="Tech Stack")
+ architecture_notes = fields.Text(string="Architecture Notes")
+
+ # UI/UX
+ ui_wireframe = fields.Binary(string="UI Wireframe")
+ ux_flow = fields.Binary(string="UX Flow Diagram")
+
+ # Database
+ er_diagram = fields.Binary(string="ER Diagram")
+ database_notes = fields.Text(string="Database Notes")
+
+ # Review
+ design_status = fields.Selection([
+ ('pending', 'Pending'),
+ ('approved', 'Approved'),
+ ('changes_required', 'Changes Required')
+ ], default="pending", string="Design Status")
+
+ reviewer_comments = fields.Text(string="Reviewer Comments")
diff --git a/addons_extensions/project_task_timesheet_extended/models/project_code_commit.py b/addons_extensions/project_task_timesheet_extended/models/project_code_commit.py
new file mode 100644
index 000000000..9422c632a
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/project_code_commit.py
@@ -0,0 +1,17 @@
+from odoo import models, fields
+
+class ProjectCommitStep(models.Model):
+ _name = 'project.commit.step'
+ _description = 'Commit Steps for Task'
+ _order = 'create_date desc'
+
+ task_id = fields.Many2one('project.task', string="Task", ondelete='cascade')
+
+ project_id = fields.Many2one('project.project',related='task_id.project_id')
+ commit_message = fields.Char(string="Commit Message", required=True)
+ commit_code = fields.Html(string="Commit Code")
+ branch_name = fields.Char(string="Branch")
+ files_changed = fields.Text(string="Files Changed")
+ commit_date = fields.Datetime(string="Commit Date", default=fields.Datetime.now)
+ notes = fields.Text(string="Notes")
+ sprint_id = fields.Many2one('project.sprint',related='task_id.sprint_id')
diff --git a/addons_extensions/project_task_timesheet_extended/models/project_costings.py b/addons_extensions/project_task_timesheet_extended/models/project_costings.py
new file mode 100644
index 000000000..27fff33cb
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/project_costings.py
@@ -0,0 +1,46 @@
+from odoo import models, fields, api
+
+# ---------------------------------------
+# MATERIAL COST
+# ---------------------------------------
+class ProjectMaterialCost(models.Model):
+ _name = "project.material.cost"
+ _description = "Material Cost for Project"
+ _order = "id desc"
+
+ project_id = fields.Many2one("project.project", string="Project", required=True)
+ material_name = fields.Char(string="Material Name", required=True)
+
+ qty = fields.Float(string="Quantity", default=1)
+ unit_cost = fields.Float(string="Unit Cost")
+ total_cost = fields.Float(string="Total Cost", compute="_compute_total_cost", store=True)
+
+ remarks = fields.Text(string="Remarks")
+
+ @api.depends("qty", "unit_cost")
+ def _compute_total_cost(self):
+ for rec in self:
+ rec.total_cost = rec.qty * rec.unit_cost if rec.qty and rec.unit_cost else 0
+
+
+# ---------------------------------------
+# EQUIPMENT COST
+# ---------------------------------------
+class ProjectEquipmentCost(models.Model):
+ _name = "project.equipment.cost"
+ _description = "Equipment Cost for Project"
+ _order = "id desc"
+
+ project_id = fields.Many2one("project.project", string="Project", required=True)
+ equipment_name = fields.Char(string="Equipment Name", required=True)
+
+ duration_hours = fields.Float(string="Duration (Hours)", default=1)
+ hourly_rate = fields.Float(string="Hourly Rate")
+ total_cost = fields.Float(string="Total Cost", compute="_compute_total_cost", store=True)
+
+ remarks = fields.Text(string="Remarks")
+
+ @api.depends("duration_hours", "hourly_rate")
+ def _compute_total_cost(self):
+ for rec in self:
+ rec.total_cost = rec.duration_hours * rec.hourly_rate if rec.duration_hours and rec.hourly_rate else 0
diff --git a/addons_extensions/project_task_timesheet_extended/models/project_resource_cost.py b/addons_extensions/project_task_timesheet_extended/models/project_resource_cost.py
new file mode 100644
index 000000000..96a4b9b95
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/project_resource_cost.py
@@ -0,0 +1,71 @@
+from odoo import models, fields, api
+from datetime import date
+
+class ProjectResourceCost(models.Model):
+ _name = "project.resource.cost"
+ _description = "Resource Cost for Project"
+
+ project_id = fields.Many2one("project.project", string="Project", required=True)
+ employee_id = fields.Many2one("hr.employee", string="Employee", required=True)
+
+ monthly_salary = fields.Float(string="Monthly Salary")
+ daily_rate = fields.Float(string="Daily Rate")
+
+ duration_days = fields.Integer(string="Estimated Working Days")
+ total_cost = fields.Float(string="Total Cost")
+
+ start_date = fields.Date(string="Estimated Start Date")
+ end_date = fields.Date(string="Estimated End Date")
+
+ # -------------------------------------------------------------
+ # 1. Get project start & end dates when project is selected
+ # -------------------------------------------------------------
+ @api.onchange('project_id')
+ def _onchange_project(self):
+ """Auto-fill start_date and end_date from project."""
+ if self.project_id:
+ # If project has dates → use them
+ self.start_date = self.project_id.start_date or date.today()
+ self.end_date = self.project_id.end_date or self.start_date
+
+ # -------------------------------------------------------------
+ # 2. If employee selected → load salary
+ # -------------------------------------------------------------
+ @api.onchange('employee_id')
+ def _onchange_employee(self):
+ if self.employee_id:
+ contract = self.employee_id.contract_id
+ if contract:
+ self.monthly_salary = contract.wage or 0.0
+ self.daily_rate = (contract.wage / 30) if contract.wage else 0.0
+
+ # -------------------------------------------------------------
+ # 3. Daily ↔ Monthly salary sync
+ # -------------------------------------------------------------
+ @api.onchange('daily_rate')
+ def _onchange_daily_rate(self):
+ if self.daily_rate:
+ self.monthly_salary = self.daily_rate * 30
+
+ if self.duration_days and self.daily_rate:
+ self.total_cost = self.daily_rate * self.duration_days
+
+ @api.onchange('monthly_salary')
+ def _onchange_monthly_salary(self):
+ if self.monthly_salary:
+ self.daily_rate = self.monthly_salary / 30
+
+ if self.duration_days and self.daily_rate:
+ self.total_cost = self.daily_rate * self.duration_days
+
+ # -------------------------------------------------------------
+ # 4. Date change → update duration and cost
+ # -------------------------------------------------------------
+ @api.onchange('start_date', 'end_date')
+ def _onchange_dates(self):
+ if self.start_date and self.end_date:
+ duration = (self.end_date - self.start_date).days + 1
+ self.duration_days = max(duration, 0)
+
+ if self.duration_days and self.daily_rate:
+ self.total_cost = self.daily_rate * self.duration_days
diff --git a/addons_extensions/project_task_timesheet_extended/models/project_risk.py b/addons_extensions/project_task_timesheet_extended/models/project_risk.py
new file mode 100644
index 000000000..ff79e1b3b
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/project_risk.py
@@ -0,0 +1,28 @@
+from odoo import models, fields
+
+class ProjectRisk(models.Model):
+ _name = "project.risk"
+ _description = "Project Risk Management"
+ _order = "id desc"
+
+ project_id = fields.Many2one(
+ "project.project",
+ string="Project",
+ required=True,
+ ondelete="cascade"
+ )
+
+ risk_description = fields.Text(string="Risk Description", required=True)
+ probability = fields.Selection([
+ ('low', "Low"),
+ ('medium', "Medium"),
+ ('high', "High")
+ ], string="Probability", required=True, default='medium')
+
+ impact = fields.Selection([
+ ('low', "Low"),
+ ('medium', "Medium"),
+ ('high', "High")
+ ], string="Impact", required=True, default='medium')
+
+ mitigation_plan = fields.Text(string="Mitigation Plan")
diff --git a/addons_extensions/project_task_timesheet_extended/models/project_sprint.py b/addons_extensions/project_task_timesheet_extended/models/project_sprint.py
new file mode 100644
index 000000000..16140a366
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/project_sprint.py
@@ -0,0 +1,30 @@
+from odoo import models, fields, api, _
+
+class ProjectSprint(models.Model):
+ _name = "project.sprint"
+ _description = "Project Sprint"
+ _order = "date_start desc"
+
+ project_id = fields.Many2one(
+ "project.project",
+ required=True,
+ ondelete="cascade"
+ )
+
+ sprint_name = fields.Char(string="Sprint Name", required=True)
+ date_start = fields.Date(string="Start Date", required=True)
+ date_end = fields.Date(string="End Date", required=True)
+
+ allocated_hours = fields.Float(string="Allocated Hours")
+
+ status = fields.Selection(
+ [
+ ("draft", "Draft"),
+ ("in_progress", "In Progress"),
+ ("done", "Completed"),
+ ],
+ default="draft",
+ string="Status"
+ )
+ done_date = fields.Date(string="Done Date")
+ note = fields.Text(string="Note")
diff --git a/addons_extensions/project_task_timesheet_extended/models/project_stages.py b/addons_extensions/project_task_timesheet_extended/models/project_stages.py
new file mode 100644
index 000000000..9ec1f4eb9
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/project_stages.py
@@ -0,0 +1,696 @@
+from odoo import api, fields, models, _
+from odoo.exceptions import ValidationError
+from markupsafe import Markup
+from datetime import datetime, timedelta
+import pytz
+
+
+class ProjectStages(models.Model):
+ _inherit = "project.project.stage"
+
+ approval_by = fields.Selection([('project_manager', 'Project Manager'), ('project_sponsor', 'Project Sponsor')])
+
+
+class projectStagesApprovalFlow(models.Model):
+ _name = 'project.stages.approval.flow'
+
+ stage_id = fields.Many2one('project.project.stage')
+ stage_approval_by = fields.Selection(related='stage_id.approval_by')
+ approval_by = fields.Many2one("res.users", domain="[('id','in',approval_by_users)]")
+ assigned_to = fields.Many2one("res.users")
+ assigned_date = fields.Datetime()
+ submission_date = fields.Datetime()
+ project_id = fields.Many2one('project.project')
+ approval_by_users = fields.Many2many('res.users', compute="_compute_all_project_managers")
+ note = fields.Text()
+ manager_level_edit_access = fields.Boolean(compute="_compute_manager_level_edit_access")
+
+ def _compute_manager_level_edit_access(self):
+ for rec in self:
+ if rec.approval_by == self.env.user or rec.project_id.user_id == self.env.user or self.env.user.has_group("project.group_project_manager"):
+ rec.manager_level_edit_access = True
+ else:
+ rec.manager_level_edit_access = False
+ def _compute_all_project_managers(self):
+ # Get the project manager Odoo group
+ group_pm = self.env.ref("project.group_project_manager")
+
+ for rec in self:
+ # Search all users who belong to this group
+ pm_users = []
+
+ if rec.stage_approval_by == 'project_manager' and rec.project_id.user_id:
+ pm_users = rec.project_id.user_id.ids
+ elif rec.stage_approval_by == 'project_sponsor' and rec.project_id.project_sponsor:
+ pm_users = rec.project_id.project_sponsor.ids
+ else:
+ pm_users = self.env['res.users'].sudo().search([
+ ('groups_id', 'in', group_pm.id)
+ ]).ids
+
+ rec.approval_by_users = [(6, 0, pm_users)]
+
+
+class ProjectProject(models.Model):
+ _inherit = 'project.project'
+
+ project_stages = fields.One2many('project.stages.approval.flow', 'project_id')
+ assign_approval_flow = fields.Boolean(default=False)
+ project_sponsor = fields.Many2one('res.users')
+ show_project_chatter = fields.Boolean(default=False)
+ project_vision = fields.Text(
+ string="Project Vision",
+ help="Concise statement describing the project's ultimate goal and purpose"
+ )
+
+ # Requirement Documentation
+ description = fields.Html("Requirement Description")
+ requirement_file = fields.Binary("Requirement Document")
+ requirement_file_name = fields.Char("Requirement File Name")
+
+ # Feasibility Assessment
+ feasibility_html = fields.Html("Feasibility Assessment")
+ feasibility_file = fields.Binary("Feasibility Document")
+ feasibility_file_name = fields.Char("Feasibility File Name")
+
+ manager_level_edit_access = fields.Boolean(compute="_compute_has_manager_level_edit_access")
+ approval_status = fields.Selection([
+ ('submitted', 'Submitted'),
+ ('reject', 'Rejected')
+ ])
+ show_submission_button = fields.Boolean(compute="_compute_access_check")
+ show_approval_button = fields.Boolean(compute="_compute_access_check")
+ show_refuse_button = fields.Boolean(compute="_compute_access_check")
+ show_back_button = fields.Boolean(compute="_compute_access_check")
+ project_activity_log = fields.Html(string="Project Activity Log")
+ project_scope = fields.Html(string="Scope", default=lambda self: """
+ Scope Description
+ 1. In Scope Items?
+ 2. Out Scope Items?
+ """)
+ risk_ids = fields.One2many(
+ "project.risk",
+ "project_id",
+ string="Project Risks"
+ )
+ # stage_id = fields.Many2one(domain="[('id','in',showable_stage_ids or [])]")
+
+ showable_stage_ids = fields.Many2many('project.project.stage',compute='_compute_project_project_stages')
+
+ # fields:
+ estimated_amount = fields.Float(string="Estimated Amount")
+ total_budget_amount = fields.Float(string="Total Budget Amount", compute="_compute_total_budget", store=True)
+
+ # Manpower
+ resource_cost_ids = fields.One2many(
+ "project.resource.cost",
+ "project_id",
+ string="Resource Costs"
+ )
+
+ # Material
+ material_cost_ids = fields.One2many(
+ "project.material.cost",
+ "project_id",
+ string="Material Costs"
+ )
+
+ # Equipment
+ equipment_cost_ids = fields.One2many(
+ "project.equipment.cost",
+ "project_id",
+ string="Equipment Costs"
+ )
+
+ architecture_design_ids = fields.One2many(
+ "project.architecture.design",
+ "project_id",
+ string="Architecture & Design"
+ )
+
+ require_sprint = fields.Boolean(
+ string="Require Sprints?",
+ default=False,
+ help="Enable sprint-based planning for this project."
+ )
+
+ sprint_ids = fields.One2many(
+ "project.sprint",
+ "project_id",
+ string="Project Sprints"
+ )
+
+ commit_step_ids = fields.One2many(
+ 'project.commit.step',
+ 'project_id',
+ string="Commit Steps"
+ )
+
+ development_document_ids = fields.One2many(
+ "task.development.document",
+ "project_id",
+ string="Development Documents"
+ )
+
+ testing_document_ids = fields.One2many(
+ "task.testing.document",
+ "project_id",
+ string="Testing Documents"
+ )
+ development_notes = fields.Html()
+ testing_notes = fields.Html()
+
+ @api.depends('require_sprint')
+ def _compute_project_project_stages(self):
+ for rec in self:
+ stage_ids = self.env['project.project.stage'].sudo().search([('active', '=', True), ('company_id','in',[self.env.company.id,False])]).ids
+
+ if not rec.require_sprint:
+ stage_ids = self.env['project.project.stage'].sudo().search([
+ ('active', '=', True),('company_id','in',[self.env.company.id,False]), ('id', '!=', self.env.ref(
+ "project_task_timesheet_extended.project_project_stage_sprint_planning").id)
+ ]).ids
+ rec.showable_stage_ids = stage_ids
+
+ @api.depends("resource_cost_ids.total_cost", "material_cost_ids.total_cost", "equipment_cost_ids.total_cost")
+ def _compute_total_budget(self):
+ for project in self:
+ project.total_budget_amount = (
+ sum(project.resource_cost_ids.mapped("total_cost"))
+ + sum(project.material_cost_ids.mapped("total_cost"))
+ + sum(project.equipment_cost_ids.mapped("total_cost"))
+ )
+
+ # --------------------------------------------------------
+ # Fetch Resource Data Button
+ # --------------------------------------------------------
+ def action_fetch_resource_data(self):
+ """Fetch all members' employee records and create manpower cost lines with full auto-fill."""
+ for project in self:
+
+ # Project users = members + project manager + project lead
+ users = project.members_ids | project.user_id | project.project_lead
+
+ # Fetch employees linked to those users
+ employees = self.env["hr.employee"].search([("user_id", "in", users.ids)])
+
+ for emp in employees:
+
+ # Avoid duplicate manpower lines
+ existing = project.resource_cost_ids.filtered(lambda r: r.employee_id == emp)
+ if existing:
+ continue
+
+ # Get active contract for salary details
+ contract = emp.contract_id
+
+ monthly_salary = contract.wage if contract else 0.0
+ daily_rate = (contract.wage / 30) if contract and contract.wage else 0.0
+
+ # Project Dates
+ start_date = project.date_start or fields.Date.today()
+ end_date = project.date or start_date
+
+ # Duration
+ duration = (end_date - start_date).days + 1 if start_date and end_date else 0
+
+ # Total Cost
+ total_cost = daily_rate * duration if daily_rate else 0
+
+ # Create manpower line
+ self.env["project.resource.cost"].create({
+ "project_id": project.id,
+ "employee_id": emp.id,
+ "monthly_salary": monthly_salary,
+ "daily_rate": daily_rate,
+ "start_date": start_date,
+ "end_date": end_date,
+ "duration_days": duration,
+ "total_cost": total_cost,
+ })
+
+ @api.depends("project_stages", "stage_id", "approval_status")
+ def _compute_access_check(self):
+ """Compute visibility of action buttons based on user permissions and project state"""
+ for project in self:
+ project.show_submission_button = False
+ project.show_approval_button = False
+ project.show_refuse_button = False
+ project.show_back_button = False
+
+ if not project.assign_approval_flow:
+ continue
+
+ user = self.env.user
+ project_manager = project.user_id
+ project_sponsor = project.project_sponsor
+
+ # Current approval timeline for this stage
+ current_approval_timeline = project.project_stages.filtered(
+ lambda s: s.stage_id == project.stage_id
+ )
+
+ # Compute button visibility based on approval flow
+ if current_approval_timeline:
+ line = current_approval_timeline[0]
+ assigned_to = line.assigned_to
+ responsible_lead = line.approval_by
+
+ # Submission button for assigned users
+ if (assigned_to == user and
+ project.approval_status != "submitted" and
+ assigned_to != responsible_lead):
+ project.show_submission_button = True
+
+ # Approval/refusal buttons for responsible leads
+ if (project.approval_status == "submitted" and
+ responsible_lead == user):
+ project.show_approval_button = True
+ project.show_refuse_button = True
+
+ # Direct approval when no assigned user
+ if not assigned_to and responsible_lead == user:
+ project.show_approval_button = True
+
+ # Direct approval when assigned user is also responsible
+ if (assigned_to == responsible_lead and
+ user == assigned_to):
+ project.show_approval_button = True
+ else:
+ # Managers can approve without specific flow
+ if user.has_group("project.group_project_manager"):
+ project.show_approval_button = True
+
+ # Managers get additional permissions
+ if user in [project_manager] or user.has_group("project.group_project_manager"):
+ project.show_back_button = True
+ if user.has_group("project.group_project_manager"):
+ project.show_approval_button = True
+
+ # Stage-specific button visibility
+ if project.stage_id:
+ stages = self.env['project.project.stage'].search([('id','in',project.showable_stage_ids.ids),
+ ('id', '!=', self.env.ref("project.project_project_stage_3").id)
+ ])
+ if stages:
+ first_stage = stages.sorted('sequence')[0]
+ last_stage = stages.sorted('sequence')[-1]
+
+ if project.stage_id == first_stage:
+ project.show_back_button = False
+ if project.stage_id == last_stage:
+ project.show_submission_button = False
+ project.show_approval_button = False
+ project.show_refuse_button = False
+
+ @api.depends("user_id")
+ def _compute_has_manager_level_edit_access(self):
+ """Determine if current user has manager-level edit permissions"""
+ for rec in self:
+ rec.manager_level_edit_access = (
+ rec.user_id == self.env.user or
+ self.env.user.has_group("project.group_project_manager")
+ )
+
+ def action_show_project_chatter(self):
+ """Toggle visibility of project chatter"""
+ for project in self:
+ project.show_project_chatter = not project.show_project_chatter
+
+ def action_assign_approval_flow(self):
+ """Configure approval flow for project stages"""
+ for project in self:
+ if not project.project_sponsor or not project.user_id:
+ raise ValidationError(_("Sponsor and Manager are required to assign Stage Approvals"))
+
+ project.assign_approval_flow = not project.assign_approval_flow
+
+ if project.assign_approval_flow:
+ # Clear existing records
+ project.project_stages.unlink()
+
+ # Fetch all project stages
+ stages = self.env['project.project.stage'].sudo().search([('id','in',project.showable_stage_ids.ids)])
+
+ for stage in stages:
+ # Determine approval authority based on stage configuration
+ approval_by = (
+ project.user_id.id if stage.sudo().approval_by == 'project_manager' else
+ project.project_sponsor.id if stage.sudo().approval_by == 'project_sponsor' else
+ False
+ )
+
+ self.env['project.stages.approval.flow'].sudo().create({
+ 'project_id': project.id,
+ 'stage_id': stage.id,
+ 'approval_by': approval_by,
+ 'assigned_to': approval_by,
+ 'assigned_date': fields.Datetime.now() if stage == project.stage_id else False,
+ 'submission_date': False,
+ })
+
+ # Log approval flow assignment
+ self.sudo()._add_activity_log("Approval flow assigned by %s" % self.env.user.name)
+ self.sudo()._post_to_project_channel(
+ _("Approval flow configured for project %s") % project.name
+ )
+ else:
+ project.sudo().project_stages.unlink()
+ self.sudo()._add_activity_log("Approval flow removed by %s" % self.env.user.name)
+ self.sudo()._post_to_project_channel(
+ _("Approval flow removed for project %s") % project.name
+ )
+
+ def submit_project_for_approval(self):
+ """Submit project for current stage approval"""
+ for project in self:
+ project.sudo().approval_status = "submitted"
+ current_stage = project.sudo().stage_id
+ current_approval_timeline = project.sudo().project_stages.filtered(
+ lambda s: s.stage_id == project.sudo().stage_id
+ )
+
+ if current_approval_timeline:
+ current_approval_timeline.sudo().submission_date = fields.Datetime.now()
+
+ stage_line = project.sudo().project_stages.filtered(lambda s: s.stage_id == current_stage)
+ responsible_user = stage_line.sudo().approval_by if stage_line else False
+
+ # Create activity log
+ activity_log = "%s : %s submitted for approval to %s" % (
+ current_stage.sudo().name,
+ self.env.user.name,
+ responsible_user.sudo().name if responsible_user else "N/A"
+ )
+ project.sudo()._add_activity_log(activity_log)
+
+ # Post to project channel
+ if responsible_user:
+ channel_message = _("Project %s submitted for approval at stage %s. %s please review.") % (
+ project.sudo().name,
+ current_stage.sudo().name,
+ project.sudo()._create_odoo_mention(responsible_user.partner_id)
+ )
+ else:
+ channel_message = _("Project %s submitted for approval at stage %s") % (
+ project.sudo().name,
+ current_stage.sudo().name
+ )
+ project.sudo()._post_to_project_channel(channel_message)
+
+ # Send notification
+ if responsible_user:
+ project.sudo().message_post(
+ body=activity_log,
+ partner_ids=[responsible_user.sudo().partner_id.id],
+ message_type='notification',
+ subtype_xmlid='mail.mt_comment'
+ )
+
+ def project_proceed_further(self):
+ """Advance project to next stage after approval"""
+ for project in self:
+ current_stage = project.stage_id
+ next_stage = self.env["project.project.stage"].search([
+ ('sequence', '>', project.stage_id.sequence),
+ ('id', '!=', self.env.ref("project.project_project_stage_3").id),
+ ('id', 'in', project.showable_stage_ids.ids),
+ ], order="sequence asc", limit=1)
+
+ current_approval_timeline = project.project_stages.filtered(
+ lambda s: s.stage_id == project.stage_id
+ )
+
+ if current_approval_timeline:
+ current_approval_timeline.submission_date = fields.Datetime.now()
+ if not current_approval_timeline.assigned_date:
+ current_approval_timeline.assigned_date = fields.Datetime.now()
+
+ if next_stage:
+ next_approval_timeline = project.project_stages.filtered(
+ lambda s: s.stage_id == next_stage
+ )
+ if next_approval_timeline and not next_approval_timeline.assigned_date:
+ next_approval_timeline.assigned_date = fields.Datetime.now()
+
+ project.stage_id = next_stage
+ project.approval_status = ""
+
+ # Create activity log
+ activity_log = "%s approved by %s → moved to %s" % (
+ current_stage.name,
+ self.env.user.name,
+ next_stage.name
+ )
+ project._add_activity_log(activity_log)
+
+ # Post to project channel
+ next_user = next_approval_timeline.assigned_to if next_approval_timeline else False
+ if next_user:
+ channel_message = _("Project %s approved at stage %s and moved to %s. %s please proceed.") % (
+ project.name,
+ current_stage.name,
+ next_stage.name,
+ project._create_odoo_mention(next_user.partner_id)
+ )
+ else:
+ channel_message = _("Project %s approved at stage %s and moved to %s") % (
+ project.name,
+ current_stage.name,
+ next_stage.name
+ )
+ project._post_to_project_channel(channel_message)
+
+ # Send notification
+ if next_user:
+ project.message_post(
+ body=activity_log,
+ partner_ids=[next_user.partner_id.id],
+ message_type='notification',
+ subtype_xmlid='mail.mt_comment'
+ )
+ else:
+ # Last stage completed
+ project.approval_status = ""
+ activity_log = "%s fully approved and completed" % project.name
+ project._add_activity_log(activity_log)
+ project._post_to_project_channel(
+ _("Project %s completed and fully approved") % project.name
+ )
+ project.message_post(body=activity_log)
+
+ def reject_and_return(self, reason=None):
+ """Reject project at current stage with optional reason"""
+ for project in self:
+ reason = reason or ""
+ current_stage = project.stage_id
+ project.approval_status = "reject"
+
+ # Create activity log
+ activity_log = "%s rejected by %s — %s" % (
+ current_stage.name,
+ self.env.user.name,
+ reason
+ )
+ project._add_activity_log(activity_log)
+
+ # Update approval timeline
+ current_approval_timeline = project.project_stages.filtered(
+ lambda s: s.stage_id == project.stage_id
+ )
+ if current_approval_timeline:
+ current_approval_timeline.note = f"Reject Reason: {reason}"
+
+ # Post to project channel
+ channel_message = _("Project %s rejected at stage %s. Reason: %s") % (
+ project.name,
+ current_stage.name,
+ reason
+ )
+ project._post_to_project_channel(channel_message)
+
+ # Send notification
+ project.message_post(body=activity_log)
+
+ # Notify responsible users
+ if current_approval_timeline:
+ responsible_user = (
+ current_approval_timeline.assigned_to or
+ current_approval_timeline.approval_by
+ )
+ if responsible_user:
+ project.message_post(
+ body=_("Project %s has been rejected and returned to you") % project.name,
+ partner_ids=[responsible_user.partner_id.id],
+ message_type='notification',
+ subtype_xmlid='mail.mt_comment'
+ )
+
+ def project_back_button(self):
+ """Revert project to previous stage"""
+ for project in self:
+ prev_stage = self.env["project.project.stage"].search([
+ ('sequence', '<', project.stage_id.sequence),
+ ('id', 'in', project.showable_stage_ids.ids)
+ ], order="sequence desc", limit=1)
+
+ if not prev_stage:
+ raise ValidationError(_("No previous stage available."))
+
+ # Create activity log
+ activity_log = "%s reverted back to %s by %s" % (
+ project.stage_id.name,
+ prev_stage.name,
+ self.env.user.name
+ )
+ project._add_activity_log(activity_log)
+
+ # Post to project channel
+ channel_message = _("Project %s reverted from %s back to %s") % (
+ project.name,
+ project.stage_id.name,
+ prev_stage.name
+ )
+ project._post_to_project_channel(channel_message)
+
+ # Update stage
+ project.stage_id = prev_stage
+ project.message_post(body=activity_log)
+
+ def action_open_reject_wizard(self):
+ """Open rejection wizard for projects"""
+ self.ensure_one()
+ return {
+ "type": "ir.actions.act_window",
+ "name": _("Reject Project"),
+ "res_model": "project.reject.reason.wizard",
+ "view_mode": "form",
+ "target": "new",
+ "context": {"default_project_id": self.id},
+ }
+
+ # Activity Log Helper Methods
+ def _get_current_datetime_formatted(self):
+ """Get current datetime in 'DD-MMM-YYYY HH:MM AM/PM' format"""
+ now = fields.Datetime.context_timestamp(self, fields.datetime.now())
+ formatted_date = now.strftime('%d-%b-%Y %I:%M %p').upper()
+ return formatted_date[1:] if formatted_date.startswith('0') else formatted_date
+
+ def _add_activity_log(self, activity_text):
+ """Add formatted entry to project activity log"""
+ formatted_datetime = self._get_current_datetime_formatted()
+ for project in self:
+ log_entry = f"[{formatted_datetime}] {activity_text}"
+ if project.project_activity_log:
+ project.project_activity_log = Markup(project.project_activity_log) + Markup(' ') + Markup(log_entry)
+ else:
+ project.project_activity_log = Markup(log_entry)
+
+ def _post_to_project_channel(self, message_body, mention_partners=None):
+ """Post message to project's discuss channel with proper mentions"""
+ for project in self:
+ if not project.id:
+ continue
+
+ # Get project channel
+ channel = (
+ project.discuss_channel_id or
+ project.default_projects_channel_id
+ )
+
+ if channel:
+ formatted_message = self._format_message_with_odoo_mentions(
+ message_body,
+ mention_partners
+ )
+ channel.message_post(
+ body=Markup(formatted_message),
+ message_type='comment',
+ subtype_xmlid='mail.mt_comment',
+ author_id=self.env.user.partner_id.id
+ )
+
+ def _format_message_with_odoo_mentions(self, message_body, mention_partners=None):
+ """Format message with proper Odoo @mentions"""
+ if not mention_partners:
+ return f'{message_body}
'
+
+ message_parts = ['', message_body]
+ for partner in mention_partners:
+ if partner and partner.name:
+ mention_html = f'
@{partner.name} '
+ message_parts.append(mention_html)
+ message_parts.append('
')
+ return ' '.join(message_parts)
+
+ def _create_odoo_mention(self, partner):
+ """Create Odoo mention link for a partner"""
+ if not partner:
+ return ""
+ return f'@{partner.name} '
+
+
+class ProjectTask(models.Model):
+ _inherit = 'project.task'
+
+
+ def _default_sprint_id(self):
+ """Return the current active (in-progress) sprint of the project."""
+ if 'project_id' in self._context:
+ project_id = self._context.get('project_id')
+ sprint = self.env['project.sprint'].search([
+ ('project_id', '=', project_id),
+ ('status', '=', 'in_progress')
+ ], limit=1)
+ return sprint.id
+ return False
+
+ sprint_id = fields.Many2one(
+ "project.sprint",
+ string="Sprint",
+ default=_default_sprint_id
+ )
+
+ require_sprint = fields.Boolean(
+ related="project_id.require_sprint",
+ store=False
+ )
+
+ commit_step_ids = fields.One2many(
+ 'project.commit.step',
+ 'task_id',
+ string="Commit Steps"
+ )
+
+ show_task_chatter = fields.Boolean(default=False)
+
+ development_document_ids = fields.One2many(
+ "task.development.document",
+ "task_id",
+ string="Development Documents"
+ )
+
+ testing_document_ids = fields.One2many(
+ "task.testing.document",
+ "task_id",
+ string="Testing Documents"
+ )
+
+ @api.onchange("project_id")
+ def _onchange_project_id_sprint_required(self):
+ for task in self:
+ if task.project_id and not task.project_id.require_sprint:
+ task.sprint_id = False
+ else:
+ if task.project_id and task.project_id.require_sprint:
+ sprint = self.env['project.sprint'].search([
+ ('project_id', '=', task.project_id.id),
+ ('status', '=', 'in_progress')
+ ], limit=1)
+ task.sprint_id = sprint.id
+
+
+ def action_show_project_task_chatter(self):
+ """Toggle visibility of project chatter"""
+ for project in self:
+ project.show_task_chatter = not project.show_task_chatter
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/models/task_documents.py b/addons_extensions/project_task_timesheet_extended/models/task_documents.py
new file mode 100644
index 000000000..5f57d93c7
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/task_documents.py
@@ -0,0 +1,44 @@
+from odoo import models, fields
+
+
+# ---------------------------------------------------------
+# Development Documents
+# ---------------------------------------------------------
+class TaskDevelopmentDocument(models.Model):
+ _name = "task.development.document"
+ _description = "Development Documents"
+ _order = "create_date desc"
+
+ task_id = fields.Many2one(
+ "project.task",
+ string="Task",
+ ondelete="cascade"
+ )
+
+ project_id = fields.Many2one("project.project",string="Project",ondelete="cascade")
+
+ name = fields.Char(string="Document Name", required=True)
+ file = fields.Binary(string="Document File", required=True)
+ file_name = fields.Char(string="Filename")
+ notes = fields.Text(string="Notes")
+
+
+# ---------------------------------------------------------
+# Testing Documents
+# ---------------------------------------------------------
+class TaskTestingDocument(models.Model):
+ _name = "task.testing.document"
+ _description = "Testing Documents"
+ _order = "create_date desc"
+
+ task_id = fields.Many2one(
+ "project.task",
+ string="Task",
+ ondelete="cascade"
+ )
+ project_id = fields.Many2one("project.project",string="Project",ondelete="cascade")
+
+ name = fields.Char(string="Document Name", required=True)
+ file = fields.Binary(string="Testing File", required=True)
+ file_name = fields.Char(string="Filename")
+ notes = fields.Text(string="Notes")
diff --git a/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv b/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv
index aaa5eea4b..4f6a086a0 100644
--- a/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv
+++ b/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv
@@ -3,12 +3,42 @@ internal_teams_admin,internal.teams.admin,model_internal_teams,project.group_pro
internal_teams_manager,internal.teams.manager,model_internal_teams,project.group_project_user,1,1,1,0
internal_teams_user,internal.teams.user,model_internal_teams,base.group_user,1,0,0,0
+access_project_sprint_user,access.project.sprint.user,model_project_sprint,project.group_project_user,1,1,1,1
+access_project_sprint_manager,access.project.sprint.manager,model_project_sprint,project.group_project_manager,1,1,1,1
+
+
+access_project_architecture_design_user,access_project_architecture_design_user,model_project_architecture_design,project.group_project_user,1,1,1,1
+access_project_architecture_design_manager,access_project_architecture_design_manager,model_project_architecture_design,project.group_project_manager,1,1,1,1
+
+access_project_risk_user,access_project_risk_user,model_project_risk,project.group_project_user,1,1,1,1
+access_project_risk_manager,access_project_risk_manager,model_project_risk,project.group_project_manager,1,1,1,1
+
+access_project_resource_cost_user,project.resource.cost.user,model_project_resource_cost,project.group_project_user,1,1,1,1
+access_project_resource_cost_manager,project.resource.cost.manager,model_project_resource_cost,project.group_project_manager,1,1,1,1
+
+access_project_material_cost_user,project.material.cost.user,model_project_material_cost,project.group_project_user,1,1,1,1
+access_project_material_cost_manager,project.material.cost.manager,model_project_material_cost,project.group_project_manager,1,1,1,1
+
+access_project_equipment_cost_user,project.equipment.cost.user,model_project_equipment_cost,project.group_project_user,1,1,1,1
+access_project_equipment_cost_manager,project.equipment.cost.manager,model_project_equipment_cost,project.group_project_manager,1,1,1,1
+
+access_project_commit_step_user,access_project_commit_step_user,model_project_commit_step,project.group_project_user,1,1,1,1
+access_project_commit_step_manager,access_project_commit_step_manager,model_project_commit_step,project.group_project_manager,1,1,1,1
+
+access_task_development_document,access_task_development_document,model_task_development_document,base.group_user,1,1,1,1
+access_task_testing_document,access_task_testing_document,model_task_testing_document,base.group_user,1,1,1,1
+
project_user_assign_wizard_manager,project.user.assign.wizard,model_project_user_assign_wizard,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
project_user_assign_wizard_admin,project.user.assign.wizard.admin,model_project_user_assign_wizard,project.group_project_manager,1,1,1,1
project_user_assign_wizard_user,project.user.assign.wizard.user,model_project_user_assign_wizard,project.group_project_manager,1,0,0,0
+project_user_project_reject_reason_wizard,project.reject.reason.wizard.user,model_project_reject_reason_wizard,base.group_user,1,1,1,1
project_user_task_reject_reason_wizard,task.reject.reason.wizard.user,model_task_reject_reason_wizard,base.group_user,1,1,1,1
+
+access_project_stages_approval_flow_admin,access.project.stages.approval.flow.admin,model_project_stages_approval_flow,project.group_project_manager,1,1,1,1
+access_project_stages_approval_flow_user,access.project.stages.approval.flow.user,model_project_stages_approval_flow,base.group_user,1,1,0,0
+
project_internal_team_members_wizard,internal.team.members.wizard.manager,model_internal_team_members_wizard,base.group_user,1,1,1,1
project_project_stage_update_wizard,project.stage.update.wizard.manager,model_project_stage_update_wizard,base.group_user,1,1,1,1
diff --git a/addons_extensions/project_task_timesheet_extended/view/project.xml b/addons_extensions/project_task_timesheet_extended/view/project.xml
index 8b0d60958..38ddf6185 100644
--- a/addons_extensions/project_task_timesheet_extended/view/project.xml
+++ b/addons_extensions/project_task_timesheet_extended/view/project.xml
@@ -82,16 +82,4 @@
-
- project.invoice.inherit.form.view
- project.project
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/view/project_stages.xml b/addons_extensions/project_task_timesheet_extended/view/project_stages.xml
new file mode 100644
index 000000000..9b6ad8a53
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/view/project_stages.xml
@@ -0,0 +1,540 @@
+
+
+
+ project.project.stage.list.inherit
+ project.project.stage
+
+
+
+
+
+
+
+
+
+ project.project.inherit.form.view
+ project.project
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [('id', 'in', showable_stage_ids)]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ not show_project_chatter
+
+
+
+
+
+ assign_approval_flow
+
+
+ Initiation
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ project.invoice.inherit.form.view
+ project.project
+
+
+
+ True
+
+
+
+
+
+ True
+
+
+ True
+
+
+
+
+
+
+
+
+ project.task.form.commit.steps
+ project.task
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ not show_task_chatter
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.py b/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.py
index 7ef123d9d..436abecff 100644
--- a/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.py
+++ b/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.py
@@ -20,3 +20,25 @@ class TaskRejectReasonWizard(models.TransientModel):
return {
"type": "ir.actions.act_window_close"
}
+
+
+
+class ProjectRejectReasonWizard(models.TransientModel):
+ _name = "project.reject.reason.wizard"
+ _description = "Project Rejection Reason Wizard"
+
+ reason = fields.Text(string="Rejection Reason", required=True)
+ project_id = fields.Many2one("project.project", string="Project", required=True)
+
+ def action_reject(self):
+ """Trigger the rejection action on the selected task"""
+ self.ensure_one()
+ if not self.reason:
+ raise UserError(_("Please enter a reason for rejection."))
+
+ # Call the existing reject method on the task
+ self.project_id.reject_and_return(reason=self.reason)
+
+ return {
+ "type": "ir.actions.act_window_close"
+ }
diff --git a/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.xml b/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.xml
index bb33d53a7..2c853e091 100644
--- a/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.xml
+++ b/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.xml
@@ -22,4 +22,27 @@
form
new
+
+
+ project.reject.reason.wizard.form
+ project.reject.reason.wizard
+
+
+
+
+
+
+
+
+
+
+
+ Reject Task
+ project.reject.reason.wizard
+ form
+ new
+
diff --git a/odoo/addons/base/security/base_groups.xml b/odoo/addons/base/security/base_groups.xml
index e00ea94e0..4a5e7c3b1 100644
--- a/odoo/addons/base/security/base_groups.xml
+++ b/odoo/addons/base/security/base_groups.xml
@@ -85,18 +85,18 @@
-
- Portal User Template
- portaltemplate
-
-
-
-
+
+
+
+
+
+
+
-
- base.template_portal_user_id
-
-
+
+
+
+