Skip to content

VPR-64 feat(phone): schoolwide and unit phone lists - #323

Open
bniedzie wants to merge 3 commits into
mainfrom
feature/VPR-64-phone-lists
Open

VPR-64 feat(phone): schoolwide and unit phone lists#323
bniedzie wants to merge 3 commits into
mainfrom
feature/VPR-64-phone-lists

Conversation

@bniedzie

@bniedzie bniedzie commented Aug 25, 2026

Copy link
Copy Markdown

This PR migrates the schoolwide and Dean's Office phone lists from Viper 1. Viewing the lists requires only basic permissions, while specific permissions allow users to edit and maintain the lists. The lists are now housed in the new Personnel area.

The migration makes the following functional changes from the Viper 1 version:

  • Rather than using the PhoneList database, creates a phones schema in the VIPER database.
  • The database structure is different, normalizing the existing data. Phone is now stored at a person level, meaning that the same person will have the same data every time they occur across lists. Name data now comes from the user.Person table, which more easily handles name changes. The primary person identifier is now IAM ID rather than Mothra ID, per an initial meeting with Brandon.
  • Change history is now tracked, rather than showing only the latest changes. This fixes a bug where deletions would not affect the last modified date shown to users.
  • A new SVMSecure.PhoneLists.SVMMaintain permission guards editing the SVM list. In Viper 1, anyone with the link could make changes.
  • Removed supervisor from the VMDO data, as it is not surfaced anywhere. This can be pulled from existing UCPath data if needed at a later date.
  • Generalized the VMDO table to support the creation of arbitrary department/unit phone lists. These are keyed by a code (e.g., VMDO), allowing the list name to change without breaking links. Each allows separate permissions. VMDO is the only list currently present.
  • Changed the general Phone List view to display direct numbers to those with list maintenance permissions as well. Previously, these users could view the data on the maintenance page only.
  • Fixed a broken link in the general Phone List view.
  • Changed phone lists to be filterable and sortable.

This PR also does some refactoring around Person selection and dialog boxes. There should be no end user impact to CMS, but a few files are affected.

This PR requires schema changes to the Production database:

CREATE SCHEMA Inventory;


