diff --git a/webroot/src/components/Page/PageHeader/PageHeader.less b/webroot/src/components/Page/PageHeader/PageHeader.less
index a25953712..f83771c9c 100644
--- a/webroot/src/components/Page/PageHeader/PageHeader.less
+++ b/webroot/src/components/Page/PageHeader/PageHeader.less
@@ -18,6 +18,10 @@
color: @white;
background-color: @primaryColor;
+ @media print {
+ display: none;
+ }
+
@media @tabletWidth {
background-color: transparent;
}
diff --git a/webroot/src/locales/en.json b/webroot/src/locales/en.json
index 6b0f99afe..4f1bc89c7 100644
--- a/webroot/src/locales/en.json
+++ b/webroot/src/locales/en.json
@@ -9,6 +9,7 @@
"cancel": "Cancel",
"close": "Close",
"skip": "Skip",
+ "print": "Print",
"confirm": "Confirm",
"caution": "Caution",
"fees": "Fees",
@@ -514,6 +515,8 @@
"licenseNumber": "License number",
"licenseExpirationDate": "License expiration date",
"residenceLocation": "Residence location",
+ "generateVerification": "Generate verification doc",
+ "generateVerificationSubtext": "Printer-friendly view",
"obtainPrivileges": "Obtain Privileges",
"privileges": "Privileges",
"privilege": "Privilege",
@@ -639,7 +642,12 @@
"eventNodeLabel": "Event: {eventNameDisplay} on {eventDate}",
"statusBlockLabel": "{status} status block",
"compactEligible": "Compact Eligible",
- "notCompactEligible": "Not Compact Eligible"
+ "notCompactEligible": "Not Compact Eligible",
+ "privilegeVerification": "Privilege Verification",
+ "practitionerInformation": "Practitioner Information",
+ "homeStateLicenses": "Home State Licenses",
+ "activeDate": "Active date",
+ "privilegeProofFooter": "This document is officially generated by Compact Connect and serves as proof of the practitioner's current licenses and privileges."
},
"military": {
"militaryStatusTitle": "Military status",
diff --git a/webroot/src/locales/es.json b/webroot/src/locales/es.json
index 393a10125..9b9426fc8 100644
--- a/webroot/src/locales/es.json
+++ b/webroot/src/locales/es.json
@@ -11,6 +11,7 @@
"cancel": "Cancelar",
"close": "Cerrar",
"skip": "Saltear",
+ "print": "Imprimir",
"fees": "Tarifas",
"choose": "Elige",
"chooseOne": "Elige uno",
@@ -545,6 +546,8 @@
"licenseNumber": "Número de licencia",
"licenseExpirationDate": "Fecha de vencimiento de la licencia",
"licenseExpiredMessage": "Su licencia ha caducado.",
+ "generateVerification": "Generar documento de verificación",
+ "generateVerificationSubtext": "Vista para imprimir",
"obtainPrivileges": "Obtener privilegios",
"expired": "Caducada",
"issued": "Emitida",
@@ -638,7 +641,12 @@
"eventNodeLabel": "Evento: {eventNameDisplay} en {eventDate}",
"statusBlockLabel": "{status} bloque de estado",
"compactEligible": "Elegible Compacto",
- "notCompactEligible": "No Compacto Elegible"
+ "notCompactEligible": "No Compacto Elegible",
+ "privilegeVerification": "Verificación de privilegios",
+ "practitionerInformation": "Información para profesionales",
+ "homeStateLicenses": "Licencias del estado de origen",
+ "activeDate": "Fecha activa",
+ "privilegeProofFooter": "Este documento es generado oficialmente por Compact Connect y sirve como prueba de las licencias y privilegios actuales del profesional."
},
"military": {
"militaryStatusTitle": "Estado militar",
diff --git a/webroot/src/models/License/License.model.spec.ts b/webroot/src/models/License/License.model.spec.ts
index 96a4b8376..4c6b42b46 100644
--- a/webroot/src/models/License/License.model.spec.ts
+++ b/webroot/src/models/License/License.model.spec.ts
@@ -145,6 +145,19 @@ describe('License model', () => {
},
]);
});
+ it('should create a License with specific values (custom displayName delimiter)', () => {
+ const data = {
+ issueState: new State({ abbrev: 'co' }),
+ licenseType: LicenseType.AUDIOLOGIST,
+ };
+ const license = new License(data);
+
+ // Test field values
+ expect(license).to.be.an.instanceof(License);
+
+ // Test methods
+ expect(license.displayName(' ... ')).to.equal('Colorado ... AUD');
+ });
it('should create a License with specific values through serializer', () => {
const data = {
compact: CompactType.ASLP,
diff --git a/webroot/src/models/License/License.model.ts b/webroot/src/models/License/License.model.ts
index 8ee3e23af..fab2ca768 100644
--- a/webroot/src/models/License/License.model.ts
+++ b/webroot/src/models/License/License.model.ts
@@ -141,8 +141,8 @@ export class License implements InterfaceLicense {
return upperCaseAbbrev;
}
- public displayName(): string {
- return `${this.issueState?.name() || ''}${this.issueState?.name() && this.licenseTypeAbbreviation() ? ' - ' : ''}${this.licenseTypeAbbreviation()}`;
+ public displayName(delimiter = ' - '): string {
+ return `${this.issueState?.name() || ''}${this.issueState?.name() && this.licenseTypeAbbreviation() ? delimiter : ''}${this.licenseTypeAbbreviation()}`;
}
public historyWithFabricatedEvents(): Array
{
diff --git a/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.less b/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.less
index a38efa23e..5fcae9c3e 100644
--- a/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.less
+++ b/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.less
@@ -24,13 +24,16 @@
flex-direction: column;
align-items: center;
justify-content: center;
+ width: 100%;
+ padding: 0 4rem;
- @media @tabletWidth {
- display: flex;
- flex-direction: row;
- align-items: center;
+ @media @desktopWidth {
+ align-items: flex-start;
justify-content: space-between;
- width: 100%;
+ }
+
+ @media @largeDesktopWidth {
+ flex-direction: row;
}
.welcome-user {
@@ -41,39 +44,57 @@
.button-block {
display: flex;
- flex-direction: row;
+ flex-direction: column;
+ align-items: flex-start;
+ width: 100%;
margin-top: 2rem;
- .view-military-btn {
- margin-right: 1rem;
+ @media @desktopWidth {
+ flex-direction: row;
+ }
- &:deep(input) {
- padding-right: 1rem;
- padding-left: 1rem;
- }
+ @media @largeDesktopWidth {
+ width: auto;
+ margin-top: 0;
+ margin-left: auto;
+ }
- @media @tabletWidth {
- margin-right: 2rem;
+ .btn-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ margin-bottom: 1.2rem;
- &:deep(input) {
- padding-right: 2.4rem;
- padding-left: 2.4rem;
+ @media @desktopWidth {
+ width: 100%;
+
+ &:not(:first-child) {
+ margin-left: 1.8rem;
}
}
- }
- .obtain-priv-btn {
- &:deep(input) {
- padding-right: 1rem;
- padding-left: 1rem;
+ @media @largeDesktopWidth {
+ width: auto;
}
- @media @tabletWidth {
+ .btn {
+ width: 100%;
+ margin-bottom: 0.4rem;
+
+ @media @largeDesktopWidth {
+ width: auto;
+ }
+
&:deep(input) {
- padding-right: 2.4rem;
- padding-left: 2.4rem;
+ width: 100%;
}
}
+
+ .btn-subtext {
+ color: darken(@darkGrey, 10%);
+ font-size: 1.2rem;
+ text-align: right;
+ }
}
}
}
diff --git a/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.ts b/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.ts
index cb1ba901a..4ab193f4a 100644
--- a/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.ts
+++ b/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.ts
@@ -41,22 +41,6 @@ export default class LicenseeDashboard extends Vue {
return this.$store.state.user;
}
- get user(): LicenseeUser | null {
- return this.userStore.model;
- }
-
- get licensee(): Licensee {
- return this.user?.licensee || new Licensee();
- }
-
- get licenseePrivileges(): Array {
- return this.licensee.privileges || [];
- }
-
- get licenseeLicenses(): Array {
- return this.licensee.licenses || [];
- }
-
get currentCompact(): Compact | null {
return this.userStore?.currentCompact || null;
}
@@ -65,6 +49,10 @@ export default class LicenseeDashboard extends Vue {
return this.currentCompact?.type || null;
}
+ get user(): LicenseeUser | null {
+ return this.userStore.model;
+ }
+
get userFullName(): string {
let name = '';
@@ -75,32 +63,29 @@ export default class LicenseeDashboard extends Vue {
return name;
}
- get homeJurisdiction(): State | null {
- return this.licensee?.homeJurisdiction || null;
- }
-
- get obtainPrivButtonLabel(): string {
- return `+ ${this.$t('licensing.obtainPrivileges')}`;
+ get licensee(): Licensee {
+ return this.user?.licensee || new Licensee();
}
- get privilegeTitle(): string {
- return this.$t('licensing.privileges');
+ get licenseeLicenses(): Array {
+ return this.licensee.licenses || [];
}
- get welcomeText(): string {
- return this.$t('common.welcome');
+ get licenseePrivileges(): Array {
+ return this.licensee.privileges || [];
}
- get isPrivilegePurchaseEnabled(): boolean {
- return this.licensee?.canPurchasePrivileges() || false;
+ get homeJurisdiction(): State | null {
+ return this.licensee?.homeJurisdiction || null;
}
- get twoHomeStateErrorText(): string {
- return this.$t('licensing.twoHomeStateErrorMessage');
+ get isGenerateProofEnabled(): boolean {
+ return this.licenseePrivileges.filter((privilege: License) =>
+ privilege.status === LicenseStatus.ACTIVE).length > 0;
}
- get licenseExpiredMessage(): string {
- return this.$t('licensing.licenseExpiredMessage');
+ get isPrivilegePurchaseEnabled(): boolean {
+ return this.licensee?.canPurchasePrivileges() || false;
}
//
@@ -120,6 +105,13 @@ export default class LicenseeDashboard extends Vue {
});
}
+ viewLicenseeProof() {
+ this.$router.push({
+ name: 'LicenseeVerification',
+ params: { compact: this.currentCompactType }
+ });
+ }
+
isLicenseActive(license: License): boolean {
return license && license.status === LicenseStatus.ACTIVE;
}
diff --git a/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.vue b/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.vue
index c2fae059b..1a6604bd5 100644
--- a/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.vue
+++ b/webroot/src/pages/LicenseeDashboard/LicenseeDashboard.vue
@@ -8,22 +8,37 @@
-
{{welcomeText}}, {{ userFullName }}
+
{{ $t('common.welcome') }}, {{ userFullName }}
@@ -54,9 +69,7 @@
:alt="$t('licensing.privilegeIcon')"
/>
-
- {{privilegeTitle}}
-
+
{{ $t('licensing.privileges') }}
{
+ it('should mount the page component', async () => {
+ const wrapper = await mountShallow(LicenseeProof);
+
+ expect(wrapper.exists()).to.equal(true);
+ expect(wrapper.findComponent(LicenseeProof).exists()).to.equal(true);
+ });
+});
diff --git a/webroot/src/pages/LicenseeProof/LicenseeProof.ts b/webroot/src/pages/LicenseeProof/LicenseeProof.ts
new file mode 100644
index 000000000..8580dcf5d
--- /dev/null
+++ b/webroot/src/pages/LicenseeProof/LicenseeProof.ts
@@ -0,0 +1,82 @@
+//
+// LicenseeProof.ts
+// CompactConnect
+//
+// Created by InspiringApps on 5/2/2025.
+//
+
+import { Component, Vue } from 'vue-facing-decorator';
+import LicenseHomeIcon from '@components/Icons/LicenseHome/LicenseHome.vue';
+import PrivilegesIcon from '@components/Icons/LicenseeUser/LicenseeUser.vue';
+import UserIcon from '@components/Icons/User/User.vue';
+import { Compact } from '@models/Compact/Compact.model';
+import { License, LicenseStatus } from '@models/License/License.model';
+import { Licensee } from '@models/Licensee/Licensee.model';
+import { State } from '@models/State/State.model';
+import { LicenseeUser } from '@/models/LicenseeUser/LicenseeUser.model';
+import moment from 'moment';
+
+@Component({
+ name: 'LicenseeProof',
+ components: {
+ UserIcon,
+ LicenseHomeIcon,
+ PrivilegesIcon,
+ }
+})
+export default class LicenseeProof extends Vue {
+ //
+ // Computed
+ //
+ get userStore(): any {
+ return this.$store.state.user;
+ }
+
+ get user(): LicenseeUser | null {
+ return this.userStore.model;
+ }
+
+ get currentCompact(): Compact | null {
+ return this.userStore?.currentCompact || null;
+ }
+
+ get currentCompactType(): string | null {
+ return this.currentCompact?.type || null;
+ }
+
+ get currentDateDisplay(): string {
+ return moment().format('MMM D, YYYY');
+ }
+
+ get licensee(): Licensee {
+ return this.user?.licensee || new Licensee();
+ }
+
+ get homeJurisdiction(): State | null {
+ return this.licensee?.homeJurisdiction || null;
+ }
+
+ get homeJurisdictionName(): string {
+ return this.homeJurisdiction?.name() || '';
+ }
+
+ get userFullName(): string {
+ return this.user?.getFullName() || '';
+ }
+
+ get licenseeLicenses(): Array {
+ return this.licensee.activeHomeJurisdictionLicenses() || [];
+ }
+
+ get licenseePrivileges(): Array {
+ return this.licensee.privileges?.filter((privilege: License) =>
+ privilege.status === LicenseStatus.ACTIVE) || [];
+ }
+
+ //
+ // Methods
+ //
+ printHandler(): void {
+ window.print();
+ }
+}
diff --git a/webroot/src/pages/LicenseeProof/LicenseeProof.vue b/webroot/src/pages/LicenseeProof/LicenseeProof.vue
new file mode 100644
index 000000000..1af830b21
--- /dev/null
+++ b/webroot/src/pages/LicenseeProof/LicenseeProof.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
{{ $t('licensing.privilegeVerification') }}
+
+
+
+ {{ $t('licensing.practitionerInformation') }}
+
+
+
+ {{ $t('common.name') }}
+ {{ userFullName }}
+
+
+ {{ $t('licensing.homeState') }}
+ {{ homeJurisdictionName }}
+
+
+
+
+
+
+ {{ $t('licensing.homeStateLicenses') }}
+
+
+
+ {{ license.displayName(', ') }}
+
+
+ {{ $t('licensing.activeDate') }}
+ {{ license.issueDateDisplay() }}
+
+
+ {{ $t('licensing.expiration') }}
+ {{ license.expireDateDisplay() }}
+
+
+
+
+
+
+ {{ $t('licensing.privileges') }}
+
+
+
+ {{ privilege.displayName(', ') }}
+
+
+ {{ $t('licensing.activeDate') }}
+ {{ privilege.issueDateDisplay() }}
+
+
+ {{ $t('licensing.expiration') }}
+ {{ privilege.expireDateDisplay() }}
+
+
+
+
+
+
+
+
+
diff --git a/webroot/src/router/routes.ts b/webroot/src/router/routes.ts
index f01104a11..8710980f1 100644
--- a/webroot/src/router/routes.ts
+++ b/webroot/src/router/routes.ts
@@ -122,6 +122,12 @@ const routes: Array = [
component: () => import(/* webpackChunkName: "militaryStatusUpdate" */ '@pages/MilitaryStatusUpdate/MilitaryStatusUpdate.vue'),
meta: { requiresAuth: true, licenseeAccess: true, },
},
+ {
+ path: '/:compact/LicenseeVerification',
+ name: 'LicenseeVerification',
+ component: () => import(/* webpackChunkName: "licenseeDashboard" */ '@pages/LicenseeProof/LicenseeProof.vue'),
+ meta: { requiresAuth: true, licenseeAccess: true, },
+ },
{
path: '/:compact/Privileges',
name: 'PrivilegePurchase',
diff --git a/webroot/src/styles.common/_reset.less b/webroot/src/styles.common/_reset.less
index 78d917609..4d3f2a200 100644
--- a/webroot/src/styles.common/_reset.less
+++ b/webroot/src/styles.common/_reset.less
@@ -41,6 +41,10 @@ body {
font-weight: @fontWeightLight; // So there is an explicit root font-weight definition to inherit
font-size: @fontSize;
background-color: @white; // So that there is an explicit background-color set
+
+ @media print {
+ min-height: auto;
+ }
}
body > #jcc-app {
diff --git a/webroot/src/styles.common/mixins/buttons.less b/webroot/src/styles.common/mixins/buttons.less
index 44f1a3a3e..d656102cf 100644
--- a/webroot/src/styles.common/mixins/buttons.less
+++ b/webroot/src/styles.common/mixins/buttons.less
@@ -95,6 +95,13 @@
fill: @buttonTransparentHoverTextColor;
}
}
+
+ &:disabled {
+ border-color: @buttonDisabledBGColor;
+ color: @buttonDisabledTextColor;
+ background-color: @buttonDisabledBGColor;
+ cursor: default;
+ }
}
&.text-like {