{{'loading-units' | translate}}
+| + |
+ |
+ {{'coding.unit-name' | translate}} | +{{formatUnitName(unit.unitName)}} | +
|---|---|
| + @if (filterValue) { + {{'no-units-matching' | translate}} "{{filterValue}}" + } @else { + {{'no-units-available' | translate}} + } + | +
Kodierschema mit Schemer Version ab 1.5 erzeugen!
' + }; + if (contentSetting.showScore) codeInfo.score = ''; + return codeInfo; + } + + private static getCodeInfoFromCodeAsText(code: CodeData, contentSetting: CodeBookContentSetting): CodeInfo { + const codeAsText = ToTextFactory.codeAsText(code, 'SIMPLE'); + const rulesDescription = contentSetting.hasOnlyManualCoding && !contentSetting.hasClosedVars ? '' : + this.getRulesDescription(codeAsText, code); + const codeInfo: CodeInfo = { + id: `${code.id}`, + label: contentSetting.codeLabelToUpper ? codeAsText.label.toUpperCase() : codeAsText.label, + description: `${rulesDescription}${code.manualInstruction ?? ''}` + }; + if (contentSetting.showScore) codeInfo.score = codeAsText.score.toString(); + return codeInfo; + } + + private static getRulesDescription(codeAsText: CodeAsText, code: CodeData): string { + let rulesDescription = ''; + codeAsText.ruleSetDescriptions.forEach( + (ruleSetDescription: string) => { + if (ruleSetDescription !== 'Keine Regeln definiert.') { + rulesDescription += `${ruleSetDescription}
`; + } else if ((code.manualInstruction ?? '') === '') rulesDescription += `${ruleSetDescription}
`; + } + ); + return rulesDescription; + } +} diff --git a/projects/ngx-coding-components/codebook-generator/src/public-api.ts b/projects/ngx-coding-components/codebook-generator/src/public-api.ts new file mode 100644 index 0000000..511d0a8 --- /dev/null +++ b/projects/ngx-coding-components/codebook-generator/src/public-api.ts @@ -0,0 +1,10 @@ +export * from './lib/codebook-generator/codebook-generator.class'; +export type { + BookVariable, + CodeBookContentSetting, + CodebookUnitDto, + CodeInfo, + ItemMetadata, + Missing, + UnitPropertiesForCodebook +} from '@iqb/ngx-coding-components/codebook-models'; diff --git a/projects/ngx-coding-components/codebook-models/ng-package.json b/projects/ngx-coding-components/codebook-models/ng-package.json new file mode 100644 index 0000000..d0a2dcd --- /dev/null +++ b/projects/ngx-coding-components/codebook-models/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/projects/ngx-coding-components/codebook-models/src/lib/codebook.interfaces.ts b/projects/ngx-coding-components/codebook-models/src/lib/codebook.interfaces.ts new file mode 100644 index 0000000..8eb8d8d --- /dev/null +++ b/projects/ngx-coding-components/codebook-models/src/lib/codebook.interfaces.ts @@ -0,0 +1,151 @@ +import { VariableInfo } from '@iqbspecs/variable-info/variable-info.interface'; + +/** + * Item metadata for codebook + */ +export interface ItemMetadata { + [key: string]: unknown; +} + +/** + * Settings for codebook content generation + */ +export interface CodeBookContentSetting { + /** Export format (docx or json) */ + exportFormat: string; + /** Missings profile name */ + missingsProfile: string; + /** Include only manual coding */ + hasOnlyManualCoding: boolean; + /** Include general instructions */ + hasGeneralInstructions: boolean; + /** Include derived variables */ + hasDerivedVars: boolean; + /** Include only variables with codes */ + hasOnlyVarsWithCodes: boolean; + /** Include closed variables */ + hasClosedVars: boolean; + /** Convert code labels to uppercase */ + codeLabelToUpper: boolean; + /** Show score */ + showScore: boolean; + /** Hide item-variable relation */ + hideItemVarRelation: boolean; +} + +/** + * Missing code definition + */ +export interface Missing { + /** Missing code */ + code: string | number; + /** Missing label */ + label: string; + /** Missing description */ + description: string; +} + +/** + * Code information for codebook + */ +export interface CodeInfo { + /** Code ID */ + id: string; + /** Code label */ + label: string; + /** Code description */ + description: string; + /** Code score (optional) */ + score?: string; +} + +/** + * Variable information for codebook + */ +export interface BookVariable { + /** Variable ID */ + id: string; + /** Variable label */ + label: string; + /** Variable source type */ + sourceType: string; + /** General instruction */ + generalInstruction: string; + /** Codes */ + codes: CodeInfo[]; +} + +/** + * Unit data for codebook + */ +export interface CodebookUnitDto { + /** Unit key */ + key: string; + /** Unit name */ + name: string; + /** Variables */ + variables: BookVariable[]; + /** Missings */ + missings: Missing[]; + /** Items (optional) */ + items?: ItemMetadata[]; +} + +/** + * Unit properties for codebook generation + */ +export interface UnitPropertiesForCodebook { + /** Unit ID */ + id: number; + /** Unit key */ + key: string; + /** Unit name */ + name: string; + /** Coding scheme */ + scheme?: string; + /** Scheme type */ + schemeType?: string; + /** Metadata */ + metadata?: { + /** Items */ + items?: ItemMetadata[]; + }; + /** Variables */ + variables?: VariableInfo[]; +} + +/** + * Unit selection item for codebook export UI + */ +export interface UnitSelectionItem { + /** Unit ID */ + unitId: number; + /** Unit name */ + unitName: string; + /** Unit alias */ + unitAlias: string | null; +} + +/** + * Missings profile for selection + */ +export interface MissingsProfile { + /** Profile ID */ + id: number; + /** Profile label */ + label: string; + /** Missings data */ + missings?: Missing[] | string; +} + +/** + * Codebook export configuration + */ +export interface CodebookExportConfig { + /** Selected unit IDs */ + selectedUnits: number[]; + /** Content options */ + contentOptions: CodeBookContentSetting; + /** Selected missings profile ID */ + missingsProfileId: number; +} diff --git a/projects/ngx-coding-components/codebook-models/src/public-api.ts b/projects/ngx-coding-components/codebook-models/src/public-api.ts new file mode 100644 index 0000000..ed91da9 --- /dev/null +++ b/projects/ngx-coding-components/codebook-models/src/public-api.ts @@ -0,0 +1 @@ +export * from './lib/codebook.interfaces'; diff --git a/projects/ngx-coding-components/package.json b/projects/ngx-coding-components/package.json index d9b47a1..003a226 100644 --- a/projects/ngx-coding-components/package.json +++ b/projects/ngx-coding-components/package.json @@ -62,8 +62,22 @@ "ngx-build-plus": "^20.0.0", "prosemirror-state": "^1.3.4", "rxjs": "~7.8.0", + "docx": "^8.5.0", + "cheerio": "^1.0.0", + "buffer": "^6.0.3", "zone.js": "~0.15.0" }, + "peerDependenciesMeta": { + "docx": { + "optional": true + }, + "cheerio": { + "optional": true + }, + "buffer": { + "optional": true + } + }, "devDependencies": { "@angular-devkit/build-angular": "^20.0.4", "@angular/compiler-cli": "^20.0.5", diff --git a/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts new file mode 100644 index 0000000..be6bcd4 --- /dev/null +++ b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts @@ -0,0 +1,222 @@ +import { + ComponentFixture, TestBed, fakeAsync, tick +} from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; + +import { + CodebookExportComponent +} from '../../../codebook-export/src/lib/codebook-export/codebook-export.component'; +import { + CodebookExportExecution, + CodebookExportJobStatus, + CodebookExportProvider +} from '../../../codebook-export/src/lib/codebook-export/codebook-export.provider'; +import { + CodebookExportConfig, + UnitSelectionItem, + MissingsProfile +} from '../../../codebook-models/src/lib/codebook.interfaces'; + +describe('CodebookExportComponent', () => { + let fixture: ComponentFixtureManual instruction
' + }, + { + id: 0, + type: 'RESIDUAL_AUTO', + label: 'Automatisch', + score: 0, + ruleSetOperatorAnd: true, + ruleSets: [], + manualInstruction: '' + } + ] + } + ] + }); + + const blob = await CodebookGenerator.generateCodebook( + [ + { + id: 1, + key: 'UNIT1', + name: 'Unit 1', + scheme + } + ], + { + ...contentSetting, + hasClosedVars: false + }, + [] + ); + const codebook = JSON.parse(await blob.text()); + + expect(codebook[0].variables.length).toBe(1); + expect(codebook[0].variables[0].codes.map((code: { id: string }) => code.id)).toEqual(['1', '0']); + }); + + describe('Studio-compatible variable filtering', () => { + const makeCode = (id: number, type: string, manualInstruction: string) => ({ + id, + label: `Code ${id}`, + score: 1, + type, + manualInstruction, + ruleSetOperatorAnd: true, + ruleSets: [] + }); + + const scheme = JSON.stringify({ + variableCodings: [ + { + id: 'v_manual', + sourceType: 'BASE', + label: 'Manual var', + manualInstruction: '', + codes: [makeCode(1, 'INTENDED', 'do it')] + }, + { + id: 'v_manual_but_only_closed', + sourceType: 'BASE', + label: 'Manual but residual auto', + manualInstruction: '', + codes: [makeCode(11, 'RESIDUAL_AUTO', 'do it')] + }, + { + id: 'v_mixed', + sourceType: 'BASE', + label: 'Mixed var', + manualInstruction: '', + codes: [ + makeCode(21, 'INTENDED', 'do it'), + makeCode(22, 'RESIDUAL_AUTO', '') + ] + }, + { + id: 'v_closed', + sourceType: 'BASE', + label: 'Closed var', + manualInstruction: '', + codes: [makeCode(2, 'RESIDUAL_AUTO', '')] + }, + { + id: 'v_uncoded', + sourceType: 'BASE', + label: 'Uncoded var', + manualInstruction: '', + codes: [makeCode(3, 'INTENDED', '')] + } + ] + }); + + const baseSettings: CodeBookContentSetting = { + exportFormat: 'json', + missingsProfile: '', + hasClosedVars: false, + hasOnlyManualCoding: false, + hasDerivedVars: false, + hasGeneralInstructions: false, + codeLabelToUpper: false, + showScore: false, + hideItemVarRelation: false, + hasOnlyVarsWithCodes: false + }; + + const getVarIds = async (settings: CodeBookContentSetting): PromiseDescription text
' + } + ] + } + ] + }; + + const blob = await CodebookDocxGenerator.generateDocx( + [unit], + { + ...contentSetting, + exportFormat: 'docx', + hideItemVarRelation: false + } + ); + const zip = await JSZip.loadAsync(await blob.arrayBuffer()); + const documentXml = await zip.file('word/document.xml')?.async('string'); + + expect(documentXml).toContain('UNIT1 Unit 1'); + expect(documentXml).toContain('VAR1 Variable 1'); + expect(documentXml).toContain('Item(s): ITEM1 ITEM2'); + expect(documentXml).not.toContain('ITEM3'); + + const scoreHeaderIndex = documentXml?.indexOf('Score') ?? -1; + const descriptionHeaderIndex = documentXml?.indexOf('Beschreibung') ?? -1; + const scoreValueIndex = documentXml?.indexOf('77') ?? -1; + const descriptionValueIndex = documentXml?.indexOf('Description text') ?? -1; + + expect(scoreHeaderIndex).toBeGreaterThan(-1); + expect(descriptionHeaderIndex).toBeGreaterThan(-1); + expect(scoreHeaderIndex).toBeLessThan(descriptionHeaderIndex); + expect(scoreValueIndex).toBeGreaterThan(-1); + expect(descriptionValueIndex).toBeGreaterThan(-1); + expect(scoreValueIndex).toBeLessThan(descriptionValueIndex); + }); +}); diff --git a/projects/ngx-coding-components/src/lib/translations/de.json b/projects/ngx-coding-components/src/lib/translations/de.json index 55736fb..fb19f43 100644 --- a/projects/ngx-coding-components/src/lib/translations/de.json +++ b/projects/ngx-coding-components/src/lib/translations/de.json @@ -1,6 +1,12 @@ { "close": "Schließen", + "export": "Exportieren", "filter-by": "Filtern nach...", + "search": "Suchen", + "search-units": "Einheiten suchen", + "loading-units": "Einheiten werden geladen...", + "no-units-matching": "Keine passenden Einheiten für", + "no-units-available": "Keine Einheiten verfügbar", "copied-to-clipboard": "In Zwischenablage kopiert", "varList": { "base": "Basisvariablen", @@ -34,6 +40,13 @@ } }, + "workspace": { + "export-coding-book": "Kodierhandbuch exportieren", + "coding-missing-profiles": "Missing-Profile", + "select-missings-profile": "Missing-Profil auswählen", + "no-missings-profile": "Keines" + }, + "schemer": { "info": { "title": "Schemer – Informationen", @@ -140,6 +153,32 @@ "rule": "Regel", "vars-with-codes-only": "Nur Variablen mit Codes", "raw-responses": "Rohantworten anzeigen", + "select-units": "Einheiten auswählen", + "select-all-units": "Alle Einheiten auswählen", + "unit-name": "Einheit", + "codebook-content": "Inhalt des Kodierhandbuchs", + "has-only-vars-with-codes": "Nur Variablen mit Codes", + "has-general-instructions": "Allgemeine Instruktionen", + "hide-item-var-relation": "Item-Variablen-Beziehung ausblenden", + "has-derived-vars": "Abgeleitete Variablen", + "has-only-manual-coding": "Nur manuelle Kodierung", + "has-closed-vars": "Geschlossene Variablen", + "show-score": "Bewertung anzeigen", + "code-label-to-upper": "Code-Labels in Großbuchstaben", + "export-format": "Exportformat", + "error-save-changes": "Bitte speichern Sie Ihre Änderungen vor dem Export.", + "codebook-generating": "Kodierhandbuch wird erzeugt", + "codebook-completed": "Kodierhandbuch wurde erzeugt", + "tooltip": { + "has-only-vars-with-codes": "Variablen ohne Codes werden nicht exportiert.", + "has-general-instructions": "Allgemeine Kodierinstruktionen werden exportiert.", + "hide-item-var-relation": "Die Zuordnung zwischen Items und Variablen wird ausgeblendet.", + "has-derived-vars": "Abgeleitete Variablen werden exportiert.", + "has-only-manual-coding": "Nur Codes mit manueller Kodierinstruktion werden exportiert.", + "has-closed-vars": "Geschlossene automatisch kodierte Variablen werden exportiert.", + "show-score": "Bewertungen der Codes werden exportiert.", + "code-label-to-upper": "Code-Labels werden in Großbuchstaben ausgegeben." + }, "instruction": "Instruktion", "variable": "Variable", "result": "Ergebnis", diff --git a/projects/ngx-coding-components/tsconfig.spec.json b/projects/ngx-coding-components/tsconfig.spec.json index f78896e..0eee8e0 100644 --- a/projects/ngx-coding-components/tsconfig.spec.json +++ b/projects/ngx-coding-components/tsconfig.spec.json @@ -9,7 +9,10 @@ "paths": { "@angular/*": ["../../node_modules/@angular/*"], "@ngx-translate/core": ["../../node_modules/@ngx-translate/core"], - "@ngx-translate/core/*": ["../../node_modules/@ngx-translate/core/*"] + "@ngx-translate/core/*": ["../../node_modules/@ngx-translate/core/*"], + "@iqb/ngx-coding-components/codebook-models": [ + "projects/ngx-coding-components/codebook-models/src/public-api.ts" + ] } }, "include": [ diff --git a/tsconfig.json b/tsconfig.json index b2b5c0b..e7d38a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,9 @@ ], "@ngx-coding-components/*": [ "projects/ngx-coding-components/src/lib/*" + ], + "@iqb/ngx-coding-components/codebook-models": [ + "projects/ngx-coding-components/codebook-models/src/public-api.ts" ] }, "sourceMap": true,