CREATE TABLE [phones].[Person](
	[PersonIam] [VARCHAR](10) NOT NULL,
	[Phone] [NVARCHAR](25) NOT NULL,
	[ModifiedDate] [DATETIME] NULL,
	[ModifiedBy] [VARCHAR](10) NULL,
	[DirectPhone] [NVARCHAR](25) NULL,
	[Office] [NVARCHAR](100) NULL,
 CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED 
(
	[PersonIam] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];


CREATE TABLE [phones].[PhoneList](
	[PhoneListId] [INT] IDENTITY(1,1) NOT NULL,
	[Name] [NVARCHAR](100) NOT NULL,
	[MaintainRole] [VARCHAR](100) NOT NULL,
	[Code] [NVARCHAR](20) NOT NULL,
 CONSTRAINT [PK_PhoneList] PRIMARY KEY CLUSTERED 
(
	[PhoneListId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];


CREATE TABLE [phones].[PhoneListUnit](
	[PhoneListUnitId] [INT] IDENTITY(1,1) NOT NULL,
	[PhoneListId] [INT] NOT NULL,
	[Name] [NVARCHAR](100) NOT NULL,
	[SortOrder] [INT] NULL,
 CONSTRAINT [PK_PhoneListUnit] PRIMARY KEY CLUSTERED 
(
	[PhoneListUnitId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];

ALTER TABLE [phones].[PhoneListUnit]  WITH CHECK ADD  CONSTRAINT [FK_PhoneListUnit_PhoneListId] FOREIGN KEY([PhoneListId])
REFERENCES [phones].[PhoneList] ([PhoneListId]);
ALTER TABLE [phones].[SVMUnitPerson] CHECK CONSTRAINT [FK_PhoneListUnit_PhoneListId];


CREATE TABLE [phones].[PhoneListUnitPerson](
	[PhoneListUnitPersonId] [INT] IDENTITY(1,1) NOT NULL,
	[PhoneListUnitId] [INT] NOT NULL,
	[PersonIam] [VARCHAR](10) NOT NULL,
	[ListFirst] [BIT] NOT NULL,
	[IsActive] [BIT] NOT NULL,
	[ModifiedBy] [VARCHAR](10) NULL,
	[ModifiedDate] [DATETIME] NULL,
 CONSTRAINT [PK_PhoneListUnitPerson] PRIMARY KEY CLUSTERED 
(
	[PhoneListUnitPersonId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];

ALTER TABLE [phones].[PhoneListUnitPerson] ADD  DEFAULT ((0)) FOR [ListFirst];
ALTER TABLE [phones].[PhoneListUnitPerson] ADD  DEFAULT ((1)) FOR [IsActive];
ALTER TABLE [phones].[PhoneListUnitPerson]  WITH CHECK ADD  CONSTRAINT [FK_PhoneListUnitPerson_PersonIam] FOREIGN KEY([PersonIam])
REFERENCES [phones].[Person] ([PersonIam]);
ALTER TABLE [phones].[SVMUnitPerson] CHECK CONSTRAINT [FK_PhoneListUnitPerson_PersonIam];
ALTER TABLE [phones].[PhoneListUnitPerson]  WITH CHECK ADD  CONSTRAINT [FK_PhoneListUnitPerson_PhoneListUnitId] FOREIGN KEY([PhoneListUnitId])
REFERENCES [phones].[PhoneListUnit] ([PhoneListUnitId]);
ALTER TABLE [phones].[SVMUnitPerson] CHECK CONSTRAINT [FK_PhoneListUnitPerson_PhoneListUnitId];


CREATE TABLE [phones].[SVMSection](
	[SectionId] [INT] IDENTITY(1,1) NOT NULL,
	[Name] [NVARCHAR](100) NULL,
	[IncludeAbbrv] [BIT] NOT NULL,
	[DirectorTitle] [NVARCHAR](50) NOT NULL,
	[SortOrder] [INT] NULL,
	[UnitName] [NVARCHAR](50) NULL,
 CONSTRAINT [PK_SVMSection] PRIMARY KEY CLUSTERED 
(
	[SectionId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];


CREATE TABLE [phones].[SVMUnit](
	[UnitId] [INT] IDENTITY(1,1) NOT NULL,
	[SectionId] [INT] NOT NULL,
	[Name] [NVARCHAR](100) NOT NULL,
	[Abbrv] [NVARCHAR](20) NULL,
	[SortOrder] [INT] NULL,
	[Fax] [NVARCHAR](25) NULL,
	[ModifiedBy] [VARCHAR](10) NULL,
	[ModifiedDate] [DATETIME] NULL,
 CONSTRAINT [PK_SVMUnit] PRIMARY KEY CLUSTERED 
(
	[UnitId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];

ALTER TABLE [phones].[SVMUnit]  WITH CHECK ADD  CONSTRAINT [FK_SVMUnit_SectionId] FOREIGN KEY([SectionId])
REFERENCES [phones].[SVMSection] ([SectionId]);
ALTER TABLE [phones].[SVMUnitPerson] CHECK CONSTRAINT [FK_SVMUnit_SectionId];


CREATE TABLE [phones].[SVMUnitPerson](
	[UnitPersonId] [INT] IDENTITY(1,1) NOT NULL,
	[UnitId] [INT] NOT NULL,
	[PersonIam] [VARCHAR](10) NOT NULL,
	[ModifiedDate] [DATETIME] NULL,
	[ModifiedBy] [VARCHAR](10) NULL,
	[Office] [NVARCHAR](50) NULL,
	[PosType] [NVARCHAR](25) NULL,
	[Interim] [NVARCHAR](10) NULL,
	[IsActive] [BIT] NOT NULL,
 CONSTRAINT [PK_SVMUnitPerson] PRIMARY KEY CLUSTERED 
(
	[UnitPersonId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];

ALTER TABLE [phones].[SVMUnitPerson] ADD  DEFAULT ((1)) FOR [IsActive];
ALTER TABLE [phones].[SVMUnitPerson]  WITH CHECK ADD  CONSTRAINT [FK_SVMUnitPerson_PersonId] FOREIGN KEY([PersonIam])
REFERENCES [phones].[Person] ([PersonIam]);
ALTER TABLE [phones].[SVMUnitPerson] CHECK CONSTRAINT [FK_SVMUnitPerson_PersonId];
ALTER TABLE [phones].[SVMUnitPerson]  WITH CHECK ADD  CONSTRAINT [FK_SVMUnitPerson_UnitId] FOREIGN KEY([UnitId])
REFERENCES [phones].[SVMUnit] ([UnitId]);
ALTER TABLE [phones].[SVMUnitPerson] CHECK CONSTRAINT [FK_SVMUnitPerson_UnitId];


CREATE TABLE [phones].[SVMFrequentNumber](
	[NumberID] [INT] IDENTITY(1,1) NOT NULL,
	[Label] [NVARCHAR](100) NOT NULL,
	[Phone] [NVARCHAR](25) NOT NULL,
	[SortOrder] [INT] NULL,
	[ModifiedBy] [VARCHAR](10) NULL,
	[ModifiedDate] [DATETIME] NULL,
	[IsActive] [BIT] NOT NULL,
 CONSTRAINT [PK_SVMFrequentNumber] PRIMARY KEY CLUSTERED 
(
	[NumberID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];

ALTER TABLE [phones].[SVMFrequentNumber] ADD  DEFAULT ((1)) FOR [IsActive];

This PR requires creating a new permission on Production: SVMSecure.PhoneLists.SVMMaintain.

This PR requires running the migration script .\RunMigrateData.bat Production for a dry run, and then .\RunMigrateData.bat Production --apply to migrate data into the new schema.

@codecov-commenter

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 999 bytes (0.04%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
viper-frontend-esm 2.29MB 999 bytes (0.04%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: viper-frontend-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/Files-*.js 946 bytes 23.52kB 4.19%
assets/ViperFetch-*.js 53 bytes 11.22kB 0.47%

Files in assets/Files-*.js:

  • ./src/CMS/components/PersonSelector.vue → Total Size: 150 bytes

  • ./src/CMS/components/FileFormDialog.vue → Total Size: 150 bytes

@codecov-commenter

codecov-commenter commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.05346% with 109 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.15%. Comparing base (c6f64b5) to head (d4aaacb).

Files with missing lines Patch % Lines
...ersonnel/Services/PhoneSVMFrequentNumberService.cs 84.00% 6 Missing and 6 partials ⚠️
VueApp/src/Personnel/pages/PhoneListMaintain.vue 84.12% 5 Missing and 5 partials ⚠️
...s/Personnel/Controllers/PhoneListUnitController.cs 84.37% 8 Missing and 2 partials ⚠️
...el/Controllers/PhoneSVMFrequentNumberController.cs 70.00% 9 Missing ⚠️
VueApp/src/Personnel/composables/svm-data-fetch.ts 91.39% 3 Missing and 5 partials ⚠️
VueApp/src/Personnel/components/PersonSelector.vue 53.33% 7 Missing ⚠️
...b/Areas/Personnel/Services/PhoneListUnitService.cs 96.33% 2 Missing and 5 partials ⚠️
.../Personnel/components/PhoneListAddRecordDialog.vue 82.35% 5 Missing and 1 partial ⚠️
VueApp/src/Personnel/pages/SVMPhonesMaintain.vue 92.64% 2 Missing and 3 partials ⚠️
...src/Personnel/composables/use-add-record-dialog.ts 88.88% 3 Missing and 1 partial ⚠️
... and 15 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #323      +/-   ##
==========================================
+ Coverage   42.38%   44.15%   +1.76%     
==========================================
  Files         994     1056      +62     
  Lines       49877    51651    +1774     
  Branches     5887     6070     +183     
==========================================
+ Hits        21142    22804    +1662     
- Misses      27798    27875      +77     
- Partials      937      972      +35     
Flag Coverage Δ
backend 41.70% <95.43%> (+1.33%) ⬆️
frontend 62.44% <91.88%> (+3.48%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
VueApp/src/CMS/components/PersonSelector.vue 28.57% <100.00%> (-38.10%) ⬇️
...ueApp/src/Personnel/components/ModifiedSummary.vue 100.00% <100.00%> (ø)
...pp/src/Personnel/components/PhoneListUnitTable.vue 100.00% <100.00%> (ø)
...pp/src/Personnel/components/RecordActionButton.vue 100.00% <100.00%> (ø)
...pp/src/Personnel/components/SVMAddRecordDialog.vue 100.00% <100.00%> (ø)
...rc/Personnel/components/SVMFrequentNumberTable.vue 100.00% <100.00%> (ø)
.../src/Personnel/components/SVMPhoneSectionTable.vue 100.00% <100.00%> (ø)
...src/Personnel/composables/phone-list-data-fetch.ts 100.00% <100.00%> (ø)
...App/src/Personnel/composables/use-person-helper.ts 100.00% <100.00%> (ø)
VueApp/src/Personnel/router/index.ts 100.00% <100.00%> (ø)
... and 55 more

... and 2 files with indirect coverage changes

@rlorenzo

Copy link
Copy Markdown
Contributor

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review skipped: 122 files exceed the limit of 100.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the schoolwide (SVM) and Dean's Office (VMDO) phone lists from Viper 1 into a new Personnel area, backed by a new normalized phones schema in the VIPER database. Viewing requires basic SVMSecure permission, while a new SVMSecure.PhoneLists.SVMMaintain permission (and per-list MaintainRole) gates editing. It adds EF Core models/services/controllers plus a full Vue 3/Quasar SPA, and refactors shared person-search logic into a reusable PersonSearchHelper used by both CMS and Personnel.

Changes:

  • New phones schema + PhonesDbContext, EF models, area services and /api/phones/... controllers with dynamic per-list maintain permissions and direct-number masking.
  • New Personnel Vue SPA (lists, maintenance views, person selector, record dialogs) plus data-migration scripts from the legacy PhoneList database.
  • Shared PersonSearchHelper extracted and adopted by CMS's SearchPeople, forcing EF parameterization (ESCAPE clause) to prevent per-term query plans and %/_ wildcard injection.

Reviewed changes

Copilot reviewed 121 out of 122 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
web/Viper.csproj Excludes Areas\Personnel\Scripts\** (separate migration project) from the web build, mirroring the Effort area.
web/Program.cs Registers PhonesDbContext, adds Personnel SPA name and the Personnel services namespace to Scrutor registration.
web/Classes/Utilities/PersonSearchHelper.cs New shared expression-tree helper for name-search autocomplete with parameterized Contains matching.
web/Areas/Personnel/Services/PhoneSVMSectionService.cs Read-only query for SVM sections, ordered with null-safe sort.
web/Areas/Personnel/Services/PhoneSVMFrequentNumberService.cs CRUD + soft-delete for SVM frequent numbers, with modified-date tracking.
web/Areas/Personnel/Services/PhonePersonLookupService.cs Looks up phone people by IAM IDs (direct number masked unless maintainer) and current-employee search.
web/Areas/Personnel/Services/PhonePermissionsService.cs Resolves edit permission from the list's MaintainRole column.
web/Areas/Personnel/Controllers/PhonePersonController.cs Person-picker endpoint merging Viper and phone data; uses foreach/Add where .Select() is preferred.
web/Areas/Personnel/Controllers/PhoneSVMModifiedDateController.cs Returns latest SVM modified date; contains a comment typo ("Identfies").
web/Areas/Personnel/Models/*, VueApp/src/Personnel/** New EF models/DTOs/Mapperly mapper and the Personnel Vue SPA (services, composables, components, tests).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +39 to +43
List<string> iamIds = [];
foreach (ViperPerson result in viperResults)
{
iamIds.Add(result.IamId);
}
private readonly PhoneSVMUnitService _phoneSVMUnitService = phoneSVMUnitService;

/// <summary>
/// Identfies when frequent numbers were last modified.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants