Skip to content

Conversation

@rlorenzo
Copy link
Contributor

No description provided.

- Add instructor list/detail pages with search, create, edit, delete
- Enforce department-level authorization on search and create
- Add server-side search term validation (min 2 chars)
- Include InstructorService with AAUD integration for dept/title lookup
Copilot AI review requested due to automatic review settings December 24, 2025 19:33
@rlorenzo rlorenzo changed the base branch from main to VPR-19-effort-course-relationships December 24, 2025 19:33

it("should handle null report units from API", () => {
const apiReportUnit: string | null = null
const reportUnits = apiReportUnit ? apiReportUnit.split(",") : []
Comment on lines +463 to +472
foreach (var instructor in instructors)
{
if (string.IsNullOrWhiteSpace(instructor.EffortTitleCode)) continue;

var paddedCode = instructor.EffortTitleCode.PadLeft(6, '0');
if (titleLookup.TryGetValue(paddedCode, out var title))
{
instructor.Title = title;
}
}
Copy link

Copilot AI left a comment

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 implements manual instructor management functionality for the Effort system, allowing authorized users to import, edit, and delete instructors for specific terms.

Key Changes:

  • Backend service layer with full CRUD operations for instructors, including search, department resolution, and audit logging
  • REST API endpoints with authorization checks based on department access and permissions
  • Vue.js UI components for instructor list, detail view, and add/edit dialogs with comprehensive error handling

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
web/Program.cs Registers InstructorService in DI container
web/Areas/Effort/Services/InstructorService.cs Core service implementing instructor CRUD operations with department determination logic and title lookup
web/Areas/Effort/Services/IInstructorService.cs Service interface defining instructor management contracts
web/Areas/Effort/Services/EffortAuditService.cs Adds AddPersonChangeAudit method for instructor audit logging
web/Areas/Effort/Services/IEffortAuditService.cs Interface update for person change auditing
web/Areas/Effort/Controllers/InstructorsController.cs API controller with authorization, department filtering, and error handling
web/Areas/Effort/Models/DTOs/Requests/*.cs Request DTOs for creating and updating instructors with validation attributes
web/Areas/Effort/Models/DTOs/Responses/*.cs Response DTOs for instructor data, departments, report units, and AAUD person search
web/Areas/Effort/Models/AutoMapperProfileEffort.cs Mapping configuration for VolunteerWos byte-to-bool conversion
test/Effort/InstructorServiceTests.cs Unit tests for service layer operations
test/Effort/InstructorsControllerTests.cs Unit tests for API controller endpoints
VueApp/src/Effort/types/index.ts TypeScript type definitions for instructor-related DTOs
VueApp/src/Effort/services/effort-service.ts Frontend API client methods for instructor operations
VueApp/src/Effort/router/routes.ts Routes for instructor list and detail pages
VueApp/src/Effort/pages/InstructorList.vue Instructor list UI with filtering, grouping by department, and action buttons
VueApp/src/Effort/pages/InstructorDetail.vue Instructor detail page showing effort records
VueApp/src/Effort/components/InstructorAddDialog.vue Dialog for searching and adding instructors with autocomplete
VueApp/src/Effort/components/InstructorEditDialog.vue Dialog for editing instructor details with department and report unit selection
VueApp/src/Effort/components/EffortLeftNav.vue Navigation update to include instructor link
VueApp/src/Effort/composables/use-effort-permissions.ts Permission composable additions for instructor operations
VueApp/src/Effort/tests/*.test.ts Frontend unit tests for dialog error handling and state management

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

public string FullName => string.IsNullOrEmpty(MiddleInitial)
? $"{LastName}, {FirstName}"
: $"{LastName}, {FirstName} {MiddleInitial}.";
public string FullName => $"{LastName}, {FirstName}";
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The FullName property no longer includes the middle initial, which is a breaking change from the original implementation. The original code showed "LastName, FirstName M." when a middle initial was present. This inconsistency could affect display logic and user expectations throughout the application, especially since AaudPersonDto still includes middle initials in its FullName formatting. Consider keeping the original logic or documenting this intentional change.

Suggested change
public string FullName => $"{LastName}, {FirstName}";
public string FullName => string.IsNullOrWhiteSpace(MiddleInitial)
? $"{LastName}, {FirstName}"
: $"{LastName}, {FirstName} {MiddleInitial}.";

Copilot uses AI. Check for mistakes.
instructor.ReportUnit = request.ReportUnits != null && request.ReportUnits.Count > 0
? string.Join(",", request.ReportUnits)
: null;
instructor.VolunteerWos = request.VolunteerWos ? (byte)1 : null;
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The VolunteerWos database field is stored as byte? where 1=true, but the UpdateInstructorAsync method sets it as (byte)1 when true and null when false. This means there's no way to represent a false value (0) versus an unset value (null), which could lead to data loss when updating an instructor who was previously marked as non-volunteer. Consider using 0 for false instead of null to preserve the three-state distinction, or document why null is the intended representation for false.

Suggested change
instructor.VolunteerWos = request.VolunteerWos ? (byte)1 : null;
instructor.VolunteerWos = request.VolunteerWos ? (byte)1 : (byte)0;

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +123
public async Task<List<AaudPersonDto>> SearchPossibleInstructorsAsync(int termCode, string? searchTerm = null, CancellationToken ct = default)
{
// Enforce minimum search length server-side to prevent employee enumeration
if (string.IsNullOrWhiteSpace(searchTerm) || searchTerm.Trim().Length < 2)
{
return [];
}
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The search enforces a minimum of 2 characters server-side to prevent employee enumeration, but the frontend InstructorAddDialog also enforces this check client-side. However, the error case where searchTerm has fewer than 2 characters returns an empty array without any indication to the user that their input was too short. While the UI template shows "Type at least 2 characters to search", this behavior could be confusing if the backend validation changes. Consider adding a more explicit validation response or documented contract between frontend and backend.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +18
// VolunteerWos: byte? (1=true) → bool
CreateMap<EffortPerson, PersonDto>()
.ForMember(d => d.VolunteerWos, opt => opt.MapFrom(s => s.VolunteerWos == 1));
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The AutoMapper configuration maps VolunteerWos from byte? to bool using the expression "s.VolunteerWos == 1", but this only handles the true case. When VolunteerWos is 0 or null, both will map to false, which loses the distinction between "explicitly set to false" (0) and "never set" (null). This is inconsistent with how UpdateInstructorAsync sets the value (1 for true, null for false). The mapping should handle all three states consistently, or the database schema should be clarified.

Suggested change
// VolunteerWos: byte? (1=true) → bool
CreateMap<EffortPerson, PersonDto>()
.ForMember(d => d.VolunteerWos, opt => opt.MapFrom(s => s.VolunteerWos == 1));
// VolunteerWos: byte? (1=true, 0=false, null=unset) → bool?
CreateMap<EffortPerson, PersonDto>()
.ForMember(
d => d.VolunteerWos,
opt => opt.MapFrom(s =>
s.VolunteerWos == 1 ? (bool?)true :
s.VolunteerWos == 0 ? (bool?)false :
(bool?)null
)
);

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +86
if (string.IsNullOrEmpty(dept))
{
var allInstructors = new List<PersonDto>();
foreach (var authorizedDept in authorizedDepts)
{
var deptInstructors = await _instructorService.GetInstructorsAsync(termCode, authorizedDept, ct);
allInstructors.AddRange(deptInstructors);
}
return Ok(allInstructors.OrderBy(i => i.LastName).ThenBy(i => i.FirstName));
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The GetInstructorsAsync method when called without department filter for non-full-access users loops through authorized departments and makes individual database queries for each department. For users with many authorized departments, this creates an N+1 query problem. Consider using a single query with an IN clause on the department codes instead of multiple sequential queries to improve performance.

Copilot uses AI. Check for mistakes.
Comment on lines +463 to +472
foreach (var instructor in instructors)
{
if (string.IsNullOrWhiteSpace(instructor.EffortTitleCode)) continue;

var paddedCode = instructor.EffortTitleCode.PadLeft(6, '0');
if (titleLookup.TryGetValue(paddedCode, out var title))
{
instructor.Title = title;
}
}
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.

it("should handle null report units from API", () => {
const apiReportUnit: string | null = null
const reportUnits = apiReportUnit ? apiReportUnit.split(",") : []
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

This guard always evaluates to false.

Copilot uses AI. Check for mistakes.
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.

2 participants