Skip to content

Commit

Permalink
Merge pull request #50 from GSA-TTS/submission-report
Browse files Browse the repository at this point in the history
Submission Report UI
  • Loading branch information
akuny authored Apr 15, 2024
2 parents 4b72a7a + 3204d6e commit 32a54fe
Show file tree
Hide file tree
Showing 11 changed files with 661 additions and 16 deletions.
3 changes: 1 addition & 2 deletions nad_ch/application/view_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,7 @@ class DataSubmissionViewModel(ViewModel):
def create_data_submission_vm(submission: DataSubmission) -> DataSubmissionViewModel:
report_json = []
if submission.report is not None:
enriched_report = enrich_report(submission.report)
report_json = json.dumps(enriched_report)
report_json = enrich_report(submission.report)

return DataSubmissionViewModel(
id=submission.id,
Expand Down
9 changes: 8 additions & 1 deletion nad_ch/controllers/web/routes/data_submissions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from flask import Blueprint, current_app, render_template, g
from flask import Blueprint, current_app, render_template, g, jsonify
from flask_login import login_required, current_user
from nad_ch.application.use_cases.data_submissions import (
get_data_submission,
Expand Down Expand Up @@ -39,3 +39,10 @@ def reports():
def view_report(submission_id):
view_model = get_data_submission(g.ctx, submission_id)
return render_template("data_submissions/show.html", submission=view_model)


@submissions_bp.route("/api/reports/<submission_id>")
@login_required
def view_report_json(submission_id):
view_model = get_data_submission(g.ctx, submission_id)
return jsonify(view_model.report)
60 changes: 60 additions & 0 deletions nad_ch/controllers/web/sass/components/_usa-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,63 @@
width: auto;
height: 30px;
}

@mixin rounded-border {
border-radius: 99px;
}

@mixin focus-style {
// Override for outline-offset when focused and not disabled
&:not([disabled]):focus,
&:not([disabled]).usa-focus {
outline: 0;
}
}

.usa-button-group-toggle {
justify-content: space-between;
}

.usa-button-toggle-on,
.usa-button-toggle-off {
@include rounded-border;
@include focus-style;
color: var(--Primary-primary-dark, #1a4480);
display: flex;
justify-content: center;
align-items: center;
min-height: 50px;
min-width: 200px;
}

.usa-button-toggle-on {
background: var(--Blue-vivid-blue-vivid-5v, #e8f5ff);

&:hover {
background: var(--Blue-vivid-blue-vivid-10v, #cfe8ff);
color: var(--Primary-primary-dark, #1a4480);
}

&:active {
background: var(--Primary-primary-dark, #1a4480);
color: var(--Base-base-lightest, #f0f0f0);
}
}

.usa-button-toggle-off {
background: var(--Base-base-lightest, #f0f0f0);
border: 2px solid var(--Primary-primary-dark, #1a4480);
border-radius: 99px;
color: var(--Primary-primary-dark, #1a4480);

&:hover {
border: 2px solid var(--Primary-primary-dark, #1a4480);
background: var(--Blue-vivid-blue-vivid-10v, #cfe8ff);
color: var(--Primary-primary-dark, #1a4480);
}

&:active {
color: var(--Base-base-lightest, #f0f0f0);
background: var(--Primary-primary-dark, #1a4480);
}
}
38 changes: 38 additions & 0 deletions nad_ch/controllers/web/sass/components/_usa-tag.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.usa-tag__success {
border-radius: 4px;
border: 1px solid var(--Green-cool-vivid-green-cool-vivid-10v, #b7f5bd);
background: var(--Green-cool-vivid-green-cool-vivid-10v, #b7f5bd);
color: black;
display: inline-flex;
padding: 2px 8px;
align-items: center;
}

.usa-tag__info {
border-radius: 4px;
background: var(--Blue-vivid-blue-vivid-10v, #cfe8ff);
color: black;
display: inline-flex;
padding: 2px 8px;
align-items: center;
}

.usa-tag__warning {
border-radius: 4px;
border: 1px solid var(--Warning-warning, #ffbe2e);
background: var(--Warning-warning, #ffbe2e);
color: black;
display: inline-flex;
padding: 2px 8px;
align-items: center;
}

.usa-tag__error {
border-radius: 4px;
border: 1px solid var(--Error-error-dark, #b50909);
background: var(--Error-error-dark, #b50909);
color: white;
display: inline-flex;
padding: 2px 8px;
align-items: center;
}
1 change: 1 addition & 0 deletions nad_ch/controllers/web/sass/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
@import 'components/usa-select';
@import 'components/usa-sidenav';
@import 'components/usa-table';
@import 'components/usa-tag';
120 changes: 120 additions & 0 deletions nad_ch/controllers/web/src/components/CompletenessReport.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* @jest-environment jsdom
*/

import {
CompletenessReport,
CompletenessReportComponent,
} from './CompletenessReport';
import { AlpineComponent } from 'alpinejs';
import { fetchReportData } from '../services';

jest.mock('../services', () => ({
fetchReportData: jest.fn(),
}));

describe('CompletenessReportComponent', () => {
let component: AlpineComponent<CompletenessReportComponent>;
const mockReportData = {
features: [
{
provided_feature_name: 'AddNum_Pre',
nad_feature_name: 'AddNum_Pre',
populated_count: 1141,
null_count: 0,
required: false,
status: 'No error',
populated_percentage: '100%',
null_percentage: '0%',
},
{
provided_feature_name: 'Add_Number',
nad_feature_name: 'Add_Number',
populated_count: 1141,
null_count: 0,
required: true,
status: 'No error',
populated_percentage: '100%',
null_percentage: '0%',
},
],
overview: {
data_update_required: false,
etl_update_required: false,
feature_count: 10,
features_flagged: 2,
},
};

beforeEach(() => {
(fetchReportData as jest.Mock).mockResolvedValue(mockReportData);
component = CompletenessReport('1');
});

it('initializes with default data', () => {
expect(component.id).toBe(1);
expect(component.isLoading).toBe(true);
expect(component.groupedFeatures).toEqual([]);
expect(component.isGroupedByStatus).toBe(false);
});

it('loads report data on init', async () => {
await component.init();
expect(fetchReportData).toHaveBeenCalledWith(1);
expect(component.isLoading).toBe(false);
expect(component.report).toEqual(mockReportData);
});

it('groups features by status', () => {
component.report = mockReportData;
const groupedFeatures = component.groupFeatures(mockReportData.features);
expect(groupedFeatures).toEqual([
{
status: 'No error',
features: [
{
provided_feature_name: 'AddNum_Pre',
nad_feature_name: 'AddNum_Pre',
populated_count: 1141,
null_count: 0,
required: false,
status: 'No error',
populated_percentage: '100%',
null_percentage: '0%',
},
{
provided_feature_name: 'Add_Number',
nad_feature_name: 'Add_Number',
populated_count: 1141,
null_count: 0,
required: true,
status: 'No error',
populated_percentage: '100%',
null_percentage: '0%',
},
],
},
]);
});

it('computes buttonText based on isGroupedByStatus', () => {
expect(component.buttonText).toBe('Group by Status');
component.isGroupedByStatus = true;
expect(component.buttonText).toBe('Ungroup by Status');
});

it('computes toggleButtonClass based on isGroupedByStatus', () => {
expect(component.toggleButtonClass).toBe('usa-button-toggle-off');
component.isGroupedByStatus = true;
expect(component.toggleButtonClass).toBe('usa-button-toggle-on');
});

it('gets status tag class based on different statuses', () => {
expect(component.getStatusTagClass('No error')).toBe('usa-tag__success');
expect(component.getStatusTagClass('Updated by calculation')).toBe(
'usa-tag__warning',
);
expect(component.getStatusTagClass('Rejected')).toBe('usa-tag__error');
expect(component.getStatusTagClass('Other')).toBe('usa-tag__info');
});
});
112 changes: 112 additions & 0 deletions nad_ch/controllers/web/src/components/CompletenessReport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { AlpineComponent } from 'alpinejs';
import { fetchReportData } from '../services';

export type CompletenessReport = {
features: Feature[];
overview: Overview;
};

type Overview = {
data_update_required: boolean;
etl_update_required: boolean;
feature_count: number;
features_flagged: number;
};

type Feature = {
provided_feature_name: string;
nad_feature_name: string;
populated_count: number;
null_count: number;
required: boolean;
status: string;
populated_percentage: string;
null_percentage: string;
};

type GroupedFeature = {
status: string;
features: Feature[];
};

export interface CompletenessReportComponent {
id: number;
isLoading: boolean;
report: CompletenessReport | null;
groupedFeatures: GroupedFeature[];
isGroupedByStatus: boolean;
buttonText: string;
toggleButtonClass: string;
init(): Promise<void>;
groupFeatures(features: Feature[]): GroupedFeature[];
getStatusTagClass(status: string): string;
}

export function CompletenessReport(
id: string,
): AlpineComponent<CompletenessReportComponent> {
return {
id: parseInt(id),
isLoading: true,
report: {
features: [],
overview: {
data_update_required: false,
etl_update_required: false,
feature_count: 0,
features_flagged: 0,
},
},
groupedFeatures: [],
isGroupedByStatus: false,
async init(): Promise<void> {
try {
this.report = await fetchReportData(this.id);
if (this.report) {
this.groupedFeatures = this.groupFeatures(this.report.features);
}
} catch (error) {
console.error('Failed to load report:', error);
} finally {
this.isLoading = false;
}
},
groupFeatures(features: Feature[]): GroupedFeature[] {
const groupedByStatus = features.reduce<Record<string, Feature[]>>(
(acc, obj) => {
if (!acc[obj.status]) {
acc[obj.status] = [];
}
acc[obj.status].push(obj);
return acc;
},
{},
);

return Object.keys(groupedByStatus).map((status) => ({
status: status,
features: groupedByStatus[status],
}));
},
get buttonText(): string {
return this.isGroupedByStatus ? 'Ungroup by Status' : 'Group by Status';
},
get toggleButtonClass(): string {
return this.isGroupedByStatus
? 'usa-button-toggle-on'
: 'usa-button-toggle-off';
},
getStatusTagClass(status: string): string {
switch (status) {
case 'No error':
return 'usa-tag__success';
case 'Updated by calculation':
return 'usa-tag__warning';
case 'Rejected':
return 'usa-tag__error';
default:
return 'usa-tag__info';
}
},
};
}
2 changes: 2 additions & 0 deletions nad_ch/controllers/web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import '@uswds/uswds/css/uswds.css';
import '@uswds/uswds';
import Alpine from 'alpinejs';
import { MappingForm } from './components/MappingForm';
import { CompletenessReport } from './components/CompletenessReport';

declare global {
interface Window {
Expand All @@ -12,6 +13,7 @@ declare global {

document.addEventListener('alpine:init', () => {
Alpine.data('MappingForm', MappingForm);
Alpine.data('CompletenessReport', CompletenessReport);
});

window.Alpine = Alpine;
Expand Down
8 changes: 8 additions & 0 deletions nad_ch/controllers/web/src/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { BASE_URL } from './config';
import { CompletenessReport } from './components/CompletenessReport';

export async function fetchReportData(id: number): Promise<CompletenessReport> {
const response = await fetch(`${BASE_URL}/api/reports/${id}`);
const reportData = await response.json();
return reportData;
}
Loading

0 comments on commit 32a54fe

Please sign in to comment.