From 9c31454c359944cf42ef2d577214cd9934d4544d Mon Sep 17 00:00:00 2001 From: vee1e Date: Mon, 23 Jun 2025 04:57:07 +0530 Subject: [PATCH] feat: - combine both upload components into one - add live updates upon upload - add preview section for forms - fix the search-bar's weird padding issue - general cleanup of things, removing some files - cleaned the run.sh file a bit too --- backend/main.py | 27 +- backend/services/database_service.py | 34 +- backend/services/xlsform_parser.py | 38 +- frontend/package-lock.json | 16 +- frontend/package.json | 4 +- frontend/src/app/app.component.ts | 14 +- frontend/src/app/app.config.ts | 4 +- frontend/src/app/app.module.ts | 21 - .../components/download/download.component.ts | 284 ------ .../file-upload/file-upload.component.ts | 364 ------- .../form-preview/form-preview.component.ts | 15 - .../app/components/navbar/navbar.component.ts | 33 +- .../app/components/search/search.component.ts | 13 +- .../app/components/upload/upload.component.ts | 892 ++++++++++++++++-- frontend/src/app/core/core.module.ts | 12 - frontend/src/app/models/form.model.ts | 2 +- frontend/src/app/services/form.service.ts | 10 +- frontend/src/app/shared/shared.module.ts | 12 - frontend/src/styles.scss | 2 +- run.sh | 47 +- 20 files changed, 917 insertions(+), 927 deletions(-) delete mode 100644 frontend/src/app/app.module.ts delete mode 100644 frontend/src/app/components/download/download.component.ts delete mode 100644 frontend/src/app/components/file-upload/file-upload.component.ts delete mode 100644 frontend/src/app/components/form-preview/form-preview.component.ts delete mode 100644 frontend/src/app/core/core.module.ts delete mode 100644 frontend/src/app/shared/shared.module.ts diff --git a/backend/main.py b/backend/main.py index bc2e666..95dbbed 100644 --- a/backend/main.py +++ b/backend/main.py @@ -37,20 +37,19 @@ async def validate_file(file: UploadFile): """ Validate the uploaded Excel file format """ - try: - if not file.filename or not file.filename.endswith(('.xls', '.xlsx')): - return FormValidation( - valid=False, - message="Invalid file format. Only .xls/.xlsx files are allowed.", - sheets=[], - form_metadata={}, - questions_count=0, - options_count=0 - ) + if not file.filename or not file.filename.endswith(('.xls', '.xlsx')): + return FormValidation( + valid=False, + message="Invalid file format. Only .xls/.xlsx files are allowed.", + sheets=[], + form_metadata={}, + questions_count=0, + options_count=0 + ) + try: parser = XLSFormParser() validation_result = await parser.validate_file(file) - return FormValidation(**validation_result) except Exception as e: logger.error(f"Error validating file: {str(e)}") @@ -90,10 +89,10 @@ async def get_form_by_id(form_id: str): form = await db_service.get_form_by_id(form_id) if not form: raise HTTPException(status_code=404, detail="Form not found") - + questions = await db_service.get_questions_by_form_id(form_id) options = await db_service.get_options_by_form_id(form_id) - + return { "form": form, "questions": questions, @@ -116,7 +115,7 @@ async def delete_form(form_id: str): success = await db_service.delete_form(form_id) if not success: raise HTTPException(status_code=404, detail="Form not found") - + return {"message": "Form deleted successfully"} except HTTPException: raise diff --git a/backend/services/database_service.py b/backend/services/database_service.py index c63b785..d9659ea 100644 --- a/backend/services/database_service.py +++ b/backend/services/database_service.py @@ -6,22 +6,22 @@ import logging logger = logging.getLogger(__name__) class DatabaseService: - + async def save_form(self, form_data: Dict[str, Any]) -> str: """Save form metadata to database""" try: if 'id' in form_data: del form_data['id'] - + form_data['_id'] = ObjectId() - + result = await forms_collection.insert_one(form_data) logger.info(f"Form saved with ID: {result.inserted_id}") return str(result.inserted_id) except Exception as e: logger.error(f"Error saving form: {e}") raise e - + async def save_questions(self, questions: List[Dict[str, Any]], form_id: str) -> List[str]: """Save questions to database""" try: @@ -29,16 +29,16 @@ class DatabaseService: for question in questions: question['form_id'] = form_id question['_id'] = ObjectId() - + result = await questions_collection.insert_one(question) question_ids.append(str(result.inserted_id)) - + logger.info(f"Saved {len(questions)} questions for form {form_id}") return question_ids except Exception as e: logger.error(f"Error saving questions: {e}") raise e - + async def save_options(self, options: List[Dict[str, Any]], form_id: str) -> List[str]: """Save answer options to database""" try: @@ -46,16 +46,16 @@ class DatabaseService: for option in options: option['form_id'] = form_id option['_id'] = ObjectId() - + result = await options_collection.insert_one(option) option_ids.append(str(result.inserted_id)) - + logger.info(f"Saved {len(options)} options for form {form_id}") return option_ids except Exception as e: logger.error(f"Error saving options: {e}") raise e - + async def get_form_by_id(self, form_id: str) -> Optional[Dict[str, Any]]: """Get form by ID""" try: @@ -67,7 +67,7 @@ class DatabaseService: except Exception as e: logger.error(f"Error getting form: {e}") return None - + async def get_questions_by_form_id(self, form_id: str) -> List[Dict[str, Any]]: """Get all questions for a form""" try: @@ -79,7 +79,7 @@ class DatabaseService: except Exception as e: logger.error(f"Error getting questions: {e}") return [] - + async def get_options_by_form_id(self, form_id: str) -> List[Dict[str, Any]]: """Get all options for a form""" try: @@ -91,7 +91,7 @@ class DatabaseService: except Exception as e: logger.error(f"Error getting options: {e}") return [] - + async def get_all_forms(self) -> List[Dict[str, Any]]: """Get all forms""" try: @@ -103,16 +103,16 @@ class DatabaseService: except Exception as e: logger.error(f"Error getting all forms: {e}") return [] - + async def delete_form(self, form_id: str) -> bool: """Delete form and all related data""" try: form_result = await forms_collection.delete_one({"_id": ObjectId(form_id)}) - + questions_result = await questions_collection.delete_many({"form_id": form_id}) - + options_result = await options_collection.delete_many({"form_id": form_id}) - + logger.info(f"Deleted form {form_id} with {questions_result.deleted_count} questions and {options_result.deleted_count} options") return form_result.deleted_count > 0 except Exception as e: diff --git a/backend/services/xlsform_parser.py b/backend/services/xlsform_parser.py index 766d898..72fbd6d 100644 --- a/backend/services/xlsform_parser.py +++ b/backend/services/xlsform_parser.py @@ -20,17 +20,17 @@ class XLSFormParser: async def validate_file(self, file: UploadFile) -> Dict[str, Any]: try: df_dict = pd.read_excel(file.file, sheet_name=None) - + sheets_validation = [] form_metadata = {} questions_count = 0 options_count = 0 - + forms_validation = self._validate_sheet( df_dict, 'Forms', self.REQUIRED_FORMS_COLUMNS ) sheets_validation.append(forms_validation) - + if forms_validation['exists'] and not forms_validation['missing_columns']: forms_df = df_dict['Forms'] if not forms_df.empty: @@ -38,27 +38,27 @@ class XLSFormParser: 'language': forms_df.iloc[0].get('Language', 'Unknown'), 'title': forms_df.iloc[0].get('Title', 'Untitled') } - + questions_validation = self._validate_sheet( df_dict, 'Questions Info', self.REQUIRED_QUESTIONS_COLUMNS ) sheets_validation.append(questions_validation) - + if questions_validation['exists'] and not questions_validation['missing_columns']: questions_df = df_dict['Questions Info'] questions_count = len(questions_df) - + options_validation = self._validate_sheet( df_dict, 'Answer Options', self.REQUIRED_OPTIONS_COLUMNS ) sheets_validation.append(options_validation) - + if options_validation['exists'] and not options_validation['missing_columns']: options_df = df_dict['Answer Options'] options_count = len(options_df) - + is_valid = all(sheet['exists'] and not sheet['missing_columns'] for sheet in sheets_validation) - + return { 'valid': is_valid, 'message': "File format is valid." if is_valid else "Invalid XLSForm structure.", @@ -86,7 +86,7 @@ class XLSFormParser: columns = list(df_dict[sheet_name].columns) if exists else [] missing_columns = [col for col in required_columns if col not in columns] row_count = len(df_dict[sheet_name]) if exists else 0 - + return { 'name': sheet_name, 'exists': exists, @@ -105,12 +105,12 @@ class XLSFormParser: options_df = df_dict['Answer Options'] form_metadata = self._parse_form_metadata(forms_df) - + form_id = await self.db_service.save_form(form_metadata) - + questions_data = self._parse_questions_data(questions_df) question_ids = await self.db_service.save_questions(questions_data, form_id) - + options_data = self._parse_options_data(options_df) option_ids = await self.db_service.save_options(options_data, form_id) @@ -146,19 +146,19 @@ class XLSFormParser: 'version': '1.0.0', 'created_at': pd.Timestamp.now().isoformat() } - + if not forms_df.empty: if 'Language' in forms_df.columns: metadata['language'] = forms_df.iloc[0]['Language'] if 'Title' in forms_df.columns: metadata['title'] = forms_df.iloc[0]['Title'] - + return metadata def _parse_questions_data(self, questions_df: pd.DataFrame) -> List[Dict[str, Any]]: """Parse questions data for database storage""" questions_data = [] - + for _, row in questions_df.iterrows(): question_data = { 'order': int(row['Order']), @@ -168,13 +168,13 @@ class XLSFormParser: 'created_at': pd.Timestamp.now().isoformat() } questions_data.append(question_data) - + return questions_data def _parse_options_data(self, options_df: pd.DataFrame) -> List[Dict[str, Any]]: """Parse options data for database storage""" options_data = [] - + for _, row in options_df.iterrows(): option_data = { 'order': int(row['Order']), @@ -183,7 +183,7 @@ class XLSFormParser: 'created_at': pd.Timestamp.now().isoformat() } options_data.append(option_data) - + return options_data def _get_form_title(self, forms_df: pd.DataFrame) -> Dict[str, str]: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cdd8438..9ed162d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,7 +32,7 @@ "@angular/cli": "^19.2.9", "@angular/compiler-cli": "^19.2.0", "@types/express": "^4.17.21", - "@types/node": "^18.19.100", + "@types/node": "^24.0.3", "typescript": "~5.7.2" } }, @@ -4979,13 +4979,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.100", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.100.tgz", - "integrity": "sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.8.0" } }, "node_modules/@types/node-forge": { @@ -12106,9 +12106,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "dev": true, "license": "MIT" }, diff --git a/frontend/package.json b/frontend/package.json index b1f75a1..2b9769c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", - "serve:ssr:mform-upload": "node dist/mform-upload/server/server.mjs" + "serve:ssr:mform-upload": "node --no-deprecation dist/mform-upload/server/server.mjs" }, "private": true, "dependencies": { @@ -34,7 +34,7 @@ "@angular/cli": "^19.2.9", "@angular/compiler-cli": "^19.2.0", "@types/express": "^4.17.21", - "@types/node": "^18.19.100", + "@types/node": "^24.0.3", "typescript": "~5.7.2" } } diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 62e552c..292fdb8 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -6,8 +6,6 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import { NavbarComponent } from './components/navbar/navbar.component'; import { SearchComponent } from './components/search/search.component'; import { UploadComponent } from './components/upload/upload.component'; -import { DownloadComponent } from './components/download/download.component'; -import { FileUploadComponent } from './components/file-upload/file-upload.component'; @Component({ selector: 'app-root', @@ -19,9 +17,7 @@ import { FileUploadComponent } from './components/file-upload/file-upload.compon MatToolbarModule, NavbarComponent, SearchComponent, - UploadComponent, - DownloadComponent, - FileUploadComponent + UploadComponent ], template: `
@@ -30,9 +26,7 @@ import { FileUploadComponent } from './components/file-upload/file-upload.compon
-
-
@@ -44,16 +38,16 @@ import { FileUploadComponent } from './components/file-upload/file-upload.compon flex-direction: column; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); } - + .main-content { flex: 1; padding: 2rem; overflow-y: auto; } - + .content-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: 1fr; gap: 2rem; margin-top: 2rem; } diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 700d2a5..362b090 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,7 +1,7 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideAnimations } from '@angular/platform-browser/animations'; -import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClient, withFetch } from '@angular/common/http'; import { routes } from './app.routes'; import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; @@ -12,6 +12,6 @@ export const appConfig: ApplicationConfig = { provideRouter(routes), provideClientHydration(withEventReplay()), provideAnimations(), - provideHttpClient() + provideHttpClient(withFetch()) ] }; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts deleted file mode 100644 index 7dfb61b..0000000 --- a/frontend/src/app/app.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { HttpClientModule } from '@angular/common/http'; -import { AppComponent } from './app.component'; -import { FileUploadComponent } from './components/file-upload/file-upload.component'; -import { CommonModule } from '@angular/common'; - -@NgModule({ - declarations: [ - AppComponent, - FileUploadComponent - ], - imports: [ - BrowserModule, - HttpClientModule, - CommonModule - ], - providers: [], - bootstrap: [AppComponent] -}) -export class AppModule { } \ No newline at end of file diff --git a/frontend/src/app/components/download/download.component.ts b/frontend/src/app/components/download/download.component.ts deleted file mode 100644 index 5fc44b3..0000000 --- a/frontend/src/app/components/download/download.component.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { MatCardModule } from '@angular/material/card'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { MatMenuModule } from '@angular/material/menu'; -import { MatDividerModule } from '@angular/material/divider'; -import { FormService, FormData, FormDetails } from '../../services/form.service'; - -@Component({ - selector: 'app-download', - standalone: true, - imports: [ - CommonModule, - MatCardModule, - MatButtonModule, - MatIconModule, - MatMenuModule, - MatDividerModule - ], - template: ` - - - download - Parsed Forms - Download or preview parsed forms from database - - - -
-
-
- description -
-

