diff --git a/package.json b/package.json index 238df0115d..c10d60d913 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "ngx-window-token": "^7.0.0", "rxjs": "~7.8.0", "tslib": "^2.6.2", + "xlsx": "^0.18.5", "zone.js": "~0.13.0" }, "devDependencies": { diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index 7ceb29ef03..88c6f5b018 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -145,6 +145,13 @@

{{ headerSubtitle }}

{{ 'ENUM.NAV_BAR_NAME.ADMINISTRATION' | translate }} + + + @@ -46,7 +48,9 @@

{{ 'TO' | translate }}

- clear + clear
diff --git a/src/app/shared/components/institution-hierarchy/institution-hierarchy.component.html b/src/app/shared/components/institution-hierarchy/institution-hierarchy.component.html index 01ea386ade..d17faeaa80 100644 --- a/src/app/shared/components/institution-hierarchy/institution-hierarchy.component.html +++ b/src/app/shared/components/institution-hierarchy/institution-hierarchy.component.html @@ -12,10 +12,10 @@ - + - + console.log(data)); } public onHierarchyLevelSelect(hierarchy: HierarchyElement): void { diff --git a/src/app/shared/constants/regex-constants.ts b/src/app/shared/constants/regex-constants.ts index f21cf81a6b..d0591811dd 100644 --- a/src/app/shared/constants/regex-constants.ts +++ b/src/app/shared/constants/regex-constants.ts @@ -17,6 +17,9 @@ export const EMAIL_REGEX: RegExp = /^[\w.-]+@([\w.-]+\.)+[\w.-]{2,6}$/; // Regex for EDRPOU and IPN export const EDRPOU_IPN_REGEX: RegExp = /^(\d{8}|\d{10})$/; +// Regex for RNOKPP +export const RNOKPP_REGEX: RegExp = /^(\d{10})$/; + // Regex for non-latin characters export const NO_LATIN_REGEX: RegExp = /^[А-ЩЬЮЯҐЄІЇа-щьюяґєії0-9.,_\s\-’!@#$%^/&*()+={}\\|<>~`':;"]+$/; diff --git a/src/app/shared/enum/enumUA/import-export.ts b/src/app/shared/enum/enumUA/import-export.ts new file mode 100644 index 0000000000..f10c33ed22 --- /dev/null +++ b/src/app/shared/enum/enumUA/import-export.ts @@ -0,0 +1,22 @@ +export enum ImportEmployeesColumnsNames { + sequenceNumber = 'sequenceNumber', + employeeSurname = 'employeeSurname', + employeeName = 'employeeName', + employeeFatherName = 'employeeFatherName', + employeeRNOKPP = 'employeeRNOKPP', + employeeAssignedRole = 'employeeAssignedRole' +} + +export enum ImportEmployeesStandardHeaders { + sequenceNumber = '№', + employeeSurname = 'Прізвище', + employeeName = 'Імя', + employeeFatherName = 'По батькові', + employeeRNOKPP = 'РНОКПП', + employeeAssignedRole = 'Призначені ролі' +} + +export enum ImportEmployeesChosenRole { + employee = 'Співробітник ЗО', + deputyDirector = 'Заступник директора ЗО' +} diff --git a/src/app/shared/models/admin-import-export.model.ts b/src/app/shared/models/admin-import-export.model.ts new file mode 100644 index 0000000000..60c1c9a127 --- /dev/null +++ b/src/app/shared/models/admin-import-export.model.ts @@ -0,0 +1,51 @@ +export interface ValidEmployee { + id: number; + employeeSurname: string; + employeeName: string; + employeeFatherName: string; + employeeRNOKPP: number; + employeeAssignedRole: string; +} +export interface Employee { + sequenceNumber: number; + employeeSurname: string; + employeeName: string; + employeeFatherName: string; + employeeRNOKPP: number; + employeeAssignedRole: string; + errors: EmployeeValidationErrors; +} + +export interface EmployeeValidationErrors { + employeeSurnameEmpty?: boolean; + employeeNameEmpty?: boolean; + employeeFatherNameEmpty?: boolean; + employeeRNOKPPEmpty?: boolean; + employeeAssignedRoleEmpty?: boolean; + + employeeSurnameLength?: boolean; + employeeNameLength?: boolean; + employeeFatherNameLength?: boolean; + + employeeSurnameLanguage?: boolean; + employeeNameLanguage?: boolean; + employeeFatherNameLanguage?: boolean; + + employeeRNOKPPFormat?: boolean; + employeeRNOKPPDuplicate?: boolean; + employeeAssignedRoleFormat?: boolean; +} + +export interface FieldValidationConfig { + checkEmpty?: boolean; + checkLength?: boolean; + checkLanguage?: boolean; + checkAssignedRole?: boolean; + checkRNOKPP?: boolean; + checkDuplicate?: boolean; +} + +export interface FieldsConfig { + fieldName: string; + validationParam: FieldValidationConfig; +} diff --git a/src/app/shared/services/employee-upload-processor/employee-upload-processor.service.spec.ts b/src/app/shared/services/employee-upload-processor/employee-upload-processor.service.spec.ts new file mode 100644 index 0000000000..2aad0f79f3 --- /dev/null +++ b/src/app/shared/services/employee-upload-processor/employee-upload-processor.service.spec.ts @@ -0,0 +1,52 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { HttpResponse } from '@angular/common/http'; +import { EmployeeUploadProcessorService } from './employee-upload-processor.service'; + +describe('EmployeeUploadProcessorService', () => { + let service: EmployeeUploadProcessorService; + let httpTestingController: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [EmployeeUploadProcessorService] + }); + service = TestBed.inject(EmployeeUploadProcessorService); + httpTestingController = TestBed.inject(HttpTestingController); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call HttpClient.put with correct parameters and return response', () => { + const mockResponse = 'Upload successful'; + const mockItems = [{ name: 'Employee1' }]; + const mockId = '123'; + service.uploadEmployeesList(mockItems, mockId).subscribe((response) => { + expect(response.body).toBe(mockResponse); + expect(response.status).toBe(200); + }); + const req = httpTestingController.expectOne('/api/v1/Provider/Upload/123/employees/upload'); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual(mockItems); + expect(req.request.responseType).toBe('text'); + req.flush(mockResponse, { status: 200, statusText: 'OK' }); + httpTestingController.verify(); + }); + + it('should handle error response from HttpClient', () => { + const mockItems = [{ name: 'Employee1' }]; + const mockId = '123'; + service.uploadEmployeesList(mockItems, mockId).subscribe( + () => {}, + (error) => { + expect(error).toEqual(new HttpResponse({ body: 'Error uploading employees', status: 500 })); + } + ); + const req = httpTestingController.expectOne('/api/v1/Provider/Upload/123/employees/upload'); + req.flush('Error uploading employees', { status: 500, statusText: 'Internal Server Error' }); + httpTestingController.verify(); + }); +}); diff --git a/src/app/shared/services/employee-upload-processor/employee-upload-processor.service.ts b/src/app/shared/services/employee-upload-processor/employee-upload-processor.service.ts new file mode 100644 index 0000000000..a7854c3bae --- /dev/null +++ b/src/app/shared/services/employee-upload-processor/employee-upload-processor.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class EmployeeUploadProcessorService { + public readonly baseApiURL = '/api/v1'; + constructor(private readonly http: HttpClient) {} + + public uploadEmployeesList(items: unknown, id: string): Observable> { + return this.http.put(`${this.baseApiURL}/Provider/Upload/${id}/employees/upload`, items, { + observe: 'response', + responseType: 'text' + }); + } +} diff --git a/src/app/shared/services/excel-upload-processor/excel-upload-processor.service.spec.ts b/src/app/shared/services/excel-upload-processor/excel-upload-processor.service.spec.ts new file mode 100644 index 0000000000..9960728653 --- /dev/null +++ b/src/app/shared/services/excel-upload-processor/excel-upload-processor.service.spec.ts @@ -0,0 +1,153 @@ +import { TestBed } from '@angular/core/testing'; +import { TranslateService } from '@ngx-translate/core'; +import * as XLSX from 'xlsx'; +import { ExcelUploadProcessorService } from './excel-upload-processor.service'; + +describe('ExcelUploadProcessorService', () => { + let service: ExcelUploadProcessorService; + let translateService: jest.Mocked; + + beforeEach(() => { + translateService = { + instant: jest.fn((key: string) => key) + } as unknown as jest.Mocked; + + TestBed.configureTestingModule({ + providers: [ExcelUploadProcessorService, { provide: TranslateService, useValue: translateService }] + }); + + service = TestBed.inject(ExcelUploadProcessorService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should set loading state to true and false', () => { + service.isLoading$.subscribe((isLoading) => { + expect(isLoading).toBe(false); + }); + + service.setLoading(true); + service.isLoading$.subscribe((isLoading) => { + expect(isLoading).toBe(true); + }); + + service.setLoading(false); + service.isLoading$.subscribe((isLoading) => { + expect(isLoading).toBe(false); + }); + }); + + it('should call alert with the correct message', () => { + const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {}); + const testMessage = 'Test Alert'; + service.showAlert(testMessage); + expect(alertSpy).toHaveBeenCalledWith(testMessage); + }); + + it('should handle invalid headers correctly', () => { + const headers = ['Header1', 'InvalidHeader']; + const validHeaders = ['Header1', 'Header2']; + const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {}); + const result = service.checkHeadersIsValid(headers, validHeaders); + + expect(result).toBe(false); + expect(alertSpy).toHaveBeenCalledWith(expect.stringContaining('IMPORT/EXPORT.FILE_HEADERS_WARNING')); + }); + + it('should validate headers correctly', () => { + const headers = ['Header1', 'Header2']; + const validHeaders = ['Header1', 'Header2']; + const result = service.checkHeadersIsValid(headers, validHeaders); + + expect(result).toBe(true); + }); + + it('should handle valid Excel file and return parsed data', (done) => { + const mockFile = new File(['test'], 'test.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + const mockHeaders = ['Header1', 'Header2']; + const mockData = [{ Header1: 'Value1', Header2: 'Value2' }]; + const mockWorkBook = { + SheetNames: ['Sheet1'], + Sheets: { Sheet1: {} } + } as XLSX.WorkBook; + jest.spyOn(service, 'getCurrentHeaders').mockReturnValue(mockHeaders); + jest.spyOn(service, 'getItemsData').mockReturnValue(mockData); + jest.spyOn(service, 'checkHeadersIsValid').mockReturnValue(true); + jest.spyOn(service as any, 'showAlert').mockImplementation(() => {}); + const fileReaderMock = { + readAsArrayBuffer: jest.fn(), + onload: null as any, + onerror: null as any + }; + jest.spyOn(globalThis, 'FileReader').mockImplementation(() => fileReaderMock as unknown as FileReader); + jest.spyOn(XLSX, 'read').mockReturnValue(mockWorkBook); + const standartHeadersBase = ['Header1', 'Header2']; + const columnNamesBase = ['Header1', 'Header2']; + service.convertExcelToJSON(mockFile, standartHeadersBase, columnNamesBase).subscribe({ + next: (result) => { + expect(result).toEqual(mockData); + done(); + }, + error: () => { + fail('Should not throw error for valid data'); + } + }); + fileReaderMock.onload?.({ target: { result: new ArrayBuffer(8) } } as any); + }); + + it('should handle invalid headers and show an alert', (done) => { + const mockFile = new File(['test'], 'test.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + const mockHeaders = ['InvalidHeader1', 'InvalidHeader2']; + const mockWorkBook = { + SheetNames: ['Sheet1'], + Sheets: { Sheet1: {} } + } as XLSX.WorkBook; + jest.spyOn(service, 'getCurrentHeaders').mockReturnValue(mockHeaders); + jest.spyOn(service, 'checkHeadersIsValid').mockReturnValue(false); + jest.spyOn(service as any, 'showAlert').mockImplementation(() => {}); + const fileReaderMock = { + readAsArrayBuffer: jest.fn(), + onload: null as any, + onerror: null as any + }; + jest.spyOn(globalThis, 'FileReader').mockImplementation(() => fileReaderMock as unknown as FileReader); + jest.spyOn(XLSX, 'read').mockReturnValue(mockWorkBook); + const standartHeadersBase = ['Header1', 'Header2']; + const columnNamesBase = ['Header1', 'Header2']; + service.convertExcelToJSON(mockFile, standartHeadersBase, columnNamesBase).subscribe({ + next: () => { + fail('Should not emit next for invalid headers'); + }, + error: (error) => { + expect(error).toEqual('Заголовки не відповідають очікуваним'); + done(); + } + }); + fileReaderMock.onload?.({ target: { result: new ArrayBuffer(8) } } as any); + }); + + it('should handle FileReader error and show an alert', (done) => { + const mockFile = new File(['test'], 'test.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + const fileReaderMock = { + readAsArrayBuffer: jest.fn(), + onload: null as any, + onerror: null as any + }; + jest.spyOn(globalThis, 'FileReader').mockImplementation(() => fileReaderMock as unknown as FileReader); + jest.spyOn(service as any, 'showAlert').mockImplementation(() => {}); + const standartHeadersBase = ['Header1', 'Header2']; + const columnNamesBase = ['Header1', 'Header2']; + service.convertExcelToJSON(mockFile, standartHeadersBase, columnNamesBase).subscribe({ + next: () => { + fail('Should not emit next for FileReader error'); + }, + error: (error) => { + expect(error).toEqual('Помилка при читанні файлу'); + done(); + } + }); + fileReaderMock.onerror?.(new ProgressEvent('error', { bubbles: true })); + }); +}); diff --git a/src/app/shared/services/excel-upload-processor/excel-upload-processor.service.ts b/src/app/shared/services/excel-upload-processor/excel-upload-processor.service.ts new file mode 100644 index 0000000000..14e3b58953 --- /dev/null +++ b/src/app/shared/services/excel-upload-processor/excel-upload-processor.service.ts @@ -0,0 +1,93 @@ +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, BehaviorSubject } from 'rxjs'; +import * as XLSX from 'xlsx/xlsx.mjs'; + +@Injectable({ + providedIn: 'root' +}) +export class ExcelUploadProcessorService { + public readonly isLoadingSubject = new BehaviorSubject(false); + public isLoading$ = this.isLoadingSubject.asObservable(); + + constructor(public readonly translate: TranslateService) {} + + public convertExcelToJSON(file: File, standartHeadersBase: string[], columnNamesBase: string[]): Observable { + return new Observable((observer) => { + const reader: FileReader = new FileReader(); + this.setLoading(true); + reader.onerror = (): void => { + this.showAlert(this.translate.instant('IMPORT/EXPORT.FILE_READER_WARNING')); + this.setLoading(false); + observer.error('Помилка при читанні файлу'); + }; + + reader.onload = (e: any): void => { + try { + const binaryString = new Uint8Array(e.target.result); + const workBook: XLSX.WorkBook = XLSX.read(binaryString, { type: 'array', WTF: true, raw: true, cellFormula: false }); + const wsname = workBook.SheetNames[0]; + const currentHeaders = this.getCurrentHeaders(workBook, wsname); + if (this.checkHeadersIsValid(currentHeaders, standartHeadersBase)) { + const items = this.getItemsData(workBook, wsname, columnNamesBase) as unknown as any[]; + this.setLoading(false); + observer.next(items); + observer.complete(); + } else { + this.setLoading(false); + observer.error('Заголовки не відповідають очікуваним'); + } + } catch (error) { + this.showAlert(this.translate.instant('IMPORT/EXPORT.FILE_READER_WARNING')); + this.setLoading(false); + observer.error(error); + } + }; + reader.readAsArrayBuffer(file); + }); + } + + public getCurrentHeaders(workBook: XLSX.WorkBook, wsname: string): string[] { + return XLSX.utils.sheet_to_json(workBook.Sheets[wsname], { header: 1 }).shift(); + } + + /** + * This method get providers from .xlsx file. + * The "header" option sets the correspondence between the key in the object and the header + * in the file (header:Director`s name = key:directorsName)the order is strict + * @returns array of objects,each object is provider`s data + */ + public getItemsData(workBook: XLSX.WorkBook, wsname: string, columnNamesBase: string[]): any[] { + return XLSX.utils.sheet_to_json(workBook.Sheets[wsname], { + header: columnNamesBase, + range: 1 + }); + } + + public checkHeadersIsValid(currentHeaders: string[], standartHeadersBase: string[]): boolean { + const isValid = standartHeadersBase.every((header, index) => { + const currentHeader = currentHeaders[index]; + return currentHeader && currentHeader.trim() === header; + }); + if (!isValid) { + const invalidHeader = currentHeaders.find((header, index) => { + const currentHeader = header || ''; + return currentHeader.trim() !== standartHeadersBase[index]; + }); + const headerMessage = invalidHeader ? invalidHeader : this.translate.instant('IMPORT/EXPORT.FILE_EMPTY_HEADER_WARNING'); + this.showAlert( + `${this.translate.instant('IMPORT/EXPORT.FILE_HEADERS_WARNING')}"${headerMessage}", + \n\n${this.translate.instant('IMPORT/EXPORT.FILE_HEADERS_EXAMPLE')}:\n${standartHeadersBase.join(' | ')}` + ); + } + return isValid; + } + + public setLoading(isLoading: boolean): void { + this.isLoadingSubject.next(isLoading); + } + + public showAlert(message: string): void { + alert(message); + } +} diff --git a/src/app/shared/services/import-validation/import-validation.service.spec.ts b/src/app/shared/services/import-validation/import-validation.service.spec.ts new file mode 100644 index 0000000000..8b2727849a --- /dev/null +++ b/src/app/shared/services/import-validation/import-validation.service.spec.ts @@ -0,0 +1,113 @@ +import { TestBed } from '@angular/core/testing'; +import { FieldsConfig } from 'shared/models/admin-import-export.model'; +import { ImportValidationService } from './import-validation.service'; + +describe('ImportValidationService', () => { + let service: ImportValidationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ImportValidationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + it('should validate fields and mark errors through checkForInvalidData', () => { + const items = [{ name: '', role: 'invalidRole', errors: {} }]; + const config = [ + { + fieldName: 'name', + validationParam: { checkEmpty: true, checkLength: false, checkLanguage: false, checkRNOKPP: false, checkAssignedRole: false } + }, + { + fieldName: 'role', + validationParam: { checkAssignedRole: true, checkEmpty: false, checkLength: false, checkLanguage: false, checkRNOKPP: false } + } + ]; + + service.checkForInvalidData(items, config); + + expect(items[0].errors).toEqual({ + nameEmpty: true, + roleFormat: true + }); + }); + + it('should mark error when length is less than 2 and config.checkLength is true', () => { + const items = [{ name: 'a', errors: {} }]; + const config = [ + { + fieldName: 'name', + validationParam: { checkLength: true, checkEmpty: false, checkLanguage: false, checkRNOKPP: false, checkAssignedRole: false } + } + ]; + + service.checkForInvalidData(items, config); + + expect(items[0].errors).toEqual({ nameLength: true }); + }); + + it('should mark error when length is more than 50 and config.checkLength is true', () => { + const items = [{ name: 'a'.repeat(51), errors: {} }]; + const config = [ + { + fieldName: 'name', + validationParam: { checkLength: true, checkEmpty: false, checkLanguage: false, checkRNOKPP: false, checkAssignedRole: false } + } + ]; + + service.checkForInvalidData(items, config); + + expect(items[0].errors).toEqual({ nameLength: true }); + }); + + it('should mark error when language is not valid and config.checkLanguage is true', () => { + const items = [{ name: 'abc123', errors: {} }]; + const config = [ + { + fieldName: 'name', + validationParam: { checkLanguage: true, checkLength: false, checkEmpty: false, checkRNOKPP: false, checkAssignedRole: false } + } + ]; + + service.checkForInvalidData(items, config); + + expect(items[0].errors).toEqual({ nameLanguage: true }); + }); + + it('should not mark an error for RNOKPP format when config.checkRNOKPP is true but not handled', () => { + const items = [{ name: '1233454', errors: {} }]; // Assume this RNOKPP format is valid per service logic + const config = [ + { + fieldName: 'name', + validationParam: { + checkRNOKPP: true, // Indicates RNOKPP logic (currently not implemented in the service) + checkLength: false, // Disabled to avoid conflicting with RNOKPP + checkEmpty: false, + checkLanguage: false, + checkAssignedRole: false, + checkDuplicate: false + } + } + ]; + + service.checkForInvalidData(items, config); + + // Since RNOKPP is not explicitly handled, no errors should be added + expect(items[0].errors).toEqual({}); + }); + it('should mark error when assigned role is invalid and config.checkAssignedRole is true', () => { + const items = [{ role: 'invalidRole', errors: {} }]; + const config = [ + { + fieldName: 'role', + validationParam: { checkAssignedRole: true, checkLength: false, checkEmpty: false, checkLanguage: false, checkRNOKPP: false } + } + ]; + + service.checkForInvalidData(items, config); + + expect(items[0].errors).toEqual({ roleFormat: true }); + }); +}); diff --git a/src/app/shared/services/import-validation/import-validation.service.ts b/src/app/shared/services/import-validation/import-validation.service.ts new file mode 100644 index 0000000000..f934feec91 --- /dev/null +++ b/src/app/shared/services/import-validation/import-validation.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { NO_LATIN_REGEX } from 'shared/constants/regex-constants'; +import { ImportEmployeesChosenRole } from 'shared/enum/enumUA/import-export'; +import { FieldValidationConfig, FieldsConfig } from 'shared/models/admin-import-export.model'; + +@Injectable({ + providedIn: 'root' +}) +export class ImportValidationService { + constructor() {} + public checkForInvalidData(items: any[], config: FieldsConfig[]): void { + items.forEach((item) => { + this.findDuplicates(items, item); + item.errors = {}; + config.forEach((field) => { + this.validateField(field.fieldName, item, items, field.validationParam); + }); + }); + } + + public findDuplicates(items: any[], item: unknown): boolean { + const rnokppList = items.map(({ employeeRNOKPP }) => employeeRNOKPP); + const result = rnokppList.filter((e) => e === item); + return result.length > 1; + } + + private validateField(fieldName: string, item: any, items: any, config: FieldValidationConfig): void { + if (config.checkEmpty && !item[fieldName]) { + item.errors[`${fieldName}Empty`] = true; + } else if (config.checkLength && (item[fieldName].length <= 2 || item[fieldName].length > 50)) { + item.errors[`${fieldName}Length`] = true; + } else if (config.checkLanguage && !NO_LATIN_REGEX.test(item[fieldName])) { + item.errors[`${fieldName}Language`] = true; + } else if (config.checkDuplicate && this.findDuplicates(items, item[fieldName])) { + item.errors[`${fieldName}Duplicate`] = true; + } else if ( + config.checkAssignedRole && + item[fieldName] !== ImportEmployeesChosenRole.employee && + item[fieldName] !== ImportEmployeesChosenRole.deputyDirector + ) { + item.errors[`${fieldName}Format`] = true; + } + } +} diff --git a/src/app/shared/styles/details.scss b/src/app/shared/styles/details.scss index c8e29ec416..56a0d91b06 100644 --- a/src/app/shared/styles/details.scss +++ b/src/app/shared/styles/details.scss @@ -146,8 +146,8 @@ object-fit: contain; } -@media(max-width: 1200px) { +@media (max-width: 1200px) { .coverImage { min-height: inherit; } -} \ No newline at end of file +} diff --git a/src/app/shared/utils/admin.utils.ts b/src/app/shared/utils/admin.utils.ts index 75e3e5bebe..bb588669ea 100644 --- a/src/app/shared/utils/admin.utils.ts +++ b/src/app/shared/utils/admin.utils.ts @@ -41,3 +41,7 @@ export function canManageInstitution(role: string): boolean { export function canManageRegion(role: string): boolean { return canManageInstitution(role) || role === Role.regionAdmin; } + +export function canManageImports(role: string): boolean { + return role === Role.techAdmin; +} diff --git a/src/app/shell/admin-tools/admin-tools.component.html b/src/app/shell/admin-tools/admin-tools.component.html index 51f4a3080c..cdfcee3f66 100644 --- a/src/app/shell/admin-tools/admin-tools.component.html +++ b/src/app/shell/admin-tools/admin-tools.component.html @@ -25,6 +25,9 @@

{{ 'ENUM.NAV_BAR_NAME.ADMINISTRATION' | translate | uppercase {{ 'ENUM.NAV_BAR_NAME.STATISTICS' | translate | uppercase }} + + {{ 'ENUM.NAV_BAR_NAME.IMPORT-EXPORT_PROVIDERS' | translate | uppercase }} + diff --git a/src/app/shell/admin-tools/admin-tools.component.ts b/src/app/shell/admin-tools/admin-tools.component.ts index c75ff10f9c..691db727a0 100644 --- a/src/app/shell/admin-tools/admin-tools.component.ts +++ b/src/app/shell/admin-tools/admin-tools.component.ts @@ -4,7 +4,7 @@ import { Observable, Subject, takeUntil } from 'rxjs'; import { Role } from 'shared/enum/role'; import { RegistrationState } from 'shared/store/registration.state'; -import { canManageInstitution, canManageRegion } from 'shared/utils/admin.utils'; +import { canManageImports, canManageInstitution, canManageRegion } from 'shared/utils/admin.utils'; @Component({ selector: 'app-admin-tools', @@ -18,6 +18,7 @@ export class AdminToolsComponent implements OnInit, OnDestroy { public readonly Role = Role; public readonly canManageInstitution = canManageInstitution; public readonly canManageRegion = canManageRegion; + public readonly canManageImports = canManageImports; public role: Role; diff --git a/src/app/shell/details/provider-details/provider-details.component.html b/src/app/shell/details/provider-details/provider-details.component.html index 1c54770da4..7dad318431 100644 --- a/src/app/shell/details/provider-details/provider-details.component.html +++ b/src/app/shell/details/provider-details/provider-details.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/app/shell/details/workshop-details/workshop-details.component.html b/src/app/shell/details/workshop-details/workshop-details.component.html index 623e0f9142..fafe23a05f 100644 --- a/src/app/shell/details/workshop-details/workshop-details.component.html +++ b/src/app/shell/details/workshop-details/workshop-details.component.html @@ -1,7 +1,7 @@
- +
{{ 'ENUM.NAV_BAR_NAME.ADMINISTRATION' | translate | uppercase }} + + + {{ 'ENUM.NAV_BAR_NAME.EMPLOYEES' | translate | uppercase }} + + {{ 'ENUM.NAV_BAR_NAME.FAVORITE' | translate | uppercase }} diff --git a/src/app/shell/personal-cabinet/provider/create-position/create-position.component.html b/src/app/shell/personal-cabinet/provider/create-position/create-position.component.html index 1eda43cfaa..0d5cc6e0ea 100644 --- a/src/app/shell/personal-cabinet/provider/create-position/create-position.component.html +++ b/src/app/shell/personal-cabinet/provider/create-position/create-position.component.html @@ -1,5 +1,5 @@ -
+

diff --git a/src/app/shell/personal-cabinet/provider/create-position/position-form/create-position-form.component.html b/src/app/shell/personal-cabinet/provider/create-position/position-form/create-position-form.component.html index a0d2d10fd6..3306488cf1 100644 --- a/src/app/shell/personal-cabinet/provider/create-position/position-form/create-position-form.component.html +++ b/src/app/shell/personal-cabinet/provider/create-position/position-form/create-position-form.component.html @@ -47,7 +47,9 @@
- +