import { Component, OnInit, HostListener, OnDestroy } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Router, ActivatedRoute } from '@angular/router'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { QAItem, QuestionAnswerService, CategoryMeta } from './question-answer-service.service'; import { AuthService } from '../services/auth.service'; // Add this simple pipe at the top of the file import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'truncate', standalone: true }) export class TruncatePipe implements PipeTransform { transform(value: string, limit: number = 100, trail: string = '...'): string { if (!value) return ''; return value.length > limit ? value.substring(0, limit) + trail : value; } } @Component({ selector: 'app-question-answer', templateUrl: './question-answer.component.html', styleUrls: ['./question-answer.component.css'], standalone: true, imports: [CommonModule, FormsModule, TruncatePipe] }) export class QuestionAnswerComponent implements OnInit, OnDestroy { selectedOption: string = ''; displayMode: 'modal' | 'page' = 'page'; isLoading: boolean = true; isModalOpen: boolean = false; selectedQuestions: QAItem[] = []; answers: Record = {}; attemptedSubmit: boolean = false; isSubmitting: boolean = false; userId: number = 0; currentTabIndex: number = 0; selectedReviewCategory: string = ''; categoriesMeta: CategoryMeta[] = []; selectedCategory: string = ''; allowedRoles = new Set(['marriage', 'interview', 'partnership']); BIT_FIELDS = new Set(['smoking', 'drinking', 'vegetarian']); showSubmissionPopup: boolean = false; isSubmissionSuccess: boolean = false; submissionMessage: string = ''; isEditingExistingProfile: boolean = false; existingProfileData: any = null; openDropdowns: { [key: string]: boolean } = {}; // Add new properties for text input popup showTextInputPopup: boolean = false; popupQuestion: QAItem | null = null; popupTextValue: string = ''; constructor( private qaService: QuestionAnswerService, private router: Router, private route: ActivatedRoute, private authService: AuthService ) { } // Add this method to check if the field should use popup overlay isAdditionalRemarksField(question?: QAItem): boolean { if (question) { // Check if this is specifically the "Additional Remarks" field const label = question.label?.toLowerCase(); const columnKey = question.column_key?.toLowerCase(); // Only show popup for Additional Remarks and similar specific fields const popupFields = [ 'additional remarks', 'remark', 'remarks' // Remove other fields if you only want Additional Remarks ]; return popupFields.some(field => label?.includes(field) || columnKey?.includes(field) ); } // For the popup overlay check (when no question parameter) const currentLabel = this.popupQuestion?.label?.toLowerCase(); const currentColumnKey = this.popupQuestion?.column_key?.toLowerCase(); const popupFields = [ 'additional remarks', 'remark', 'remarks' ]; return popupFields.some(field => currentLabel?.includes(field) || currentColumnKey?.includes(field) ); } // Add these methods for dropdown functionality toggleDropdown(columnKey: string): void { // Close all other dropdowns first this.openDropdowns = {}; // Toggle the current dropdown this.openDropdowns[columnKey] = !this.openDropdowns[columnKey]; // Add/remove class to form group for z-index management setTimeout(() => { const formGroup = document.querySelector(`[id="q-${columnKey}"]`); if (formGroup) { if (this.openDropdowns[columnKey]) { formGroup.classList.add('dropdown-open'); } else { formGroup.classList.remove('dropdown-open'); } } }); } isDropdownOpen(columnKey: string): boolean { return this.openDropdowns[columnKey] === true; } getSelectedOptionsText(columnKey: string): string { const value = this.answers[columnKey]; if (!Array.isArray(value) || value.length === 0) { return ''; } if (value.length === 1) { return value[0]; } return `${value.length} options selected`; } areAllOptionsSelected(question: QAItem): boolean { const value = this.answers[question.column_key]; const options = this.getMultiselectOptions(question); return Array.isArray(value) && options.length > 0 && value.length === options.length; } toggleSelectAll(question: QAItem): void { const options = this.getMultiselectOptions(question); if (!this.areAllOptionsSelected(question)) { // Select all this.answers[question.column_key] = [...options]; } else { // Deselect all this.answers[question.column_key] = []; } this.onAnswerChange(question.column_key, this.answers[question.column_key]); } // Add these methods for text input popup functionality openTextInputPopup(question: QAItem): void { // Only open popup for specific fields (like Additional Remarks) if (this.isAdditionalRemarksField(question)) { this.popupQuestion = question; this.popupTextValue = this.answers[question.column_key] || ''; this.showTextInputPopup = true; // Initialize textarea height after view updates setTimeout(() => { this.initializeTextareaHeight(); }); } // For other fields, do nothing (no popup will open) } closeTextInputPopup(): void { this.showTextInputPopup = false; this.popupQuestion = null; this.popupTextValue = ''; } saveTextInputPopup(): void { if (this.popupQuestion) { this.answers[this.popupQuestion.column_key] = this.popupTextValue; this.onAnswerChange(this.popupQuestion.column_key, this.popupTextValue); } this.closeTextInputPopup(); } // Auto-expanding textarea functionality for popup only onTextareaInput(event: Event): void { const textarea = event.target as HTMLTextAreaElement; this.adjustTextareaHeight(textarea); } initializeTextareaHeight(): void { const textarea = document.querySelector('.text-input-popup-field') as HTMLTextAreaElement; if (textarea) { this.adjustTextareaHeight(textarea); textarea.focus(); // Set cursor to end of text const length = textarea.value.length; textarea.setSelectionRange(length, length); } } adjustTextareaHeight(textarea: HTMLTextAreaElement): void { // Reset height to auto to get the correct scrollHeight textarea.style.height = 'auto'; // Set the height to scrollHeight to expand, with max limit const maxHeight = 400; const newHeight = Math.min(textarea.scrollHeight, maxHeight); textarea.style.height = newHeight + 'px'; // Enable scrolling only when content exceeds max height textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden'; } private enhanceUserExperience(): void { if (this.selectedCategory !== 'review') { setTimeout(() => { const formContainer = document.querySelector('.form-container'); if (formContainer) { formContainer.scrollTop = 0; } }, 150); } } getProgressBarWidth(): number { if (this.selectedCategory === 'review') { return 94; } const currentIndex = this.categoriesMeta.findIndex(cat => cat.key === this.selectedCategory); if (currentIndex >= 0) { return ((currentIndex + 1) / (this.categoriesMeta.length + 1)) * 100; } return 0; } getStepLabel(title: string): string { const words = title.split(' '); return words.length > 2 ? words[0] : title.split(' ')[0]; } @HostListener('window:resize') onWindowResize() { // Responsive behavior placeholder } get totalRequired(): number { return this.selectedQuestions.filter(q => q.required).length; } get answeredRequired(): number { return this.selectedQuestions.filter(q => q.required && this.isAnswered(q)).length; } get progressPercent(): number { return this.totalRequired ? Math.round((this.answeredRequired / this.totalRequired) * 100) : 0; } get allFieldsAnswered(): boolean { return this.selectedQuestions.length > 0 && this.selectedQuestions.every(q => this.isAnswered(q)); } get unansweredCount(): number { return this.selectedQuestions.filter(q => !this.isAnswered(q)).length; } getCategoryQuestions(key: string): QAItem[] { const norm = key.replace(/\s+/g, '').toLowerCase(); return this.selectedQuestions.filter(q => (q.category || '').replace(/\s+/g, '').toLowerCase() === norm); } getCategoryAnsweredCount(key: string): number { return this.getCategoryQuestions(key).filter(q => this.isAnswered(q)).length; } getCategoryRequiredCount(key: string): number { const qs = this.getCategoryQuestions(key); const requiredCnt = qs.filter(q => q.required).length; return requiredCnt || qs.length; } getCategoryPercent(key: string): number { const denom = this.getCategoryRequiredCount(key) || 1; return Math.round((this.getCategoryAnsweredCount(key) / denom) * 100); } showSubmissionSuccessPopup(): void { this.showSubmissionPopup = true; this.isSubmissionSuccess = true; this.submissionMessage = 'Profile submitted successfully!'; } redirectToHome(): void { this.showSubmissionPopup = false; this.router.navigate(['/']); } redirectToPreferences(): void { this.showSubmissionPopup = false; this.router.navigate(['/userpreferences'], { queryParams: { user_id: this.userId } }); } closeSuccessModal(): void { this.showSubmissionPopup = false; // Remove blur effects from main content const container = document.querySelector('.container'); const tabs = document.querySelectorAll('.tab'); if (container) { container.classList.remove('blur-background'); } tabs.forEach(tab => { tab.classList.remove('blur-background'); }); } // Single closeModal method that handles both scenarios closeModal(): void { if (this.displayMode === 'modal') { this.isModalOpen = false; this.selectedQuestions = []; this.answers = {}; this.selectedOption = ''; } else { // For page mode, close the success modal this.closeSuccessModal(); } } private checkExistingProfile(role: string): void { this.isLoading = true; this.qaService.getExistingProfile(role, this.userId).subscribe({ next: (existingData: any) => { console.log('🟢 Existing profile data received:', existingData); if (existingData && Object.keys(existingData).length > 0 && !existingData.error) { this.isEditingExistingProfile = true; this.existingProfileData = existingData; console.log('🟢 Editing existing profile with data:', this.existingProfileData); } else { this.isEditingExistingProfile = false; this.existingProfileData = null; console.log('🟡 No existing profile found or empty data'); } this.onSelect(role, 'page'); }, error: (err: HttpErrorResponse) => { console.log('🟡 No existing profile found or error:', err.message); this.isEditingExistingProfile = false; this.existingProfileData = null; this.onSelect(role, 'page'); } }); } onSelect(role: string, mode: 'modal' | 'page' = 'page'): void { this.selectedOption = role; this.displayMode = mode; this.isModalOpen = mode === 'modal'; this.isLoading = true; // Fetch categories first, then questions this.qaService.getCategories(role).subscribe({ next: (categories: CategoryMeta[]) => { this.categoriesMeta = categories; // Set default review category if (this.categoriesMeta.length > 0) { this.selectedReviewCategory = this.categoriesMeta[0].key; } this.fetchQuestions(role); }, error: (err: HttpErrorResponse) => { console.error('Failed to load categories, using defaults', err); // Fallback to default categories if API fails this.categoriesMeta = this.getDefaultCategories(); // Set default review category if (this.categoriesMeta.length > 0) { this.selectedReviewCategory = this.categoriesMeta[0].key; } this.fetchQuestions(role); } }); } private getDefaultCategories(): CategoryMeta[] { return [ { key: 'Personal', title: 'Personal Information' }, { key: 'Location', title: 'Location & Family' }, { key: 'Lifestyle', title: 'Lifestyle & Preferences' }, { key: 'Education', title: 'Education & Career' } ]; } ngOnInit(): void { this.route.queryParamMap.subscribe(params => { const role = (params.get('role') || '').toLowerCase(); const userIdFromParams = params.get('user_id'); if (userIdFromParams) { this.userId = Number(userIdFromParams); } else { this.userId = this.authService.userId || 0; } if (this.allowedRoles.has(role)) { this.checkExistingProfile(role); } }); // Set default review category if (this.categoriesMeta.length > 0) { this.selectedReviewCategory = this.categoriesMeta[0].key; } } // Add this method to handle review category selection selectReviewCategory(categoryKey: string): void { this.selectedReviewCategory = categoryKey; } ngOnDestroy(): void { // Clean up if needed } selectCategory(key: string): void { this.selectedCategory = key; const categoryIndex = this.categoriesMeta.findIndex(cat => cat.key === key); this.currentTabIndex = categoryIndex >= 0 ? categoryIndex : this.categoriesMeta.length; // Initialize selected review category when entering review tab if (key === 'review' && this.categoriesMeta.length > 0) { this.selectedReviewCategory = this.categoriesMeta[0].key; } this.enhanceUserExperience(); } trackQuestion(index: number, item: QAItem): string { return item.column_key; } fetchQuestions(role: string): void { this.qaService.getQuestions(role).subscribe({ next: (data: QAItem[]) => { this.selectedQuestions = data; console.log('🟢 Questions loaded:', this.selectedQuestions.length); this.initializeAnswers(this.selectedQuestions); this.isLoading = false; this.attemptedSubmit = false; // Set initial category after questions are loaded if (this.categoriesMeta.length > 0) { this.selectedCategory = this.categoriesMeta[0].key; this.currentTabIndex = 0; } if (role === 'marriage') { this.displayMode = 'page'; this.isModalOpen = false; } else if (this.displayMode === 'modal') { this.isModalOpen = true; } else { this.isModalOpen = false; } setTimeout(() => { this.enhanceUserExperience(); }, 500); }, error: (err: HttpErrorResponse) => { this.isLoading = false; this.showSubmissionPopup = true; this.isSubmissionSuccess = false; this.submissionMessage = 'Failed to load questions. Please try again.'; } }); } private initializeAnswers(questions: QAItem[]): void { this.answers = {}; console.log('🟢 Initializing answers. Editing existing profile:', this.isEditingExistingProfile); console.log('🟢 Existing profile data:', this.existingProfileData); questions.forEach(q => { if (this.isEditingExistingProfile && this.existingProfileData) { const existingValue = this.existingProfileData[q.column_key]; console.log(`🟢 Processing field ${q.column_key}:`, { value: existingValue, type: typeof existingValue, inputType: q.input_type }); if (q.input_type === 'multiselect') { // Handle multiselect fields if (typeof existingValue === 'string' && existingValue) { // Clean and split comma-separated string const cleanValue = existingValue.replace(/[\[\]"]/g, ''); this.answers[q.column_key] = cleanValue.split(',') .map((item: string) => item.trim()) .filter(item => item.length > 0); console.log(`🟢 Multiselect parsed: ${q.column_key} =`, this.answers[q.column_key]); } else if (Array.isArray(existingValue)) { this.answers[q.column_key] = existingValue.filter(item => item && item.length > 0); } else { this.answers[q.column_key] = []; } } else if (q.input_type === 'radio' || q.input_type === 'select') { // Handle radio buttons and select fields if (existingValue !== null && existingValue !== undefined && existingValue !== '') { this.answers[q.column_key] = String(existingValue).trim(); console.log(`🟢 Radio/Select set: ${q.column_key} = "${this.answers[q.column_key]}"`); } else { this.answers[q.column_key] = ''; } } else if (q.input_type === 'checkbox') { // Handle checkbox fields if (existingValue === 1 || existingValue === '1' || existingValue === true || existingValue === 'true' || existingValue === 'Yes') { this.answers[q.column_key] = true; } else if (existingValue === 0 || existingValue === '0' || existingValue === false || existingValue === 'false' || existingValue === 'No') { this.answers[q.column_key] = false; } else { this.answers[q.column_key] = false; } console.log(`🟢 Checkbox set: ${q.column_key} =`, this.answers[q.column_key]); } else if (q.input_type === 'date' && existingValue) { // Handle date fields try { if (existingValue instanceof Date) { this.answers[q.column_key] = existingValue.toISOString().split('T')[0]; } else if (typeof existingValue === 'string') { const date = new Date(existingValue); if (!isNaN(date.getTime())) { this.answers[q.column_key] = date.toISOString().split('T')[0]; } else { this.answers[q.column_key] = existingValue; } } else { this.answers[q.column_key] = existingValue; } } catch { this.answers[q.column_key] = existingValue; } } else { // Handle text, textarea, number fields if (existingValue !== null && existingValue !== undefined) { this.answers[q.column_key] = String(existingValue); } else { this.answers[q.column_key] = ''; } console.log(`🟢 Text field set: ${q.column_key} = "${this.answers[q.column_key]}"`); } } else { // Initialize empty values for new profile this.answers[q.column_key] = q.input_type === 'multiselect' ? [] : q.input_type === 'checkbox' ? false : ''; console.log(`🟡 New profile - empty value for: ${q.column_key}`); } }); console.log('🟢 Final answers object:', this.answers); } onAnswerChange(key: string, value: any): void { this.answers[key] = value; console.log(`🟢 Answer changed: ${key} =`, value); } isAnswered(q: QAItem): boolean { const v = this.answers[q.column_key]; if (q.input_type === 'multiselect') { return Array.isArray(v) && v.length > 0; } if (q.input_type === 'checkbox') { return v !== undefined && v !== null; } return v !== undefined && v !== null && v !== '' && v !== false; } private normalizeForBackend(obj: Record): Record { const out: Record = {}; for (const [k, v] of Object.entries(obj)) { if (!this.BIT_FIELDS.has(k)) { out[k] = v; continue; } if (v === undefined || v === null || v === '') { out[k] = null; continue; } if (typeof v === 'boolean') { out[k] = v ? 1 : 0; continue; } if (typeof v === 'number') { out[k] = v !== 0 ? 1 : 0; continue; } if (typeof v === 'string') { const s = v.trim().toLowerCase(); if (['yes', 'true', 'y', '1'].includes(s)) { out[k] = 1; continue; } if (['no', 'false', 'n', '0'].includes(s)) { out[k] = 0; continue; } out[k] = v; continue; } out[k] = v; } return out; } submitAnswers(): void { this.attemptedSubmit = true; const incompleteRequired = this.selectedQuestions.filter(q => q.required && !this.isAnswered(q)); if (incompleteRequired.length) { const first = incompleteRequired[0]; const el = document.getElementById('q-' + first.column_key); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); el.classList.add('highlight-required'); setTimeout(() => { el.classList.remove('highlight-required'); }, 2000); } return; } this.isLoading = true; this.isSubmitting = true; const normalizedAnswers = this.normalizeForBackend(this.answers); const fields: Record = {}; this.selectedQuestions.forEach((q) => { fields[q.column_key] = normalizedAnswers[q.column_key] ?? null; }); const payload = { user_id: this.userId, ...fields }; console.log('🟢 Submitting payload:', payload); const submitObservable = this.isEditingExistingProfile ? this.qaService.updateAnswers(this.selectedOption, this.userId, payload) : this.qaService.submitAnswers(this.selectedOption, this.userId, payload); submitObservable.subscribe({ next: (response) => { this.isLoading = false; this.isSubmitting = false; console.log('🟢 Submission successful:', response); // Update progress tracking const userId = this.userId; if (userId) { const existingProgress = localStorage.getItem(`user_progress_${userId}`); let progressData: any = {}; if (existingProgress) { try { progressData = JSON.parse(existingProgress); } catch (e) { console.error('Error parsing existing progress:', e); } } progressData.profileCompleted = true; localStorage.setItem(`user_progress_${userId}`, JSON.stringify(progressData)); } // Dispatch event for progress updates const profileCompletedEvent = new CustomEvent('profileCompleted', { detail: { userId: this.userId, role: this.selectedOption, timestamp: Date.now() } }); window.dispatchEvent(profileCompletedEvent); this.showSubmissionSuccessPopup(); }, error: (err: HttpErrorResponse) => { this.isLoading = false; this.isSubmitting = false; const reason = err.status === 0 ? 'Network/CORS or server unreachable' : (err.error?.error || err.message || 'Server error'); console.error('🔴 Submission error:', err); this.showSubmissionPopup = true; this.isSubmissionSuccess = false; this.submissionMessage = `Submission failed: ${reason}. Please try again.`; } }); } getMultiselectOptions(question: QAItem): string[] { return question.options || []; } isMultiselectSelected(key: string, option: string): boolean { return Array.isArray(this.answers[key]) && this.answers[key].includes(option); } @HostListener('document:click', ['$event']) onDocumentClick(event: MouseEvent): void { const target = event.target as HTMLElement; if (!target.closest('.multiselect-dropdown-checkbox')) { // Remove dropdown-open class from all form groups document.querySelectorAll('.form-group.dropdown-open').forEach(group => { group.classList.remove('dropdown-open'); }); this.openDropdowns = {}; } } onMultiselectChange(event: Event, key: string, option?: string): void { const input = event.target as HTMLInputElement; if (!Array.isArray(this.answers[key])) { this.answers[key] = []; } const value = option || input.value; const isChecked = input.checked; if (isChecked) { if (!this.answers[key].includes(value)) { this.answers[key].push(value); } } else { this.answers[key] = this.answers[key].filter((v: string) => v !== value); } this.onAnswerChange(key, this.answers[key]); } isRadioSelected(key: string, option: string): boolean { return this.answers[key] === option; } onRadioChange(key: string, option: string): void { this.answers[key] = option; this.onAnswerChange(key, option); } goToPreviousCategory(currentKey: string): void { const currentIndex = this.categoriesMeta.findIndex(cat => cat.key === currentKey); if (currentIndex > 0) { this.selectedCategory = this.categoriesMeta[currentIndex - 1].key; this.currentTabIndex = currentIndex - 1; this.enhanceUserExperience(); } } goToNextCategory(currentKey: string): void { const currentIndex = this.categoriesMeta.findIndex(cat => cat.key === currentKey); if (currentIndex < this.categoriesMeta.length - 1) { this.selectedCategory = this.categoriesMeta[currentIndex + 1].key; this.currentTabIndex = currentIndex + 1; } else { this.selectedCategory = 'review'; this.currentTabIndex = this.categoriesMeta.length; // Ensure review category is set when navigating to review if (this.categoriesMeta.length > 0) { this.selectedReviewCategory = this.categoriesMeta[0].key; } } this.enhanceUserExperience(); } getAnswerDisplay(question: QAItem): string { const value = this.answers[question.column_key]; if (value === null || value === undefined || value === '') { return '(Not answered)'; } if (Array.isArray(value)) { return value.length > 0 ? value.join(', ') : '(Not answered)'; } if (typeof value === 'boolean') { return value ? 'Yes' : 'No'; } return String(value); } getPlaceholder(question: QAItem): string { if (question.placeholder) return question.placeholder; switch (question.input_type) { case 'date': return 'Select date'; case 'select': return 'Select option'; default: return 'Enter your answer'; } } showError(q: QAItem): boolean { return this.attemptedSubmit && !!q.required && !this.isAnswered(q); } toTitleCase(text: string): string { return text.replace(/\w\S*/g, (txt) => { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }); } getCategoryIcon(categoryKey: string): string { const iconMap: { [key: string]: string } = { 'Personal': 'fa-user', 'Location': 'fa-home', 'Lifestyle': 'fa-heart', 'Education': 'fa-graduation-cap', 'review': 'fa-check' }; return iconMap[categoryKey] || 'fa-question'; } getCurrentCategoryIcon(): string { return this.getCategoryIcon(this.selectedCategory); } getCurrentCategoryTitle(): string { if (this.selectedCategory === 'review') { return 'Review Your Information'; } const category = this.categoriesMeta.find(c => c.key === this.selectedCategory); return category?.title || 'Category'; } getCurrentCategoryDescription(): string { const descriptions: Record = { 'Personal': 'Please provide your personal information', 'Location': 'Share your location and family details', 'Lifestyle': 'Describe your lifestyle and preferences', 'Education': 'Tell us about your education and career', 'review': 'Review all the information you have provided' }; return descriptions[this.selectedCategory] || 'Complete this section'; } getReviewCategoryTitle(categoryKey: string): string { const category = this.categoriesMeta.find(c => c.key === categoryKey); return category?.title || categoryKey; } getCategoryAnsweredCountByKey(categoryKey: string): number { return this.getCategoryAnsweredCount(categoryKey); } getCategoryRequiredCountByKey(categoryKey: string): number { return this.getCategoryRequiredCount(categoryKey); } }