{{form.title}}

-

{{form.language}} • {{form.version}} • {{form.created_at | date:'short'}}

-
-
- -
- - - - - - - -
-
-
- - -
- info -

No forms have been parsed yet

- -
-
-
- - - - - -
- `, - styles: [` - .download-card { - background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - height: 100%; - min-height: 300px; - color: white; - - ::ng-deep { - .mat-mdc-card-header { - padding: 1rem; - } - - .mat-mdc-card-avatar { - background: rgba(255, 255, 255, 0.1); - border-radius: 50%; - padding: 8px; - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - - mat-icon { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - } - } - - .mat-mdc-card-title { - color: white; - } - - .mat-mdc-card-subtitle { - color: rgba(255, 255, 255, 0.7); - } - } - } - - .form-list { - max-height: 400px; - overflow-y: auto; - } - - .form-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem; - background: rgba(255, 255, 255, 0.05); - border-radius: 8px; - margin-bottom: 0.5rem; - transition: all 0.3s ease; - - &:hover { - background: rgba(255, 255, 255, 0.1); - } - } - - .form-info { - display: flex; - align-items: center; - gap: 1rem; - - mat-icon { - color: rgba(255, 255, 255, 0.7); - } - } - - .form-details { - h3 { - margin: 0; - font-size: 1rem; - font-weight: 500; - } - - p { - margin: 0; - font-size: 0.875rem; - color: rgba(255, 255, 255, 0.7); - } - } - - .no-forms { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 2rem; - text-align: center; - height: 100%; - min-height: 200px; - - mat-icon { - font-size: 48px; - width: 48px; - height: 48px; - margin-bottom: 1rem; - color: rgba(255, 255, 255, 0.5); - } - - p { - margin: 0 0 1rem; - color: rgba(255, 255, 255, 0.7); - } - } - - .warn-text { - color: #f44336; - } - - mat-card-actions { - display: flex; - justify-content: flex-end; - gap: 1rem; - padding: 1rem; - - button { - border: 2px solid; - border-radius: 8px; - padding: 8px 16px; - font-weight: 500; - transition: all 0.3s ease; - - &[color="primary"] { - background-color: #3f51b5; - border-color: #3f51b5; - color: white; - - &:hover { - background-color: #303f9f; - border-color: #303f9f; - } - } - - &[color="warn"] { - background-color: #f44336; - border-color: #f44336; - color: white; - - &:hover { - background-color: #d32f2f; - border-color: #d32f2f; - } - } - - mat-icon { - margin-right: 8px; - } - } - } - `] -}) -export class DownloadComponent implements OnInit { - parsedForms: FormData[] = []; - - constructor(private formService: FormService) {} - - ngOnInit(): void { - this.loadForms(); - } - - loadForms(): void { - this.formService.getAllForms().subscribe(response => { - this.parsedForms = response.forms; - }); - } - - previewForm(form: FormData) { - console.log('Preview form:', form); - } - - downloadForm(form: FormData) { - console.log('Download form:', form); - } - - deleteForm(form: FormData) { - this.formService.deleteForm(form.id).subscribe(() => { - this.parsedForms = this.parsedForms.filter(f => f.id !== form.id); - }); - } - - downloadAllForms() { - console.log('Download all forms'); - } - - clearAllForms() { - const deletePromises = this.parsedForms.map(form => - this.formService.deleteForm(form.id).toPromise() - ); - - Promise.all(deletePromises).then(() => { - this.parsedForms = []; - }); - } -} \ No newline at end of file diff --git a/frontend/src/app/components/file-upload/file-upload.component.ts b/frontend/src/app/components/file-upload/file-upload.component.ts deleted file mode 100644 index 825765b..0000000 --- a/frontend/src/app/components/file-upload/file-upload.component.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormService } from '../../services/form.service'; -import { FormValidation, ParsedForm } from '../../models/form.model'; - -@Component({ - selector: 'app-file-upload', - standalone: true, - imports: [CommonModule], - template: ` -
-
- -
- - -
-
- -
-
-
-

Validation Result

-

Status: {{ validationResult.valid ? 'Valid' : 'Invalid' }}

-

{{ validationResult.message }}

- - - -
-

File Statistics

-
-
- Questions: {{ validationResult.questions_count || 0 }} -
-
- Answer Options: {{ validationResult.options_count || 0 }} -
-
-
- -
-

Sheet Details

-
-
-
-
{{ sheet.name }}
- {{ sheet.exists && sheet.missing_columns.length === 0 ? '✓' : '✗' }} -
-
-

Exists: {{ sheet.exists ? 'Yes' : 'No' }}

-

Rows: {{ sheet.row_count }}

-

Columns: {{ sheet.columns.join(', ') }}

-
- Missing Columns: -
    -
  • {{ col }}
  • -
-
-
-
-
-
-
-
- -
-
-

Form Details

-

Title: {{ parsedForm.title.default }}

-

Version: {{ parsedForm.version }}

-

Number of Groups: {{ parsedForm.groups.length }}

-
-
-
-
- `, - styles: [` - .file-upload-container { - background: rgba(255, 255, 255, 0.1); - border-radius: 8px; - padding: 2rem; - margin: 2rem 0; - color: white; - display: flex; - flex-direction: column; - gap: 2rem; - } - - .upload-section { - display: flex; - flex-direction: column; - gap: 1rem; - align-items: center; - } - - .file-input { - padding: 1rem; - background: rgba(255, 255, 255, 0.05); - border: 2px solid rgba(255, 255, 255, 0.2); - border-radius: 8px; - width: 100%; - max-width: 400px; - color: white; - font-weight: 500; - transition: all 0.3s ease; - cursor: pointer; - - &:hover { - border-color: rgba(255, 255, 255, 0.4); - background: rgba(255, 255, 255, 0.1); - } - - &:focus { - outline: none; - border-color: #3f51b5; - background: rgba(63, 81, 181, 0.1); - } - - &::file-selector-button { - border: 2px solid #3f51b5; - border-radius: 6px; - padding: 6px 12px; - font-weight: 500; - transition: all 0.3s ease; - background-color: #3f51b5; - color: white; - cursor: pointer; - margin-right: 1rem; - - &:hover { - background-color: #303f9f; - border-color: #303f9f; - } - } - } - - .button-group { - display: flex; - gap: 1rem; - } - - .action-button { - padding: 0.5rem 1rem; - border: none; - border-radius: 4px; - cursor: pointer; - font-weight: 500; - transition: all 0.3s ease; - } - - .action-button:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .validate { - background: #4CAF50; - color: white; - } - - .upload { - background: #2196F3; - color: white; - } - - .results-container { - display: flex; - flex-direction: column; - gap: 1.5rem; - } - - .result-section { - width: 100%; - } - - .validation-result, .form-details { - background: rgba(255, 255, 255, 0.05); - padding: 1.5rem; - border-radius: 4px; - width: 100%; - } - - .validation-result.valid { - border-left: 4px solid #4CAF50; - } - - .validation-result.invalid { - border-left: 4px solid #f44336; - } - - .status { - font-weight: 600; - margin-bottom: 0.5rem; - } - - .message { - color: rgba(255, 255, 255, 0.8); - margin-bottom: 1rem; - } - - h4 { - margin: 0 0 0.5rem 0; - color: rgba(255, 255, 255, 0.9); - font-size: 1rem; - } - - .metadata-section, .stats-section, .sheets-section { - margin-top: 1.5rem; - padding-top: 1rem; - border-top: 1px solid rgba(255, 255, 255, 0.1); - } - - .metadata-grid, .stats-grid { - display: flex; - gap: 1rem; - } - - .metadata-item, .stat-item { - flex: 1; - background: rgba(255, 255, 255, 0.05); - padding: 0.5rem; - border-radius: 4px; - } - - .sheets-grid { - display: flex; - flex-wrap: wrap; - gap: 1rem; - } - - .sheet-item { - flex: 1 1 calc(33.33% - 1rem); - background: rgba(255, 255, 255, 0.05); - padding: 1rem; - border-radius: 4px; - border-left: 4px solid transparent; - } - - .sheet-item.valid { - border-left-color: #4CAF50; - } - - .sheet-item.invalid { - border-left-color: #f44336; - } - - .sheet-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.5rem; - } - - .sheet-header h5 { - margin: 0; - color: rgba(255, 255, 255, 0.9); - font-size: 0.9rem; - } - - .sheet-status { - padding: 0.25rem 0.5rem; - border-radius: 4px; - background: rgba(255, 255, 255, 0.1); - font-size: 0.8rem; - } - - .sheet-details p { - margin: 0.25rem 0; - font-size: 0.85rem; - } - - .missing-columns { - margin-top: 0.5rem; - } - - .missing-columns ul { - margin: 0.25rem 0; - padding-left: 1rem; - } - - .missing-columns li { - color: #f44336; - font-size: 0.85rem; - } - - h3 { - margin: 0 0 1rem 0; - color: rgba(255, 255, 255, 0.9); - font-size: 1.2rem; - } - - p { - margin: 0.5rem 0; - color: rgba(255, 255, 255, 0.7); - line-height: 1.5; - } - - strong { - color: rgba(255, 255, 255, 0.9); - } - `] -}) -export class FileUploadComponent { - selectedFile: File | null = null; - validationResult: FormValidation | null = null; - parsedForm: ParsedForm | null = null; - - constructor(private formService: FormService) {} - - onFileSelected(event: Event): void { - const input = event.target as HTMLInputElement; - if (input.files?.length) { - this.selectedFile = input.files[0]; - this.validationResult = null; - this.parsedForm = null; - } - } - - validateFile(): void { - if (this.selectedFile) { - this.formService.validateFile(this.selectedFile).subscribe({ - next: (result) => { - this.validationResult = result; - console.log('Validation result:', result); - }, - error: (error) => { - console.error('Validation error:', error); - this.validationResult = { - valid: false, - message: 'Error validating file: ' + error.message - }; - } - }); - } - } - - uploadFile(): void { - if (this.selectedFile) { - this.formService.uploadFile(this.selectedFile).subscribe({ - next: (result) => { - this.parsedForm = result; - console.log('Upload result:', result); - }, - error: (error) => { - console.error('Upload error:', error); - this.validationResult = { - valid: false, - message: 'Error uploading file: ' + error.message - }; - } - }); - } - } -} diff --git a/frontend/src/app/components/form-preview/form-preview.component.ts b/frontend/src/app/components/form-preview/form-preview.component.ts deleted file mode 100644 index cd1bb13..0000000 --- a/frontend/src/app/components/form-preview/form-preview.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-form-preview', - imports: [], - template: ` -

- form-preview works! -

- `, - styles: `` -}) -export class FormPreviewComponent { - -} diff --git a/frontend/src/app/components/navbar/navbar.component.ts b/frontend/src/app/components/navbar/navbar.component.ts index e6b2638..6eaa637 100644 --- a/frontend/src/app/components/navbar/navbar.component.ts +++ b/frontend/src/app/components/navbar/navbar.component.ts @@ -1,29 +1,18 @@ import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @Component({ selector: 'app-navbar', standalone: true, - imports: [CommonModule, MatToolbarModule, MatButtonModule, MatIconModule], + imports: [CommonModule, MatToolbarModule, MatIconModule], template: ` - `, styles: [` @@ -47,24 +36,6 @@ import { MatIconModule } from '@angular/material/icon'; width: 24px; height: 24px; } - - .navbar-actions { - margin-left: auto; - display: flex; - gap: 1rem; - } - - button { - display: flex; - align-items: center; - gap: 0.5rem; - color: white; - transition: all 0.3s ease; - - &:hover { - background: rgba(255, 255, 255, 0.1); - } - } `] }) -export class NavbarComponent {} \ No newline at end of file +export class NavbarComponent {} \ No newline at end of file diff --git a/frontend/src/app/components/search/search.component.ts b/frontend/src/app/components/search/search.component.ts index c17c2e1..67b1145 100644 --- a/frontend/src/app/components/search/search.component.ts +++ b/frontend/src/app/components/search/search.component.ts @@ -39,15 +39,16 @@ import { MatIconModule } from '@angular/material/icon'; .search-field { width: 100%; - + ::ng-deep { .mat-mdc-form-field-flex { - background: rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.04); border-radius: 8px; } .mat-mdc-text-field-wrapper { background: transparent; + padding: 0; } .mat-mdc-form-field-outline { @@ -72,12 +73,12 @@ import { MatIconModule } from '@angular/material/icon'; export class SearchComponent { searchQuery = ''; - onSearch() { - console.log('Searching for:', this.searchQuery); + onSearch(): void { + // TODO: Implement search functionality } - clearSearch() { + clearSearch(): void { this.searchQuery = ''; this.onSearch(); } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/upload/upload.component.ts b/frontend/src/app/components/upload/upload.component.ts index e2ea732..b24f2da 100644 --- a/frontend/src/app/components/upload/upload.component.ts +++ b/frontend/src/app/components/upload/upload.component.ts @@ -1,9 +1,11 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { FormService, FormData, FormDetails, OptionData } from '../../services/form.service'; +import { FormValidation } from '../../models/form.model'; @Component({ selector: 'app-upload', @@ -16,12 +18,19 @@ import { MatProgressBarModule } from '@angular/material/progress-bar'; MatProgressBarModule ], template: ` - + + cloud_upload + Upload & Manage Forms + Drag & drop Excel files or browse to upload + + +
cloud_upload

Drag & Drop Excel Files

@@ -29,26 +38,183 @@ import { MatProgressBarModule } from '@angular/material/progress-bar'; -
- -
+ + +
+
+ description + {{selectedFile.name}} +
+
+ + +
+
+ + +
{{uploadProgress}}%
-
-

Uploaded Files

-
- description - {{file.name}} - + +
+

Validation Result

+

Status: {{ validationResult.valid ? 'Valid' : 'Invalid' }}

+

{{ validationResult.message }}

+ + + +
+

File Statistics

+
+
+ Questions: {{ validationResult.questions_count || 0 }} +
+
+ Answer Options: {{ validationResult.options_count || 0 }} +
+
+
+ +
+

Sheet Details

+
+
+
+
{{ sheet.name }}
+ {{ sheet.exists && sheet.missing_columns.length === 0 ? '✓' : '✗' }} +
+
+

Exists: {{ sheet.exists ? 'Yes' : 'No' }}

+

Rows: {{ sheet.row_count }}

+

Columns: {{ sheet.columns.join(', ') }}

+
+ Missing Columns: +
    +
  • {{ col }}
  • +
+
+
+
+
+
+
+ + +
+

Parsed Forms

+
+
+ description +
+

{{form.title}}

+

{{form.language}} • {{form.version}} • {{form.created_at | date:'short'}}

+
+
+ +
+ +
+
+
+ + +
+ +
@@ -61,12 +227,53 @@ import { MatProgressBarModule } from '@angular/material/progress-bar'; border: 2px dashed rgba(255, 255, 255, 0.2); transition: all 0.3s ease; height: 100%; - min-height: 300px; + min-height: 400px; + color: white; &.dragover { border-color: #4CAF50; background: rgba(76, 175, 80, 0.1); } + + ::ng-deep { + .mat-mdc-card-header { + padding: 1rem; + } + + .mat-mdc-card-content { + display: flex; + flex-direction: column; + height: 100%; + padding: 0; + } + + .mat-mdc-card-avatar { + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; + padding: 8px; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + + mat-icon { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + } + } + + .mat-mdc-card-title { + color: white; + } + + .mat-mdc-card-subtitle { + color: rgba(255, 255, 255, 0.7); + } + } } .upload-area { @@ -77,7 +284,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar'; padding: 2rem; color: white; text-align: center; - height: 100%; + flex: 1; min-height: 200px; .upload-icon { @@ -98,29 +305,38 @@ import { MatProgressBarModule } from '@angular/material/progress-bar'; margin: 1rem 0; color: rgba(255, 255, 255, 0.6); } + } - button { - border: 2px solid; - border-radius: 8px; - padding: 8px 16px; - font-weight: 500; - transition: all 0.3s ease; - background-color: #3f51b5; - border-color: #3f51b5; - color: white; + .file-actions { + padding: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); - &:hover { - background-color: #303f9f; - border-color: #303f9f; + .selected-file { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + padding: 0.5rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + + mat-icon { + color: rgba(255, 255, 255, 0.7); } } + + .button-group { + display: flex; + gap: 1rem; + justify-content: center; + } } .upload-progress { - margin-top: 1rem; display: flex; align-items: center; gap: 1rem; + padding: 1rem; color: white; mat-progress-bar { @@ -128,39 +344,498 @@ import { MatProgressBarModule } from '@angular/material/progress-bar'; } } - .uploaded-files { - margin-top: 2rem; - color: white; + .validation-result { + padding: 1rem; + margin: 1rem; + border-radius: 4px; + background: rgba(255, 255, 255, 0.05); + + &.valid { + border-left: 4px solid #4CAF50; + } + + &.invalid { + border-left: 4px solid #f44336; + } + + h3 { + margin: 0 0 0.5rem; + font-size: 1.1rem; + } + + .status { + font-weight: 500; + margin: 0.5rem 0; + } + + .message { + margin: 0.5rem 0; + color: rgba(255, 255, 255, 0.8); + } + + .metadata-section { + margin-top: 1rem; + + h4 { + margin: 0 0 0.5rem; + font-size: 1rem; + } + + .metadata-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + + .metadata-item { + padding: 0.25rem 0; + } + } + } + + .stats-section { + margin-top: 1rem; + + h4 { + margin: 0 0 0.5rem; + font-size: 1rem; + } + + .stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + + .stat-item { + padding: 0.25rem 0; + } + } + } + + .sheets-section { + margin-top: 1rem; + + h4 { + margin: 0 0 0.5rem; + font-size: 1rem; + } + + .sheets-grid { + display: grid; + grid-template-columns: 1fr; + gap: 0.5rem; + } + + .sheet-item { + padding: 0.5rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + margin-bottom: 0.5rem; + transition: all 0.3s ease; + + &.valid { + border-left: 3px solid #4CAF50; + } + + &.invalid { + border-left: 3px solid #f44336; + } + + &:hover { + background: rgba(255, 255, 255, 0.1); + } + + .sheet-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.5rem; + + h5 { + margin: 0; + font-size: 1rem; + font-weight: 500; + } + + .sheet-status { + padding: 0.25rem 0.5rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + font-weight: 500; + } + } + + .sheet-details { + p { + margin: 0.25rem 0; + font-size: 0.875rem; + color: rgba(255, 255, 255, 0.7); + } + + .missing-columns { + margin-top: 0.5rem; + + strong { + margin-right: 0.5rem; + } + + ul { + margin: 0; + padding-left: 1rem; + } + } + } + } + } + } + + .form-list { + padding: 1rem; + max-height: 300px; + overflow-y: auto; h3 { margin: 0 0 1rem; font-size: 1.1rem; + color: white; + } + } + + .form-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + margin-bottom: 0.5rem; + transition: all 0.3s ease; + cursor: pointer; + + &:hover { + background: rgba(255, 255, 255, 0.1); + } + } + + .form-info { + display: flex; + align-items: center; + gap: 1rem; + + mat-icon { + color: rgba(255, 255, 255, 0.7); + } + } + + .form-details { + h4 { + margin: 0; + font-size: 1rem; + font-weight: 500; } - .file-item { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem; - background: rgba(255, 255, 255, 0.05); - border-radius: 4px; - margin-bottom: 0.5rem; + p { + margin: 0; + font-size: 0.875rem; + color: rgba(255, 255, 255, 0.7); + } + } - mat-icon { - color: rgba(255, 255, 255, 0.6); + .form-details-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + + .modal-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + .modal-content { + background: rgba(26, 26, 46, 0.95); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 2rem; + border-radius: 8px; + width: 80%; + max-width: 800px; + max-height: 80vh; + overflow-y: auto; + position: relative; + color: white; + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + h2 { + margin: 0; + color: white; + font-size: 1.5rem; + font-weight: 500; + } + + button { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + color: white; + + &:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.3); + } + + mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + } + } } - span { - flex: 1; + .modal-body { + .form-info-section { + margin-bottom: 2rem; + padding: 1rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + + h3 { + margin: 0 0 1rem; + color: white; + font-size: 1.2rem; + font-weight: 500; + } + + .info-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; + + .info-item { + padding: 0.5rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + + strong { + display: block; + margin-bottom: 0.25rem; + color: rgba(255, 255, 255, 0.8); + font-size: 0.875rem; + } + + span { + color: white; + font-weight: 500; + } + } + } + } + + .questions-section { + margin-top: 2rem; + + h3 { + margin: 0 0 1rem; + color: white; + font-size: 1.2rem; + font-weight: 500; + } + + .questions-list { + .question-item { + margin-bottom: 1rem; + padding: 1rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + border-left: 3px solid #3f51b5; + + .question-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 0.5rem; + + .question-number { + background: #3f51b5; + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: bold; + } + + h4 { + margin: 0; + color: white; + font-size: 1rem; + font-weight: 500; + flex: 1; + } + + .question-type { + padding: 0.25rem 0.5rem; + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.8); + } + } + + .question-details { + margin-bottom: 0.5rem; + + p { + margin: 0.25rem 0; + color: rgba(255, 255, 255, 0.7); + font-size: 0.875rem; + + strong { + color: rgba(255, 255, 255, 0.9); + } + } + } + + .question-options { + margin-top: 0.5rem; + + h5 { + margin: 0 0 0.5rem; + color: rgba(255, 255, 255, 0.8); + font-size: 0.875rem; + } + + .options-list { + .option-item { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.25rem; + padding: 0.25rem 0.5rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 4px; + + .option-id { + background: rgba(255, 255, 255, 0.1); + color: white; + padding: 0.125rem 0.375rem; + border-radius: 3px; + font-size: 0.75rem; + font-weight: bold; + min-width: 20px; + text-align: center; + } + + .option-label { + flex: 1; + color: white; + font-size: 0.875rem; + } + + .option-order { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.5); + } + } + } + } + } + } + } + + .all-options-section { + margin-top: 2rem; + padding: 1rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + + h3 { + margin: 0 0 1rem; + color: white; + font-size: 1.2rem; + font-weight: 500; + } + + .options-list { + .option-item { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + padding: 0.5rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + + .option-id { + background: rgba(255, 255, 255, 0.1); + color: white; + padding: 0.25rem 0.5rem; + border-radius: 3px; + font-size: 0.75rem; + font-weight: bold; + min-width: 30px; + text-align: center; + } + + .option-label { + flex: 1; + color: white; + font-size: 0.875rem; + } + + .option-order { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.5); + } + } + } + } } } } `] }) -export class UploadComponent { +export class UploadComponent implements OnInit { isDragOver = false; + isUploading = false; + isValidating = false; uploadProgress = 0; - uploadedFiles: File[] = []; + selectedFile: File | null = null; + validationResult: FormValidation | null = null; + parsedForms: FormData[] = []; + selectedFormDetails: FormDetails | null = null; + + constructor(private formService: FormService) {} + + ngOnInit(): void { + this.loadForms(); + } onDragOver(event: DragEvent) { event.preventDefault(); @@ -180,47 +855,122 @@ export class UploadComponent { this.isDragOver = false; const files = event.dataTransfer?.files; - if (files) { - this.handleFiles(Array.from(files)); + if (files && files.length > 0) { + this.selectedFile = files[0]; + this.validationResult = null; } } onFileSelected(event: Event) { const input = event.target as HTMLInputElement; - if (input.files) { - this.handleFiles(Array.from(input.files)); + if (input.files && input.files.length > 0) { + this.selectedFile = input.files[0]; + this.validationResult = null; } + input.value = ''; } - handleFiles(files: File[]) { - const excelFiles = files.filter(file => - file.type === 'application/vnd.ms-excel' || - file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - ); + validateFile() { + if (!this.selectedFile) return; - if (excelFiles.length > 0) { - this.uploadedFiles.push(...excelFiles); - this.simulateUpload(); - } + this.isValidating = true; + this.formService.validateFile(this.selectedFile).subscribe({ + next: (result) => { + this.validationResult = result; + this.isValidating = false; + }, + error: (error: any) => { + console.error('Validation failed:', error); + this.isValidating = false; + } + }); } - simulateUpload() { + uploadFile() { + if (!this.selectedFile || !this.validationResult?.valid) return; + + this.isUploading = true; this.uploadProgress = 0; - const interval = setInterval(() => { - this.uploadProgress += 10; - if (this.uploadProgress >= 100) { - clearInterval(interval); - setTimeout(() => { - this.uploadProgress = 0; - }, 500); + + const progressInterval = setInterval(() => { + this.uploadProgress = Math.min(this.uploadProgress + 10, 90); + if (this.uploadProgress >= 90) { + clearInterval(progressInterval); } }, 200); + + this.formService.uploadFile(this.selectedFile).subscribe({ + next: () => { + clearInterval(progressInterval); + this.uploadProgress = 100; + setTimeout(() => { + this.resetUploadState(); + }, 500); + }, + error: (error: any) => { + clearInterval(progressInterval); + this.resetUploadState(); + console.error('Upload failed:', error); + } + }); } - removeFile(file: File) { - const index = this.uploadedFiles.indexOf(file); - if (index > -1) { - this.uploadedFiles.splice(index, 1); + private resetUploadState(): void { + this.isUploading = false; + this.uploadProgress = 0; + this.selectedFile = null; + this.validationResult = null; + this.loadForms(); + } + + loadForms(): void { + this.formService.getAllForms().subscribe({ + next: (response) => { + this.parsedForms = response.forms; + }, + error: (error: any) => { + console.error('Failed to load forms:', error); + } + }); + } + + deleteForm(form: FormData, event: Event): void { + event.stopPropagation(); + if (confirm(`Are you sure you want to delete "${form.title}"?`)) { + this.formService.deleteForm(form.id).subscribe({ + next: () => { + this.loadForms(); + }, + error: (error: any) => { + console.error('Failed to delete form:', error); + } + }); } } -} \ No newline at end of file + + showFormDetails(form: FormData): void { + this.formService.getFormById(form.id).subscribe({ + next: (response: FormDetails) => { + this.selectedFormDetails = response; + }, + error: (error: any) => { + console.error('Failed to load form details:', error); + } + }); + } + + closeFormDetails(): void { + this.selectedFormDetails = null; + } + + getQuestionOptions(questionId: string): OptionData[] { + if (!this.selectedFormDetails) return []; + + const question = this.selectedFormDetails.questions.find(q => q.id === questionId); + if (!question) return []; + + return this.selectedFormDetails.options.filter(option => + option.order === question.order + ); + } +} diff --git a/frontend/src/app/core/core.module.ts b/frontend/src/app/core/core.module.ts deleted file mode 100644 index 339a911..0000000 --- a/frontend/src/app/core/core.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - - - -@NgModule({ - declarations: [], - imports: [ - CommonModule - ] -}) -export class CoreModule { } diff --git a/frontend/src/app/models/form.model.ts b/frontend/src/app/models/form.model.ts index 45f803a..a9b3427 100644 --- a/frontend/src/app/models/form.model.ts +++ b/frontend/src/app/models/form.model.ts @@ -52,4 +52,4 @@ export interface ParsedForm { version: string; groups: FormGroup[]; settings?: { [key: string]: string }; -} \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/app/services/form.service.ts b/frontend/src/app/services/form.service.ts index a862d95..29c46d3 100644 --- a/frontend/src/app/services/form.service.ts +++ b/frontend/src/app/services/form.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { FormValidation, ParsedForm } from '../models/form.model'; +import { FormValidation } from '../models/form.model'; export interface FormData { id: string; @@ -47,7 +47,7 @@ export interface FormsResponse { providedIn: 'root' }) export class FormService { - private apiUrl = 'http://localhost:8000/api'; + private readonly apiUrl = 'http://localhost:8000/api'; constructor(private http: HttpClient) { } @@ -57,10 +57,10 @@ export class FormService { return this.http.post(`${this.apiUrl}/validate`, formData); } - uploadFile(file: File): Observable { + uploadFile(file: File): Observable { const formData = new FormData(); formData.append('file', file); - return this.http.post(`${this.apiUrl}/upload`, formData); + return this.http.post(`${this.apiUrl}/upload`, formData); } getAllForms(): Observable { @@ -74,4 +74,4 @@ export class FormService { deleteForm(formId: string): Observable<{ message: string }> { return this.http.delete<{ message: string }>(`${this.apiUrl}/forms/${formId}`); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts deleted file mode 100644 index 76cf203..0000000 --- a/frontend/src/app/shared/shared.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - - - -@NgModule({ - declarations: [], - imports: [ - CommonModule - ] -}) -export class SharedModule { } diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 5fc93f4..900804c 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1,5 +1,5 @@ @import '@angular/material/prebuilt-themes/purple-green.css'; .mat-typography { - margin: 0 !important; + margin: 0; } diff --git a/run.sh b/run.sh index 268f96a..b4905a4 100755 --- a/run.sh +++ b/run.sh @@ -1,44 +1,27 @@ #!/bin/bash -# Define your project root (where this script is located) PROJECT_ROOT=$(pwd) - -# Define the paths relative to the project root FRONTEND_PATH="$PROJECT_ROOT/frontend" BACKEND_PATH="$PROJECT_ROOT/backend" - -# Name of your tmux session SESSION_NAME="dev-project" -# Check if the tmux session already exists -tmux has-session -t "$SESSION_NAME" 2>/dev/null +if ! tmux has-session -t "$SESSION_NAME"; then + tmux new-session -s "$SESSION_NAME" -d + tmux split-window -v -t "$SESSION_NAME:0.0" -if [ $? != 0 ]; then - echo "Creating new tmux session: $SESSION_NAME" - # Create a new tmux session and window - tmux new-session -s "$SESSION_NAME" -d + # --- Top Pane (Frontend) --- + tmux send-keys -t "$SESSION_NAME:0.0" "cd $FRONTEND_PATH" C-m + tmux send-keys -t "$SESSION_NAME:0.0" "ng serve" C-m - # Split the window into two panes - tmux split-window -v -t "$SESSION_NAME:0.0" # Splits the first pane vertically + # --- Bottom Pane (Backend) --- + tmux send-keys -t "$SESSION_NAME:0.0" "select-pane -t 1" C-m + tmux send-keys -t "$SESSION_NAME:0.1" "cd $BACKEND_PATH" C-m + tmux send-keys -t "$SESSION_NAME:0.1" "source .venv/bin/activate" C-m + tmux send-keys -t "$SESSION_NAME:0.1" "uvicorn main:app --reload" C-m - # --- Top Pane (Frontend) --- - echo "Setting up top pane (frontend)..." - tmux send-keys -t "$SESSION_NAME:0.0" "cd $FRONTEND_PATH" C-m - tmux send-keys -t "$SESSION_NAME:0.0" "echo 'Starting Angular development server...'" C-m - tmux send-keys -t "$SESSION_NAME:0.0" "ng serve" C-m - - # --- Bottom Pane (Backend) --- - echo "Setting up bottom pane (backend)..." - tmux send-keys -t "$SESSION_NAME:0.0" "select-pane -t 1" C-m # Select the bottom pane - tmux send-keys -t "$SESSION_NAME:0.1" "cd $BACKEND_PATH" C-m - tmux send-keys -t "$SESSION_NAME:0.1" "echo 'Activating Python virtual environment...'" C-m - tmux send-keys -t "$SESSION_NAME:0.1" "source .venv/bin/activate" C-m - tmux send-keys -t "$SESSION_NAME:0.1" "echo 'Starting FastAPI development server...'" C-m - tmux send-keys -t "$SESSION_NAME:0.1" "uvicorn main:app --reload" C-m - - echo "Tmux session '$SESSION_NAME' created. Attaching..." - tmux attach-session -t "$SESSION_NAME" + tmux attach-session -t "$SESSION_NAME" else - echo "Tmux session '$SESSION_NAME' already exists. Attaching..." - tmux attach-session -t "$SESSION_NAME" + echo "Tmux session '$SESSION_NAME' already exists. Attaching..." + tmux attach-session -t "$SESSION_NAME" fi +