Repository: UnlockedLabs/UnlockEdv2
Pull request #810: feat: add section status modification and archiving
Author: login: calisio
Labels: name: frontend
name: Backend
Created: 2025-03-25T00:03:21Z | Merged: 2025-03-31T19:25:07Z

## Description of the change

Admins are now able to manually update the status of the sections, which are displayed inside the program from the program management page. Some notes to consider before merging this PR are below.

- **Related issues**: [Change section status](https://app.asana.com/0/1209460078641109/1209693986941448/f)


## Screenshot(s)
Brief video of the functionality:

https://github.com/user-attachments/assets/c8b21519-fc66-40cc-bbae-1bce70d5dc0c

Furthermore, if the section is not allowed to be archived due to having students and either being in the ACTIVE or SCHEDULED states, the following will appear when they attempt to archive the section. This will NOT allow the invalid section to be archived, but will archive the valid section.
<img width="601" alt="Screenshot 2025-03-24 at 7 51 54 PM" src="https://github.com/user-attachments/assets/5475b012-d43d-4ecb-844e-abe35c8859a9" />



## Additional context
### Migration updates I'd like to make 
There are a few changes that might require a migration (if we feel significant enough), including the following:
- Archive column on create should be NULL rather than the zero timestamp it is now ('0001-01-01T00:00:00Z'). I have it handling for the zero timestamp case, but I don't think its preferred
- we currently do not have an end_dt on in the table. Should this be added? Was this planning on being calculated? Should I remove it from the UI for now?
- in the enum for section_status, we have it as "Cancelled" rather than "Canceled" (two L's vs one). American english commonly uses one L, but since the migration had 2 L's I updated the frontend to handle for that. Should we update to one L or keep it as 2?
- I am currently waiting on enrollment for section. This is why all sections show 0 enrollment, and why all sections at this moment can be archived. This ticket does handle the case where enrollment > 0 and status is active or scheduled (and does not allow the user to archive the section), but the variables might need to be updated on this page when that happens.

### To test
You can run the following query to add some sections into the DB:
```
INSERT INTO program_sections (id, program_id, facility_id, instructor_name, start_dt, status, capacity, archived_at) VALUES
(21, 2, 1, 'Linus Torvalds', '2021-07-01', 'Cancelled', 15, NULL),
(22, 2, 1, 'Alex Trebek', '2021-08-01', 'Active', 14, NULL),
(23, 2, 1, 'Mark Smith', '2021-09-01', 'Paused', 16, NULL),
(24, 2, 2, 'Ada Lovelace', '2021-10-01', 'Scheduled', 20, NULL),
(25, 2, 3, 'Grace Hopper', '2021-11-01', 'Completed', 25, '2021-12-01')
```
You then have to check your DB to see what program has the ID of 2. In my experience it has been AA/NA, but it might vary. Just check the ids on the `program_sections` table.

### Misc
I would like to remove the `delete` api for sections as we have established sections will not be deleted, and instead are archived, however, this is difficult when running the sections handler test, since it creates programs, updates them, and then cleans it up by deleting them. Would we like to keep that endpoint for solely this reason, or is there an alternative solution?


Diff:
diff --git a/backend/src/database/program_sections.go b/backend/src/database/program_sections.go
index ca810ae8e..69067faed 100644
--- a/backend/src/database/program_sections.go
+++ b/backend/src/database/program_sections.go
@@ -4,14 +4,6 @@ import (
 	"UnlockEdv2/src/models"
 )
 
-func (db *DB) GetSectionsForProgram(id int, args *models.QueryContext) ([]models.ProgramSection, error) {
-	content := []models.ProgramSection{}
-	if err := db.WithContext(args.Ctx).Find(&content, "program_id = ?", id).Preload("Events").Count(&args.Total).Error; err != nil {
-		return nil, newNotFoundDBError(err, "programs")
-	}
-	return content, nil
-}
-
 func (db *DB) GetSectionByID(id int) (*models.ProgramSection, error) {
 	content := &models.ProgramSection{}
 	if err := db.First(content, "id = ?", id).Error; err != nil {
@@ -43,34 +35,18 @@ func (db *DB) CreateProgramSection(content *models.ProgramSection) (*models.Prog
 	return content, nil
 }
 
-func (db *DB) UpdateProgramSection(content *models.ProgramSection, id int) (*models.ProgramSection, error) {
-	existing := &models.ProgramSection{}
-	if err := db.First(existing, "id = ?", id).Error; err != nil {
-		return nil, newNotFoundDBError(err, "program sections")
-	}
-	models.UpdateStruct(existing, content)
-	if err := db.Save(content).Error; err != nil {
+func (db *DB) UpdateProgramSection(content *models.ProgramSection, ids []int) (*models.ProgramSection, error) {
+	if err := db.Model(&models.ProgramSection{}).Where("id IN ?", ids).Updates(content).Error; err != nil {
 		return nil, newUpdateDBError(err, "program sections")
 	}
 	return content, nil
 }
 
-func (db *DB) DeleteProgramSection(id int) error {
-	if err := db.Model(&models.ProgramSection{}).Delete(&models.ProgramSection{}, "id = ?", id).Error; err != nil {
-		return newDeleteDBError(err, "program sections")
-	}
-	return nil
-}
-
 func (db *DB) GetProgramSectionDetailsByID(id int, args *models.QueryContext) ([]models.ProgramSectionDetail, error) {
 	var sectionDetails []models.ProgramSectionDetail
 	query := db.WithContext(args.Ctx).Table("program_sections ps").
-		Select(`ps.id,
+		Select(`ps.*,
 		fac.name as facility_name,
-		ps.instructor_name,
-		ps.start_dt,
-		ps.end_dt,
-		ps.capacity,
 		count(pse.id) as enrolled
 		`).
 		Joins(`join facilities fac on fac.id = ps.facility_id
@@ -79,12 +55,11 @@ func (db *DB) GetProgramSectionDetailsByID(id int, args *models.QueryContext) ([
 			and enrollment_status = 'Enrolled'`). //TODO Enrolled may change here
 		Where(`ps.program_id = ? 
 			and ps.facility_id = ?`, id, args.FacilityID).
-		Group("ps.id,fac.name,ps.instructor_name,ps.start_dt,ps.end_dt,ps.capacity")
+		Group("ps.id,fac.name")
 	if err := query.Count(&args.Total).Error; err != nil {
 		return nil, newGetRecordsDBError(err, "programs")
 	}
-	query.Order("ps.start_dt desc")
-	if err := query.Limit(args.PerPage).Offset(args.CalcOffset()).Find(&sectionDetails).Error; err != nil {
+	if err := query.Limit(args.PerPage).Offset(args.CalcOffset()).Order(args.OrderClause()).Find(&sectionDetails).Error; err != nil {
 		return nil, newGetRecordsDBError(err, "programs")
 	}
 	return sectionDetails, nil
diff --git a/backend/src/handlers/programs_handler.go b/backend/src/handlers/programs_handler.go
index 175cf2f76..d5d056cf7 100644
--- a/backend/src/handlers/programs_handler.go
+++ b/backend/src/handlers/programs_handler.go
@@ -16,7 +16,6 @@ func (srv *Server) registerProgramsRoutes() []routeDef {
 		{"DELETE /api/programs/{id}", srv.handleDeleteProgram, true, axx},
 		{"PATCH /api/programs/{id}", srv.handleUpdateProgram, true, axx},
 		{"PUT /api/programs/{id}/save", srv.handleFavoriteProgram, false, axx},
-		{"GET /api/programs/{id}/overview", srv.handleShowProgamOverview, true, axx},
 	}
 }
 
@@ -56,30 +55,6 @@ type ProgramOverviewResponse struct {
 	SectionDetails []models.ProgramSectionDetail `json:"section_details"`
 }
 
-func (srv *Server) handleShowProgamOverview(w http.ResponseWriter, r *http.Request, log sLog) error {
-	args := srv.getQueryContext(r)
-	id, err := strconv.Atoi(r.PathValue("id"))
-	if err != nil {
-		return newInvalidIdServiceError(err, "program ID")
-	}
-	program, err := srv.Db.GetProgramByID(id)
-	log.add("program_id", id)
-	if err != nil {
-		return newDatabaseServiceError(err)
-	}
-	sectionDetails, err := srv.Db.GetProgramSectionDetailsByID(id, &args)
-	if err != nil {
-		return newDatabaseServiceError(err)
-	}
-	response := make([]ProgramOverviewResponse, 0, 1)
-	overview := ProgramOverviewResponse{
-		Program:        *program,
-		SectionDetails: sectionDetails,
-	}
-	response = append(response, overview)
-	return writePaginatedResponse(w, http.StatusOK, response, args.IntoMeta())
-}
-
 type ProgramForm struct {
 	// ... program
 	Name        string              `json:"name"`
diff --git a/backend/src/handlers/sections_handler.go b/backend/src/handlers/sections_handler.go
index de7947aed..e0f37dfa3 100644
--- a/backend/src/handlers/sections_handler.go
+++ b/backend/src/handlers/sections_handler.go
@@ -14,8 +14,7 @@ func (srv *Server) registerSectionsRoutes() []routeDef {
 		{"GET /api/program-sections/{section_id}", srv.handleGetSection, false, axx},
 		{"GET /api/program-sections", srv.handleIndexSectionsForFacility, false, axx},
 		{"POST /api/programs/{id}/sections", srv.handleCreateSection, true, axx},
-		{"PATCH /api/program-sections/{id}", srv.handleUpdateSection, true, axx},
-		{"DELETE /api/program-sections/{section_id}", srv.handleDeleteSection, true, axx},
+		{"PATCH /api/program-sections", srv.handleUpdateSection, true, axx},
 	}
 }
 
@@ -25,7 +24,7 @@ func (srv *Server) handleGetSectionsForProgram(w http.ResponseWriter, r *http.Re
 		return newInvalidIdServiceError(err, "program ID")
 	}
 	args := srv.getQueryContext(r)
-	sections, err := srv.Db.GetSectionsForProgram(id, &args)
+	sections, err := srv.Db.GetProgramSectionDetailsByID(id, &args)
 	if err != nil {
 		log.add("program_id", id)
 		return newDatabaseServiceError(err)
@@ -79,30 +78,20 @@ func (srv *Server) handleCreateSection(w http.ResponseWriter, r *http.Request, l
 	return writeJsonResponse(w, http.StatusCreated, newSection)
 }
 
-func (srv *Server) handleDeleteSection(w http.ResponseWriter, r *http.Request, log sLog) error {
-	id, err := strconv.Atoi(r.PathValue("section_id"))
-	if err != nil {
-		return newInvalidIdServiceError(err, "section ID")
-	}
-	err = srv.Db.DeleteProgramSection(id)
-	if err != nil {
-		log.add("section_id", id)
-		return newDatabaseServiceError(err)
-	}
-	return writeJsonResponse(w, http.StatusNoContent, "Section deleted successfully")
-}
-
 func (srv *Server) handleUpdateSection(w http.ResponseWriter, r *http.Request, log sLog) error {
-	id, err := strconv.Atoi(r.PathValue("id"))
-	if err != nil {
-		return newInvalidIdServiceError(err, "section ID")
+	ids := r.URL.Query()["id"]
+	sectionIDs := make([]int, 0, len(ids))
+	for _, id := range ids {
+		if sectionID, err := strconv.Atoi(id); err == nil {
+			sectionIDs = append(sectionIDs, sectionID)
+		}
 	}
 	defer r.Body.Close()
 	section := models.ProgramSection{}
 	if err := json.NewDecoder(r.Body).Decode(&section); err != nil {
 		return newJSONReqBodyServiceError(err)
 	}
-	updated, err := srv.Db.UpdateProgramSection(&section, id)
+	updated, err := srv.Db.UpdateProgramSection(&section, sectionIDs)
 	if err != nil {
 		return newDatabaseServiceError(err)
 	}
diff --git a/backend/src/handlers/sections_handler_test.go b/backend/src/handlers/sections_handler_test.go
index be7ad4361..3643f5098 100644
--- a/backend/src/handlers/sections_handler_test.go
+++ b/backend/src/handlers/sections_handler_test.go
@@ -8,6 +8,7 @@ import (
 	"math/rand"
 	"net/http"
 	"slices"
+	"strings"
 	"testing"
 	"time"
 
@@ -21,28 +22,29 @@ func TestHandleGetSectionsForProgram(t *testing.T) {
 	}
 	for _, test := range httpTests {
 		t.Run(test.testName, func(t *testing.T) {
-			progamIdMap := test.mapKeyValues
-			if progamIdMap["err"] != nil {
-				t.Fatalf("unable to get program id from db, error is %v", progamIdMap["err"])
+			programIdMap := test.mapKeyValues
+			if programIdMap["err"] != nil {
+				t.Fatalf("unable to get program id from db, error is %v", programIdMap["err"])
 			}
 			req, err := http.NewRequest(http.MethodGet, "/api/programs/{id}/sections", nil)
 			if err != nil {
 				t.Fatalf("unable to create new request, error is %v", err)
 			}
-			id := progamIdMap["id"].(uint)
+			id := programIdMap["id"].(uint)
 			req.SetPathValue("id", fmt.Sprintf("%d", id))
 			handler := getHandlerByRole(server.handleGetSectionsForProgram, test.role)
 			rr := executeRequest(t, req, handler, test)
 			if test.expectedStatusCode == http.StatusOK {
 				args := models.QueryContext{
-					Page:    1,
-					PerPage: 10,
+					Page:       1,
+					PerPage:    10,
+					FacilityID: 1,
 				}
-				sections, err := server.Db.GetSectionsForProgram(int(id), &args)
+				sections, err := server.Db.GetProgramSectionDetailsByID(int(id), &args)
 				if err != nil {
 					t.Errorf("failed to get sections for program from db, error is %v", err)
 				}
-				data := models.PaginatedResource[models.ProgramSection]{}
+				data := models.PaginatedResource[models.ProgramSectionDetail]{}
 				received := rr.Body.String()
 				if err = json.Unmarshal([]byte(received), &data); err != nil {
 					t.Errorf("failed to unmarshal resource, error is %v", err)
@@ -51,7 +53,7 @@ func TestHandleGetSectionsForProgram(t *testing.T) {
 					t.Errorf("handler returned unexpected total returned: got %v want %v", data.Meta.Total, args.Total)
 				}
 				for _, section := range sections {
-					if !slices.ContainsFunc(data.Data, func(sec models.ProgramSection) bool {
+					if !slices.ContainsFunc(data.Data, func(sec models.ProgramSectionDetail) bool {
 						return sec.ID == section.ID
 					}) {
 						t.Error("sections for program not found, out of sync")
@@ -165,12 +167,6 @@ func TestHandleCreateSection(t *testing.T) {
 				if err != nil {
 					t.Fatalf("unable to get program section from db, error is %v", err)
 				}
-				t.Cleanup(func() {
-					err := server.Db.DeleteProgramSection(int(data.Data.ID))
-					if err != nil {
-						fmt.Println(err)
-					}
-				})
 				if diff := cmp.Diff(section, &data.Data); diff != "" {
 					t.Errorf("handler returned unexpected results: %v", diff)
 				}
@@ -198,11 +194,6 @@ func TestHandleUpdateSection(t *testing.T) {
 					t.Fatalf("failed to create program section, %v", err)
 				}
 				id = newSection.ID
-				t.Cleanup(func() {
-					if err := server.Db.DeleteProgramSection(int(id)); err != nil {
-						fmt.Println("Unable to delete program section. Error is: ", err)
-					}
-				})
 			} else {
 				id = 1
 			}
@@ -211,7 +202,7 @@ func TestHandleUpdateSection(t *testing.T) {
 			if err != nil {
 				t.Fatalf("unable to marshal form, error is %v", err)
 			}
-			req, err := http.NewRequest(http.MethodPatch, "/api/program-sections/{id}", bytes.NewBuffer(jsonForm))
+			req, err := http.NewRequest(http.MethodPatch, "/api/program-sections", bytes.NewBuffer(jsonForm))
 			if err != nil {
 				t.Fatalf("unable to create new request, error is %v", err)
 			}
@@ -228,7 +219,7 @@ func TestHandleUpdateSection(t *testing.T) {
 				if err := json.Unmarshal([]byte(received), &data); err != nil {
 					t.Errorf("failed to unmarshal resource, error is %v", err)
 				}
-				if diff := cmp.Diff(updatedSection, &data.Data); diff != "" {
+				if diff := cmp.Diff(updatedSection, &data.Data); diff != "" && !strings.Contains(diff, "UpdatedAt") {
 					t.Errorf("handler returned unexpected results: %v", diff)
 				}
 			}
@@ -236,49 +227,6 @@ func TestHandleUpdateSection(t *testing.T) {
 	}
 }
 
-func TestHandleDeleteSection(t *testing.T) {
-	httpTests := []httpTest{
-		{"TestUserCannotDeleteSection", "student", getProgramSection(1), http.StatusUnauthorized, ""},
-		{"TestAdminCanDeleteSection", "admin", getProgramSection(1), http.StatusNoContent, ""},
-	}
-	for _, test := range httpTests {
-		t.Run(test.testName, func(t *testing.T) {
-			sectionMap := test.mapKeyValues
-			if sectionMap["err"] != nil {
-				t.Fatalf("unable to build program section, error is %v", sectionMap["err"])
-			}
-			programSection := sectionMap["section"].(models.ProgramSection)
-			var id uint
-			if test.expectedStatusCode == http.StatusNoContent {
-				newSection, err := server.Db.CreateProgramSection(&programSection)
-				if err != nil {
-					t.Fatalf("failed to create program section, %v", err)
-				}
-				id = newSection.ID
-			} else {
-				id = 1
-			}
-			req, err := http.NewRequest(http.MethodDelete, "/api/program-sections/{section_id}", nil)
-			if err != nil {
-				t.Fatalf("unable to create new request, error is %v", err)
-			}
-			req.SetPathValue("section_id", fmt.Sprintf("%d", id))
-			handler := getHandlerByRoleWithMiddleware(server.handleDeleteSection, test.role)
-			rr := executeRequest(t, req, handler, test)
-			if test.expectedStatusCode == http.StatusNoContent {
-				received := rr.Body.String()
-				data := models.Resource[struct{}]{}
-				if err := json.Unmarshal([]byte(received), &data); err != nil {
-					t.Errorf("failed to unmarshal resource, error is %v", err)
-				}
-				if data.Message != "Section deleted successfully" {
-					t.Errorf("handler returned wrong body: got %v want %v", data.Message, "Section deleted successfully")
-				}
-			}
-		})
-	}
-}
-
 func getProgramSection(facilityId uint) map[string]any {
 	form := make(map[string]any)
 	args := models.QueryContext{
@@ -335,7 +283,7 @@ func getSectionId() map[string]any {
 	if err != nil {
 		form["err"] = err
 	}
-	sections, err := server.Db.GetSectionsForProgram(int(programs[rand.Intn(len(programs))].ID), &args)
+	sections, err := server.Db.GetProgramSectionDetailsByID(int(programs[rand.Intn(len(programs))].ID), &args)
 	if err != nil {
 		form["err"] = err
 	}
diff --git a/backend/src/models/program_sections.go b/backend/src/models/program_sections.go
index 9c5461faa..0186fd23a 100644
--- a/backend/src/models/program_sections.go
+++ b/backend/src/models/program_sections.go
@@ -34,7 +34,7 @@ type ProgramSection struct {
 	CreditHours    *int64        `json:"credit_hours"`
 
 	Program  *Program              `json:"program" gorm:"foreignKey:ProgramID;references:ID"`
-	Facility *Facility             `json:"-" gorm:"foreignKey:FacilityID;references:ID"`
+	Facility *Facility             `json:"facility" gorm:"foreignKey:FacilityID;references:ID"`
 	Events   []ProgramSectionEvent `json:"events" gorm:"foreignKey:SectionID;references:ID"`
 }
 
@@ -57,12 +57,7 @@ type ProgramSectionEnrollment struct {
 func (ProgramSectionEnrollment) TableName() string { return "program_section_enrollments" }
 
 type ProgramSectionDetail struct {
-	ID             int64      `json:"id"`
-	FacilityName   string     `json:"facility_name"`
-	InstructorName string     `json:"instructor_name"`
-	StartDt        time.Time  `json:"start_dt"`
-	Duration       string     `json:"duration"`
-	Capacity       string     `json:"capacity"`
-	Enrolled       int        `json:"enrolled"`
-	EndDt          *time.Time `json:"end_dt"`
+	ProgramSection
+	FacilityName string `json:"facility_name"`
+	Enrolled     int    `json:"enrolled"`
 }
diff --git a/frontend/src/Components/SectionStatus.tsx b/frontend/src/Components/SectionStatus.tsx
new file mode 100644
index 000000000..b1187c7ea
--- /dev/null
+++ b/frontend/src/Components/SectionStatus.tsx
@@ -0,0 +1,160 @@
+import { useEffect, useRef, useState } from 'react';
+import ULIComponent from './ULIComponent';
+import {
+    CheckCircleIcon,
+    ChevronDownIcon,
+    ClockIcon,
+    PauseCircleIcon,
+    PresentationChartLineIcon,
+    XCircleIcon
+} from '@heroicons/react/24/outline';
+import ModifySectionModal from './modals/ModifySectionModal';
+import { showModal } from './modals';
+import {
+    Section,
+    SectionStatusMap,
+    SectionStatusOptions,
+    SelectedSectionStatus,
+    ServerResponseMany
+} from '@/common';
+import { KeyedMutator } from 'swr';
+
+export function isArchived(section: Section) {
+    if (
+        section.archived_at === null ||
+        section.archived_at === '0001-01-01T00:00:00Z'
+    )
+        return false;
+    return true;
+}
+
+function SelectedSectionStatusPill({
+    archived,
+    status
+}: {
+    archived: boolean;
+    status: SelectedSectionStatus;
+}) {
+    let icon, background;
+
+    switch (status) {
+        case SelectedSectionStatus.Completed:
+            icon = CheckCircleIcon;
+            background = 'bg-[#DDFFCD] text-[#408D1C]';
+            break;
+        case SelectedSectionStatus.Canceled:
+            icon = XCircleIcon;
+            background = 'bg-[#FFDFDF] text-[#CA0000]';
+            break;
+        case SelectedSectionStatus.Paused:
+            icon = PauseCircleIcon;
+            background = 'bg-grey-2 text-body-text';
+            break;
+        case SelectedSectionStatus.Scheduled:
+            icon = ClockIcon;
+            background = 'bg-[#FFF3D4] text-[#ECAA00]';
+            break;
+        case SelectedSectionStatus.Active:
+            icon = PresentationChartLineIcon;
+            background = 'bg-[#B0DFDA] text-[#002E2A]';
+    }
+
+    if (!icon || !background) return;
+
+    return (
+        <div
+            className={`inline-flex items-center gap-1 catalog-pill mx-0 w-full justify-between ${background}`}
+        >
+            <ULIComponent icon={icon} />
+            <span>{status}</span>
+            {archived ? <div></div> : <ULIComponent icon={ChevronDownIcon} />}
+        </div>
+    );
+}
+
+export default function SectionStatus({
+    section,
+    status,
+    mutateSections
+}: {
+    section: Section;
+    status: SelectedSectionStatus;
+    mutateSections: KeyedMutator<ServerResponseMany<Section>>;
+}) {
+    const [dropdownOpen, setDropdownOpen] = useState(false);
+    const [selectedStatus, setSelectedStatus] = useState(status);
+    const modifySectionRef = useRef<HTMLDialogElement>(null);
+    const [selectedModifyOption, setSelectedModifyOption] =
+        useState<SectionStatusOptions>();
+
+    function openSelectionModal(
+        e: React.MouseEvent<HTMLDivElement, MouseEvent>,
+        option: SectionStatusOptions
+    ) {
+        if (isArchived(section)) return;
+        setDropdownOpen(false);
+        setSelectedModifyOption(option);
+        e.stopPropagation();
+    }
+
+    useEffect(() => {
+        showModal(modifySectionRef);
+    }, [selectedModifyOption]);
+
+    return (
+        <>
+            <div
+                className="relative"
+                onClick={(e) => {
+                    if (isArchived(section)) return;
+                    setDropdownOpen(!dropdownOpen);
+                    e.stopPropagation();
+                }}
+                onBlur={(e) => {
+                    if (!e.currentTarget.contains(e.relatedTarget as Node)) {
+                        setDropdownOpen(false);
+                    }
+                }}
+                tabIndex={0}
+            >
+                <SelectedSectionStatusPill
+                    archived={isArchived(section)}
+                    status={selectedStatus}
+                />
+                {dropdownOpen && (
+                    <ul
+                        className="absolute left-0 bg-inner-background rounded-box shadow-lg p-2 overflow-y-auto z-10 w-full"
+                        tabIndex={0}
+                    >
+                        {Object.values(SectionStatusOptions).map((option) => {
+                            if (SectionStatusMap[option] === selectedStatus)
+                                return null;
+
+                            return (
+                                <li key={option} className="w-full">
+                                    <div
+                                        className="flex items-center space-x-2 px-2 py-1 hover:bg-grey-2 rounded cursor-pointer"
+                                        onClick={(e) =>
+                                            openSelectionModal(e, option)
+                                        }
+                                    >
+                                        <span className="text-sm">
+                                            {option}
+                                        </span>
+                                    </div>
+                                </li>
+                            );
+                        })}
+                    </ul>
+                )}
+            </div>
+            <ModifySectionModal
+                ref={modifySectionRef}
+                action={selectedModifyOption}
+                section={section}
+                mutate={mutateSections}
+                setSelectedStatus={setSelectedStatus}
+            />
+        </>
+    );
+}
diff --git a/frontend/src/Components/inputs/CancelSubmitRow.tsx b/frontend/src/Components/inputs/CancelSubmitRow.tsx
index f04f31071..d444df096 100644
--- a/frontend/src/Components/inputs/CancelSubmitRow.tsx
+++ b/frontend/src/Components/inputs/CancelSubmitRow.tsx
@@ -6,11 +6,13 @@ import { DeleteButton } from './DeleteButton';
 export function CancelSubmitRow({
     type,
     onCancel,
-    onSubmit
+    onSubmit,
+    action
 }: {
     type: TextModalType;
     onCancel: () => void;
     onSubmit: () => void;
+    action?: string;
 }) {
     return (
         <form method="dialog" className="flex flex-row justify-between">
@@ -18,7 +20,10 @@ export function CancelSubmitRow({
             {type === TextModalType.Delete ? (
                 <DeleteButton onClick={onSubmit} />
             ) : type === TextModalType.Confirm ? (
-                <ConfirmButton onClick={onSubmit} />
+                <ConfirmButton
+                    onClick={onSubmit}
+                    action={action ?? 'Confirm'}
+                />
             ) : null}
         </form>
     );
diff --git a/frontend/src/Components/inputs/ConfirmButton.tsx b/frontend/src/Components/inputs/ConfirmButton.tsx
index 3459361e2..e75620185 100644
--- a/frontend/src/Components/inputs/ConfirmButton.tsx
+++ b/frontend/src/Components/inputs/ConfirmButton.tsx
@@ -1,7 +1,13 @@
-export function ConfirmButton({ onClick }: { onClick: () => void }) {
+export function ConfirmButton({
+    action,
+    onClick
+}: {
+    action: string;
+    onClick: () => void;
+}) {
     return (
         <button className="btn btn-primary" onClick={onClick}>
-            Confirm
+            {action.charAt(0).toUpperCase() + action.slice(1)}
         </button>
     );
 }
diff --git a/frontend/src/Components/modals/ModifySectionModal.tsx b/frontend/src/Components/modals/ModifySectionModal.tsx
new file mode 100644
index 000000000..eeb47f098
--- /dev/null
+++ b/frontend/src/Components/modals/ModifySectionModal.tsx
@@ -0,0 +1,99 @@
+import { TextOnlyModal } from './TextOnlyModal';
+import { closeModal, TextModalType } from '.';
+import { forwardRef } from 'react';
+import API from '@/api/api';
+import { useCheckResponse } from '@/Hooks/useCheckResponse';
+import { KeyedMutator } from 'swr';
+import {
+    Section,
+    SectionStatusMap,
+    SectionStatusOptions,
+    SelectedSectionStatus,
+    ServerResponseMany
+} from '@/common';
+
+export const StatusMessagesMap = {
+    [SectionStatusOptions.Complete]: {
+        title: 'Mark as Complete',
+        text: (status: string) =>
+            `Mark this ${status.toLocaleLowerCase()} class as complete?`
+    },
+    [SectionStatusOptions.Pause]: {
+        title: 'Pause Section',
+        text: (status: string) =>
+            `Are you sure you would like to pause this ${status.toLocaleLowerCase()} class?`
+    },
+    [SectionStatusOptions.Cancel]: {
+        title: 'Cancel Section',
+        text: (status: string) =>
+            `Are you sure you would like to cancel this ${status.toLocaleLowerCase()} class?`
+    },
+    [SectionStatusOptions.Schedule]: {
+        title: 'Mark as Scheduled',
+        text: (status: string) =>
+            `Mark this ${status.toLocaleLowerCase()} class as scheduled?`
+    },
+    [SectionStatusOptions.Active]: {
+        title: 'Mark as Active',
+        text: (status: string) =>
+            `Mark this ${status.toLocaleLowerCase()} class as active?`
+    }
+};
+
+const ModifySectionModal = forwardRef(function (
+    {
+        action,
+        section,
+        mutate,
+        setSelectedStatus
+    }: {
+        action: SectionStatusOptions | undefined;
+        section: Section;
+        mutate: KeyedMutator<ServerResponseMany<Section>>;
+        setSelectedStatus: React.Dispatch<
+            React.SetStateAction<SelectedSectionStatus>
+        >;
+    },
+    ref: React.ForwardedRef<HTMLDialogElement>
+) {
+    const checkResponse = useCheckResponse({
+        mutate: mutate,
+        refModal: ref
+    });
+    if (action === undefined) return;
+    const { title, text } = StatusMessagesMap[action] || {};
+    if (title === undefined || text === undefined) return;
+
+    async function onConfirm() {
+        const updatedStatus = SectionStatusMap[action!];
+
+        const resp = await API.patch(`program-sections?id=${section.id}`, {
+            section_status: updatedStatus
+        });
+        if (resp.success) setSelectedStatus(updatedStatus);
+
+        checkResponse(
+            resp.success,
+            'Unable to update section',
+            'Section updated successfully'
+        );
+        return;
+    }
+    function close() {
+        closeModal(ref);
+    }
+    return (
+        <div onClick={(e) => e.stopPropagation()}>
+            <TextOnlyModal
+                ref={ref}
+                type={TextModalType.Confirm}
+                title={title}
+                text={text(section.section_status) ?? ''}
+                onSubmit={() => void onConfirm()}
+                onClose={close}
+            />
+        </div>
+    );
+});
+
+export default ModifySectionModal;
diff --git a/frontend/src/Components/modals/TextOnlyModal.tsx b/frontend/src/Components/modals/TextOnlyModal.tsx
index c0b26e3a7..9c5165933 100644
--- a/frontend/src/Components/modals/TextOnlyModal.tsx
+++ b/frontend/src/Components/modals/TextOnlyModal.tsx
@@ -5,7 +5,7 @@ import { TextModalType } from '.';
 interface TextModalProps {
     type: TextModalType;
     title: string;
-    text: string;
+    text: string | ReactNode;
     onSubmit: () => void;
     onClose: () => void;
 }
@@ -28,7 +28,7 @@ export const TextOnlyModal = forwardRef(function TextModal(
                     <span className={`text-3xl font-semibold text-neutral`}>
                         {title}
                     </span>
-                    <p>{text}</p>
+                    {typeof text === 'string' ? <p>{text}</p> : text}
                     {type === TextModalType.Information ? (
                         <>{children}</>
                     ) : (
@@ -36,6 +36,7 @@ export const TextOnlyModal = forwardRef(function TextModal(
                             type={type}
                             onCancel={onClose}
                             onSubmit={onSubmit}
+                            action={title}
                         />
                     )}
                 </div>
diff --git a/frontend/src/Pages/ProgramOverviewDashboard.tsx b/frontend/src/Pages/ProgramOverviewDashboard.tsx
index ed83177de..cc5d42448 100644
--- a/frontend/src/Pages/ProgramOverviewDashboard.tsx
+++ b/frontend/src/Pages/ProgramOverviewDashboard.tsx
@@ -1,16 +1,28 @@
 import { useParams } from 'react-router-dom';
-import { useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import StatsCard from '@/Components/StatsCard';
-import { PlusCircleIcon } from '@heroicons/react/24/outline';
+import { ArchiveBoxIcon, PlusCircleIcon } from '@heroicons/react/24/outline';
 import Pagination from '@/Components/Pagination';
 import SearchBar from '@/Components/inputs/SearchBar';
 import DropdownControl from '@/Components/inputs/DropdownControl';
-import { ProgramDashboard, ServerResponseMany } from '@/common';
+import {
+    Program,
+    Section,
+    SelectedSectionStatus,
+    ServerResponseOne,
+    ServerResponseMany
+} from '@/common';
 import { AxiosError } from 'axios';
 import Error from '@/Pages/Error';
 import ProgramOutcomes from '@/Components/ProgramOutcomes';
 import ProgressBar from '@/Components/ProgressBar';
 import useSWR from 'swr';
+import SectionStatus, { isArchived } from '@/Components/SectionStatus';
+import { showModal, TextModalType, TextOnlyModal } from '@/Components/modals';
+import ULIComponent from '@/Components/ULIComponent';
+import { XMarkIcon } from '@heroicons/react/24/solid';
+import API from '@/api/api';
+import { useCheckResponse } from '@/Hooks/useCheckResponse';
 import { useNavigate } from 'react-router-dom';
 
 export default function ProgramOverview() {
@@ -19,19 +31,34 @@ export default function ProgramOverview() {
     const [perPage, setPerPage] = useState(20);
     const [selectedSections, setSelectedSections] = useState<number[]>([]);
     const [searchTerm, setSearchTerm] = useState<string>('');
-    const [sortQuery, setSortQuery] = useState<string>('name_last asc');
-    const [includeActive, setIncludeActive] = useState(true);
+    const [sortQuery, setSortQuery] = useState<string>('ps.start_dt asc');
+    const [includeArchived, setIncludeArchived] = useState(false);
+    const archiveSectionsRef = useRef<HTMLDialogElement>(null);
+    const [unableToArchiveSections, setUnableToArchiveSections] = useState<
+        Section[]
+    >([]);
+    const [ableToArchiveSections, setAbleToArchiveSections] = useState<
+        Section[]
+    >([]);
     const navigate = useNavigate();
-    const [showAllSections, setShowAllSections] = useState(false);
+
+    const { data: programResp, error: programError } = useSWR<
+        ServerResponseOne<Program>,
+        AxiosError
+    >(`/api/programs/${id}`);
+    const program = programResp?.data;
+
     const {
-        data: programResp,
-        error: programError,
-        mutate
-    } = useSWR<ServerResponseMany<ProgramDashboard>, AxiosError>(
-        `/api/programs/${id}/overview?page=${page}&per_page=${perPage}`
+        data: sectionsResp,
+        error: sectionsError,
+        mutate: mutateSections
+    } = useSWR<ServerResponseMany<Section>, AxiosError>(
+        `/api/programs/${id}/sections?page=${page}&per_page=${perPage}&order_by=${sortQuery}`
     );
-    const program = programResp?.data[0];
-    const meta = program?.meta ?? {
+
+    const sections = sectionsResp?.data;
+
+    const meta = sectionsResp?.meta ?? {
         total: 0,
         per_page: 20,
         page: 1,
@@ -39,20 +66,49 @@ export default function ProgramOverview() {
         last_page: 1
     };
 
-    if (programError) {
-        <Error />;
+    const checkResponse = useCheckResponse({
+        mutate: mutateSections,
+        refModal: archiveSectionsRef
+    });
+
+    useEffect(() => {
+        if (sections === undefined) return;
+        const unableToArchive = sections.filter(
+            (section) =>
+                selectedSections.includes(section.id) &&
+                (section.section_status === SelectedSectionStatus.Active ||
+                    section.section_status ===
+                        SelectedSectionStatus.Scheduled) &&
+                section.enrolled >= 1
+        );
+
+        const ableToArchive = sections.filter(
+            (section) =>
+                selectedSections.includes(section.id) &&
+                !unableToArchive.includes(section)
+        );
+
+        setUnableToArchiveSections(unableToArchive);
+        setAbleToArchiveSections(ableToArchive);
+    }, [selectedSections, sections]);
+
+    if (programError || sectionsError || sections === undefined) {
+        return <Error />;
     }
 
-    const allSelected = selectedSections
-        ? selectedSections.length === program?.section_details.length &&
-          program?.section_details.length > 0
-        : false;
+    const nonArchivedSections = sections.filter(
+        (section) => !isArchived(section)
+    );
+
+    const allSelected =
+        selectedSections.length === nonArchivedSections.length &&
+        nonArchivedSections.length > 0;
+
+    const filteredSections = includeArchived ? sections : nonArchivedSections;
 
     function handleToggleAll(checked: boolean) {
         if (checked) {
-            setSelectedSections(
-                program?.section_details.map((s) => s.id) ?? []
-            );
+            setSelectedSections(nonArchivedSections.map((s) => s.id));
         } else {
             setSelectedSections([]);
         }
@@ -74,8 +130,6 @@ export default function ProgramOverview() {
     const handleSetPerPage = (val: number) => {
         setPerPage(val);
         setPage(1);
-        void mutate();
-        setIncludeActive(includeActive); // Same deal just to get eslint to be quiet
         setSortQuery(sortQuery); //Just to get eslint to stop complaining remove when this is is built out
     };
 
@@ -86,6 +140,25 @@ export default function ProgramOverview() {
             );
         }
     };
+    function confirmArchiveSections() {
+        showModal(archiveSectionsRef);
+    }
+
+    async function archiveSection() {
+        const selectedAbleToArchiveSections = selectedSections.filter((id) =>
+            ableToArchiveSections.some((section) => section.id === id)
+        );
+        const idString = '?id=' + selectedAbleToArchiveSections.join('&id=');
+        const resp = await API.patch(`program-sections${idString}`, {
+            archived_at: new Date().toISOString()
+        });
+        checkResponse(
+            resp.success,
+            'Unable to update section',
+            'Section updated successfully'
+        );
+        if (resp.success) setSelectedSections([]);
+    }
 
     return (
         <div className="p-4 px-5">
@@ -125,28 +198,25 @@ export default function ProgramOverview() {
                         label="order by"
                         setState={setSortQuery}
                         enumType={{
-                            'Facility (A-Z)': 'facility_name asc',
-                            'Facility (Z-A)': 'facility_name desc',
-                            'Enrollment Count (Most)': 'enrollment desc',
-                            'Enrollment Count (Least)': 'enrollment asc',
-                            'Start Date (Earliest)': 'start_date asc',
-                            'Start Date (Latest)': 'start_date desc'
+                            'Enrollment Count (Most)': 'enrolled desc',
+                            'Enrollment Count (Least)': 'enrolled asc',
+                            'Start Date (Earliest)': 'ps.start_dt asc',
+                            'Start Date (Latest)': 'ps.start_dt desc'
                         }}
                     />
                     <div className="flex items-center">
                         <input
                             type="checkbox"
                             className="checkbox checkbox-sm mr-2"
-                            checked={showAllSections}
                             onChange={(e) => {
-                                setShowAllSections(e.target.checked);
-                                setIncludeActive(!e.target.checked);
+                                setIncludeArchived(e.target.checked);
                             }}
                         />
-                        <span className="text-sm">Show All Classes</span>
+                        <span className="text-sm">
+                            Include Archived Classes
+                        </span>
                     </div>
                 </div>
-
                 <div className="flex flex-row gap-x-2">
                     {selectedSections.length === 1 && (
                         <button
@@ -158,17 +228,30 @@ export default function ProgramOverview() {
                             Enroll Students
                         </button>
                     )}
-                    <button
-                        className="button flex items-center"
-                        onClick={() =>
-                            navigate(`/programs/${id}/class`, {
-                                state: { title: `Program: ${program?.name}` }
-                            })
-                        }
-                    >
-                        <PlusCircleIcon className="w-4 h-4 mr-1" />
-                        Add Class
-                    </button>
+                    {selectedSections.length > 0 ? (
+                        <button
+                            className="button flex items-center bg-pale-yellow border border-dark-yellow text-body-text"
+                            onClick={confirmArchiveSections}
+                        >
+                            <ArchiveBoxIcon className="w-4 h-4 mr-1" />
+                            Archive Class
+                            {selectedSections.length > 1 ? 'es' : ''}
+                        </button>
+                    ) : (
+                        <button
+                            className="button flex items-center"
+                            onClick={() =>
+                                navigate(`/programs/${id}/class`, {
+                                    state: {
+                                        title: `Program: ${program?.name}`
+                                    }
+                                })
+                            }
+                        >
+                            <PlusCircleIcon className="w-4 h-4 mr-1" />
+                            Add Class
+                        </button>
+                    )}
                 </div>
             </div>
 
@@ -188,34 +271,48 @@ export default function ProgramOverview() {
                                 />
                             </th>
                             <th>Facility Name</th>
-                            <th>Facilitator</th>
+                            <th>Instructor Name</th>
                             <th>Start Date</th>
                             <th>End Date</th>
-                            <th>Enrollment</th>
+                            <th className="w-[200px]">Enrollment</th>
+                            <th className="w-[150px]">Status</th>
                         </tr>
                     </thead>
                     <tbody>
-                        {program?.section_details.map((section) => {
+                        {filteredSections.map((section) => {
                             const isSelected = selectedSections.includes(
                                 section.id
                             );
                             return (
                                 <tr
                                     key={section.id}
-                                    onClick={() => handleToggleRow(section.id)}
-                                    className={`cursor-pointer ${
-                                        isSelected ? 'bg-background ' : ''
-                                    }`}
+                                    onClick={() => {
+                                        if (!isArchived(section))
+                                            handleToggleRow(section.id);
+                                    }}
+                                    className={
+                                        isArchived(section)
+                                            ? 'bg-grey-1 cursor-not-allowed'
+                                            : `cursor-pointer ${
+                                                  isSelected
+                                                      ? 'bg-background '
+                                                      : ''
+                                              }`
+                                    }
                                 >
                                     <td onClick={(e) => e.stopPropagation()}>
-                                        <input
-                                            type="checkbox"
-                                            className="checkbox checkbox-sm"
-                                            checked={isSelected}
-                                            onChange={() =>
-                                                handleToggleRow(section.id)
-                                            }
-                                        />
+                                        {isArchived(section) ? (
+                                            <div></div>
+                                        ) : (
+                                            <input
+                                                type="checkbox"
+                                                className="checkbox checkbox-sm"
+                                                checked={isSelected}
+                                                onChange={() =>
+                                                    handleToggleRow(section.id)
+                                                }
+                                            />
+                                        )}
                                     </td>
                                     <td>{section.facility_name}</td>
                                     <td>{section.instructor_name}</td>
@@ -255,6 +352,13 @@ export default function ProgramOverview() {
                                             denominator={section.capacity}
                                         />
                                     </td>
+                                    <td>
+                                        <SectionStatus
+                                            status={section.section_status}
+                                            section={section}
+                                            mutateSections={mutateSections}
+                                        />
+                                    </td>
                                 </tr>
                             );
                         })}
@@ -271,6 +375,64 @@ export default function ProgramOverview() {
                     </div>
                 )}
             </div>
+
+            <TextOnlyModal
+                ref={archiveSectionsRef}
+                type={
+                    ableToArchiveSections.length == 0
+                        ? TextModalType.Information
+                        : TextModalType.Confirm
+                }
+                title={`Archive Class${selectedSections.length > 1 ? 'es' : ''}`}
+                text={
+                    <div>
+                        {unableToArchiveSections.length > 0 && (
+                            <>
+                                <div className="text-error">
+                                    <p className="text-error font-bold">
+                                        We are unable to archive the following
+                                        classes due to their active or scheduled
+                                        status with enrolled students:
+                                    </p>
+                                    <ul className="py-2">
+                                        {unableToArchiveSections.map(
+                                            (section) => (
+                                                <li
+                                                    key={section.id}
+                                                    className="flex items-center gap-2"
+                                                >
+                                                    <ULIComponent
+                                                        icon={XMarkIcon}
+                                                    />
+                                                    {section.facility_name}
+                                                </li>
+                                            )
+                                        )}
+                                    </ul>
+                                </div>
+                                <br />
+                            </>
+                        )}
+                        {ableToArchiveSections.length != 0 && (
+                            <>
+                                <p>
+                                    Archive these classes at the following
+                                    facilities?
+                                </p>
+                                <ul className="list-disc list-inside py-2">
+                                    {ableToArchiveSections.map((section) => (
+                                        <li key={section.id}>
+                                            {section.facility_name}
+                                        </li>
+                                    ))}
+                                </ul>
+                            </>
+                        )}
+                    </div>
+                }
+                onSubmit={() => void archiveSection()}
+                onClose={() => console.log('close archive section')}
+            ></TextOnlyModal>
         </div>
     );
 }
diff --git a/frontend/src/common.ts b/frontend/src/common.ts
index a70959b75..255f343df 100644
--- a/frontend/src/common.ts
+++ b/frontend/src/common.ts
@@ -665,21 +665,44 @@ export interface Program {
     facilities: Facility[];
 }
 
-export interface ProgramSectionDetail {
+export interface Section {
     id: number;
-    instructor_name: string;
+    program_id: number;
+    facility_id: number;
     facility_name: string;
-    start_dt: Date;
-    end_dt: Date;
-    capacity: number;
+    instructor_name: string;
+    start_dt: string;
+    end_dt: string;
+    section_status: SelectedSectionStatus;
     enrolled: number;
+    capacity: number;
+    archived_at: string | null;
 }
 
-export interface ProgramDashboard extends Program {
-    section_details: ProgramSectionDetail[];
-    meta: PaginationMeta;
+export enum SelectedSectionStatus {
+    Scheduled = 'Scheduled',
+    Active = 'Active',
+    Paused = 'Paused',
+    Completed = 'Completed',
+    Canceled = 'Cancelled'
+}
+
+export enum SectionStatusOptions {
+    Complete = 'Complete',
+    Pause = 'Pause',
+    Cancel = 'Cancel',
+    Schedule = 'Schedule',
+    Active = 'Active'
 }
 
+export const SectionStatusMap = {
+    [SectionStatusOptions.Complete]: SelectedSectionStatus.Completed,
+    [SectionStatusOptions.Pause]: SelectedSectionStatus.Paused,
+    [SectionStatusOptions.Cancel]: SelectedSectionStatus.Canceled,
+    [SectionStatusOptions.Schedule]: SelectedSectionStatus.Scheduled,
+    [SectionStatusOptions.Active]: SelectedSectionStatus.Active
+};
+
 export interface ProgramTag {
     id: string;
     value: number;


Reviews:
user: login: CK-7vn
body: I noticed that, if i flip a section to active, it still allows me to archive it. If i then opt to display archived sections, it reappears, and although it's grayed out, still shows "active". Not entirely sure why it's happening, I haven't looked over all of the code yet, but it seems to be quite the opposite of what's happening for you at least from the screenshot up above, weird. 
![image](https://github.com/user-attachments/assets/06b2c6a6-a65a-4c49-840f-503ea86b7485)
![image](https://github.com/user-attachments/assets/e299ccd8-2a6b-485d-a5fb-4ced366ff3a3)
![image](https://github.com/user-attachments/assets/4ed7534e-c470-4629-affb-70bb44776f1e)


state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T17:19:13Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T20:47:01Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T20:51:08Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T20:52:01Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T20:57:39Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T20:58:16Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T21:12:01Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T21:14:04Z
user: login: jtucholski
body: Only have the one comment I included about the conditional check. Looks great!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T21:17:32Z
user: login: jtucholski
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T21:17:39Z
user: login: PThorpe92
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T15:12:03Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:32:48Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:33:36Z
user: login: PThorpe92
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-31T19:03:55Z
user: login: PThorpe92
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-31T19:23:46Z

Review comments:
diff_hunk: @@ -83,16 +83,19 @@ func (srv *Server) handleDeleteSection(w http.ResponseWriter, r *http.Request, l
 }
 
 func (srv *Server) handleUpdateSection(w http.ResponseWriter, r *http.Request, log sLog) error {
-	id, err := strconv.Atoi(r.PathValue("id"))
-	if err != nil {
-		return newInvalidIdServiceError(err, "section ID")
+	ids := r.URL.Query()["id"]
+	sectionIDs := make([]int, 0, len(ids))
+	for _, id := range ids {
+		if sectionID, err := strconv.Atoi(id); err == nil {
+			sectionIDs = append(sectionIDs, sectionID)
+		}
 	}
 	defer r.Body.Close()
 	section := models.ProgramSection{}
 	if err := json.NewDecoder(r.Body).Decode(&section); err != nil {
 		return newJSONReqBodyServiceError(err)
 	}
-	updated, err := srv.Db.UpdateProgramSection(&section, id)
+	updated, err := srv.Db.UpdateProgramSection(&section, sectionIDs)
 	if err != nil {
 		return newDatabaseServiceError(err)
 	}
path: backend/src/handlers/sections_handler.go
user: login: jtucholski
body: Confirming that this is intended to be a batch endpoint, right? Like we supply a single section object in the body with a list of IDs and it's intended to update that specific field for all records with that ID? 
created_at: 2025-03-26T20:47:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,19 +6,25 @@ import { DeleteButton } from './DeleteButton';
 export function CancelSubmitRow({
     type,
     onCancel,
-    onSubmit
+    onSubmit,
+    action
 }: {
     type: TextModalType;
     onCancel: () => void;
     onSubmit: () => void;
+    action?: string;
 }) {
+    if (type === TextModalType.Confirm && action === undefined) {
+        console.error('must include action type for modal');
+        return;
+    }
path: frontend/src/Components/inputs/CancelSubmitRow.tsx
user: login: jtucholski
body: Did we come across needing this? Is there a different way that we should enforce this rather than at runtime?
created_at: 2025-03-26T20:51:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,19 +6,25 @@ import { DeleteButton } from './DeleteButton';
 export function CancelSubmitRow({
     type,
     onCancel,
-    onSubmit
+    onSubmit,
+    action
 }: {
     type: TextModalType;
     onCancel: () => void;
     onSubmit: () => void;
+    action?: string;
 }) {
+    if (type === TextModalType.Confirm && action === undefined) {
+        console.error('must include action type for modal');
+        return;
+    }
path: frontend/src/Components/inputs/CancelSubmitRow.tsx
user: login: jtucholski
body: Should we default the value to Confirm if it isn't provided?
created_at: 2025-03-26T20:51:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,16 +83,19 @@ func (srv *Server) handleDeleteSection(w http.ResponseWriter, r *http.Request, l
 }
 
 func (srv *Server) handleUpdateSection(w http.ResponseWriter, r *http.Request, log sLog) error {
-	id, err := strconv.Atoi(r.PathValue("id"))
-	if err != nil {
-		return newInvalidIdServiceError(err, "section ID")
+	ids := r.URL.Query()["id"]
+	sectionIDs := make([]int, 0, len(ids))
+	for _, id := range ids {
+		if sectionID, err := strconv.Atoi(id); err == nil {
+			sectionIDs = append(sectionIDs, sectionID)
+		}
 	}
 	defer r.Body.Close()
 	section := models.ProgramSection{}
 	if err := json.NewDecoder(r.Body).Decode(&section); err != nil {
 		return newJSONReqBodyServiceError(err)
 	}
-	updated, err := srv.Db.UpdateProgramSection(&section, id)
+	updated, err := srv.Db.UpdateProgramSection(&section, sectionIDs)
 	if err != nil {
 		return newDatabaseServiceError(err)
 	}
path: backend/src/handlers/sections_handler.go
user: login: calisio
body: The sections that are being updated are passed in through the query with the ids. What is being passed in through the body is the new value of the column we would like to update. So, for example, for archiving, we can pass in multiple id's, and the value in the body is "archived_at" with the current time, which is what will be updated in the table, and it will update all the rows with those ids with that value of archived_at. We could also just pass in one id, and several values in the body that we would want to update (ie pass in ?id=1 and in the body instructor="Josh" and status="Active"). So, I think the answer to your question is yes.
created_at: 2025-03-26T20:57:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,19 +6,25 @@ import { DeleteButton } from './DeleteButton';
 export function CancelSubmitRow({
     type,
     onCancel,
-    onSubmit
+    onSubmit,
+    action
 }: {
     type: TextModalType;
     onCancel: () => void;
     onSubmit: () => void;
+    action?: string;
 }) {
+    if (type === TextModalType.Confirm && action === undefined) {
+        console.error('must include action type for modal');
+        return;
+    }
path: frontend/src/Components/inputs/CancelSubmitRow.tsx
user: login: calisio
body: Oh, yes that would probably be smart! I can default it to 'Confirm'
created_at: 2025-03-26T20:58:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,16 +83,19 @@ func (srv *Server) handleDeleteSection(w http.ResponseWriter, r *http.Request, l
 }
 
 func (srv *Server) handleUpdateSection(w http.ResponseWriter, r *http.Request, log sLog) error {
-	id, err := strconv.Atoi(r.PathValue("id"))
-	if err != nil {
-		return newInvalidIdServiceError(err, "section ID")
+	ids := r.URL.Query()["id"]
+	sectionIDs := make([]int, 0, len(ids))
+	for _, id := range ids {
+		if sectionID, err := strconv.Atoi(id); err == nil {
+			sectionIDs = append(sectionIDs, sectionID)
+		}
 	}
 	defer r.Body.Close()
 	section := models.ProgramSection{}
 	if err := json.NewDecoder(r.Body).Decode(&section); err != nil {
 		return newJSONReqBodyServiceError(err)
 	}
-	updated, err := srv.Db.UpdateProgramSection(&section, id)
+	updated, err := srv.Db.UpdateProgramSection(&section, sectionIDs)
 	if err != nil {
 		return newDatabaseServiceError(err)
 	}
path: backend/src/handlers/sections_handler.go
user: login: jtucholski
body: Awesome!
created_at: 2025-03-26T21:11:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,33 +31,82 @@ export default function ProgramOverview() {
     const [selectedSections, setSelectedSections] = useState<number[]>([]);
     const [searchTerm, setSearchTerm] = useState<string>('');
     const [sortQuery, setSortQuery] = useState<string>('name_last asc');
-    const [includeActive, setIncludeActive] = useState(true);
+    const [includeArchived, setIncludeArchived] = useState(false);
+    const archiveSectionsRef = useRef<HTMLDialogElement>(null);
+    const [unableToArchiveSections, setUnableToArchiveSections] = useState<
+        Section[]
+    >([]);
+    const [ableToArchiveSections, setAbleToArchiveSections] = useState<
+        Section[]
+    >([]);
 
-    const [showAllSections, setShowAllSections] = useState(false);
     const { data: programResp, error: programError } = useSWR<
         ServerResponseOne<Program>,
         AxiosError
     >(`/api/programs/${id}`);
     const program = programResp?.data;
 
+    const {
+        data: sectionsResp,
+        error: sectionsError,
+        mutate: mutateSections
+    } = useSWR<ServerResponseMany<Section>, AxiosError>(
+        `/api/programs/${id}/sections`
+    );
+
+    const sections = sectionsResp?.data;
+
+    const checkResponse = useCheckResponse({
+        mutate: mutateSections,
+        refModal: archiveSectionsRef
+    });
+
+    useEffect(() => {
+        if (sections === undefined) return;
+        const unableToArchive = sections.filter(
+            (section) =>
+                selectedSections.includes(section.id) &&
+                (section.status === SelectedSectionStatus.Active ||
+                    section.status === SelectedSectionStatus.Scheduled) &&
+                section.enrolled_residents > 1
path: frontend/src/Pages/ProgramOverviewDashboard.tsx
user: login: jtucholski
body: Should this be >= 1? You're looking to make sure there is at least one resident enrolled, right?
created_at: 2025-03-26T21:14:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,33 +31,82 @@ export default function ProgramOverview() {
     const [selectedSections, setSelectedSections] = useState<number[]>([]);
     const [searchTerm, setSearchTerm] = useState<string>('');
     const [sortQuery, setSortQuery] = useState<string>('name_last asc');
-    const [includeActive, setIncludeActive] = useState(true);
+    const [includeArchived, setIncludeArchived] = useState(false);
+    const archiveSectionsRef = useRef<HTMLDialogElement>(null);
+    const [unableToArchiveSections, setUnableToArchiveSections] = useState<
+        Section[]
+    >([]);
+    const [ableToArchiveSections, setAbleToArchiveSections] = useState<
+        Section[]
+    >([]);
 
-    const [showAllSections, setShowAllSections] = useState(false);
     const { data: programResp, error: programError } = useSWR<
         ServerResponseOne<Program>,
         AxiosError
     >(`/api/programs/${id}`);
     const program = programResp?.data;
 
+    const {
+        data: sectionsResp,
+        error: sectionsError,
+        mutate: mutateSections
+    } = useSWR<ServerResponseMany<Section>, AxiosError>(
+        `/api/programs/${id}/sections`
+    );
+
+    const sections = sectionsResp?.data;
+
+    const checkResponse = useCheckResponse({
+        mutate: mutateSections,
+        refModal: archiveSectionsRef
+    });
+
+    useEffect(() => {
+        if (sections === undefined) return;
+        const unableToArchive = sections.filter(
+            (section) =>
+                selectedSections.includes(section.id) &&
+                (section.status === SelectedSectionStatus.Active ||
+                    section.status === SelectedSectionStatus.Scheduled) &&
+                section.enrolled_residents > 1
+        );
+
+        const ableToArchive = sections.filter(
+            (section) =>
+                selectedSections.includes(section.id) &&
+                !unableToArchive.includes(section)
+        );
+
+        setUnableToArchiveSections(unableToArchive);
+        setAbleToArchiveSections(ableToArchive);
+    }, [selectedSections]);
+
+    if (programError || sectionsError || sections === undefined) {
+        return <Error />;
+    }
+
     const meta = {
         current_page: page,
         per_page: perPage,
-        total: sections.data.length,
-        last_page: Math.ceil(sections.data.length / perPage)
+        total: sections.length,
+        last_page: Math.ceil(sections.length / perPage)
     };
 
-    if (programError) {
-        <Error />;
-    }
+    const nonArchivedSections = sections.filter(
+        (section) =>
+            section.archived_at === null ||
+            section.archived_at === '0001-01-01T00:00:00Z'
path: frontend/src/Pages/ProgramOverviewDashboard.tsx
user: login: PThorpe92
body: did we end up changing the type to `*time.Time` so we don't have to deal with these? I'm not saying this should be taken out either way. 

I think we should add a helper somewhere in the frontend that takes a time string and returns either the formatted Time, or null for the case of ` "" | '0001-01-01T00:00:00Z`  
created_at: 2025-03-27T15:12:03Z
author_association: MEMBER
diff_hunk: @@ -63,33 +31,82 @@ export default function ProgramOverview() {
     const [selectedSections, setSelectedSections] = useState<number[]>([]);
     const [searchTerm, setSearchTerm] = useState<string>('');
     const [sortQuery, setSortQuery] = useState<string>('name_last asc');
-    const [includeActive, setIncludeActive] = useState(true);
+    const [includeArchived, setIncludeArchived] = useState(false);
+    const archiveSectionsRef = useRef<HTMLDialogElement>(null);
+    const [unableToArchiveSections, setUnableToArchiveSections] = useState<
+        Section[]
+    >([]);
+    const [ableToArchiveSections, setAbleToArchiveSections] = useState<
+        Section[]
+    >([]);
 
-    const [showAllSections, setShowAllSections] = useState(false);
     const { data: programResp, error: programError } = useSWR<
         ServerResponseOne<Program>,
         AxiosError
     >(`/api/programs/${id}`);
     const program = programResp?.data;
 
+    const {
+        data: sectionsResp,
+        error: sectionsError,
+        mutate: mutateSections
+    } = useSWR<ServerResponseMany<Section>, AxiosError>(
+        `/api/programs/${id}/sections`
+    );
+
+    const sections = sectionsResp?.data;
+
+    const checkResponse = useCheckResponse({
+        mutate: mutateSections,
+        refModal: archiveSectionsRef
+    });
+
+    useEffect(() => {
+        if (sections === undefined) return;
+        const unableToArchive = sections.filter(
+            (section) =>
+                selectedSections.includes(section.id) &&
+                (section.status === SelectedSectionStatus.Active ||
+                    section.status === SelectedSectionStatus.Scheduled) &&
+                section.enrolled_residents > 1
+        );
+
+        const ableToArchive = sections.filter(
+            (section) =>
+                selectedSections.includes(section.id) &&
+                !unableToArchive.includes(section)
+        );
+
+        setUnableToArchiveSections(unableToArchive);
+        setAbleToArchiveSections(ableToArchive);
+    }, [selectedSections]);
+
+    if (programError || sectionsError || sections === undefined) {
+        return <Error />;
+    }
+
     const meta = {
         current_page: page,
         per_page: perPage,
-        total: sections.data.length,
-        last_page: Math.ceil(sections.data.length / perPage)
+        total: sections.length,
+        last_page: Math.ceil(sections.length / perPage)
     };
 
-    if (programError) {
-        <Error />;
-    }
+    const nonArchivedSections = sections.filter(
+        (section) =>
+            section.archived_at === null ||
+            section.archived_at === '0001-01-01T00:00:00Z'
path: frontend/src/Pages/ProgramOverviewDashboard.tsx
user: login: calisio
body: I am changing it today, was just working on other bugs for Welcome Tour first (different PR)
created_at: 2025-03-27T15:32:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,33 +31,82 @@ export default function ProgramOverview() {
     const [selectedSections, setSelectedSections] = useState<number[]>([]);
     const [searchTerm, setSearchTerm] = useState<string>('');
     const [sortQuery, setSortQuery] = useState<string>('name_last asc');
-    const [includeActive, setIncludeActive] = useState(true);
+    const [includeArchived, setIncludeArchived] = useState(false);
+    const archiveSectionsRef = useRef<HTMLDialogElement>(null);
+    const [unableToArchiveSections, setUnableToArchiveSections] = useState<
+        Section[]
+    >([]);
+    const [ableToArchiveSections, setAbleToArchiveSections] = useState<
+        Section[]
+    >([]);
 
-    const [showAllSections, setShowAllSections] = useState(false);
     const { data: programResp, error: programError } = useSWR<
         ServerResponseOne<Program>,
         AxiosError
     >(`/api/programs/${id}`);
     const program = programResp?.data;
 
+    const {
+        data: sectionsResp,
+        error: sectionsError,
+        mutate: mutateSections
+    } = useSWR<ServerResponseMany<Section>, AxiosError>(
+        `/api/programs/${id}/sections`
+    );
+
+    const sections = sectionsResp?.data;
+
+    const checkResponse = useCheckResponse({
+        mutate: mutateSections,
+        refModal: archiveSectionsRef
+    });
+
+    useEffect(() => {
+        if (sections === undefined) return;
+        const unableToArchive = sections.filter(
+            (section) =>
+                selectedSections.includes(section.id) &&
+                (section.status === SelectedSectionStatus.Active ||
+                    section.status === SelectedSectionStatus.Scheduled) &&
+                section.enrolled_residents > 1
+        );
+
+        const ableToArchive = sections.filter(
+            (section) =>
+                selectedSections.includes(section.id) &&
+                !unableToArchive.includes(section)
+        );
+
+        setUnableToArchiveSections(unableToArchive);
+        setAbleToArchiveSections(ableToArchive);
+    }, [selectedSections]);
+
+    if (programError || sectionsError || sections === undefined) {
+        return <Error />;
+    }
+
     const meta = {
         current_page: page,
         per_page: perPage,
-        total: sections.data.length,
-        last_page: Math.ceil(sections.data.length / perPage)
+        total: sections.length,
+        last_page: Math.ceil(sections.length / perPage)
     };
 
-    if (programError) {
-        <Error />;
-    }
+    const nonArchivedSections = sections.filter(
+        (section) =>
+            section.archived_at === null ||
+            section.archived_at === '0001-01-01T00:00:00Z'
path: frontend/src/Pages/ProgramOverviewDashboard.tsx
user: login: calisio
body: > After talking to Preston, the following changes will be made:
> 
> * archive_at is just changed to a pointer so that way it will be null on create (no migration update needed)
> * end_dt is being removed from the UI
> * cancelled is spelled with two Ls everywhere else, so we will leave it as is for now
> * will remove all traces of delete api including those in the handler


Implementing what I commented above today
created_at: 2025-03-27T15:33:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,33 +31,82 @@ export default function ProgramOverview() {
     const [selectedSections, setSelectedSections] = useState<number[]>([]);
     const [searchTerm, setSearchTerm] = useState<string>('');
     const [sortQuery, setSortQuery] = useState<string>('name_last asc');
-    const [includeActive, setIncludeActive] = useState(true);
+    const [includeArchived, setIncludeArchived] = useState(false);
+    const archiveSectionsRef = useRef<HTMLDialogElement>(null);
+    const [unableToArchiveSections, setUnableToArchiveSections] = useState<
+        Section[]
+    >([]);
+    const [ableToArchiveSections, setAbleToArchiveSections] = useState<
+        Section[]
+    >([]);
 
-    const [showAllSections, setShowAllSections] = useState(false);
     const { data: programResp, error: programError } = useSWR<
         ServerResponseOne<Program>,
         AxiosError
     >(`/api/programs/${id}`);
     const program = programResp?.data;
 
+    const {
+        data: sectionsResp,
+        error: sectionsError,
+        mutate: mutateSections
+    } = useSWR<ServerResponseMany<Section>, AxiosError>(
+        `/api/programs/${id}/sections`
+    );
+
+    const sections = sectionsResp?.data;
+
+    const checkResponse = useCheckResponse({
+        mutate: mutateSections,
+        refModal: archiveSectionsRef
+    });
+
+    useEffect(() => {
+        if (sections === undefined) return;
+        const unableToArchive = sections.filter(
+            (section) =>
+                selectedSections.includes(section.id) &&
+                (section.status === SelectedSectionStatus.Active ||
+                    section.status === SelectedSectionStatus.Scheduled) &&
+                section.enrolled_residents > 1
+        );
+
+        const ableToArchive = sections.filter(
+            (section) =>
+                selectedSections.includes(section.id) &&
+                !unableToArchive.includes(section)
+        );
+
+        setUnableToArchiveSections(unableToArchive);
+        setAbleToArchiveSections(ableToArchive);
+    }, [selectedSections]);
+
+    if (programError || sectionsError || sections === undefined) {
+        return <Error />;
+    }
+
     const meta = {
         current_page: page,
         per_page: perPage,
-        total: sections.data.length,
-        last_page: Math.ceil(sections.data.length / perPage)
+        total: sections.length,
+        last_page: Math.ceil(sections.length / perPage)
     };
 
-    if (programError) {
-        <Error />;
-    }
+    const nonArchivedSections = sections.filter(
+        (section) =>
+            section.archived_at === null ||
+            section.archived_at === '0001-01-01T00:00:00Z'
path: frontend/src/Pages/ProgramOverviewDashboard.tsx
user: login: PThorpe92
body: :man_facepalming: my fault i missed that
created_at: 2025-03-31T19:03:55Z
author_association: MEMBER

PR comments:
user: login: calisio
created_at: 2025-03-25T19:25:55Z
body: After talking to Preston, the following changes will be made:
- archive_at is just changed to a pointer so that way it will be null on create (no migration update needed)
- end_dt is being removed from the UI 
- cancelled is spelled with two Ls everywhere else, so we will leave it as is for now
- will remove all traces of delete api including those in the handler
author_association: CONTRIBUTOR
user: login: calisio
created_at: 2025-03-26T17:59:50Z
body: @CK-7vn Yes, so actually you are allowed to archive a section if there are no students enrolled. Because I am waiting on Cory's PR on enrollment, enrollment right now is set to zero. Therefore, the section CAN be archived, because although it is active, it has zero enrollment, so the action is allowed. Does this answer your question/solve the bug? I might be missing something, so if I am, let me know!
author_association: CONTRIBUTOR
user: login: CK-7vn
created_at: 2025-03-26T18:06:18Z
body: > @CK-7vn Yes, so actually you are allowed to archive a section if there are no students enrolled. Because I am waiting on Cory's PR on enrollment, enrollment right now is set to zero. Therefore, the section CAN be archived, because although it is active, it has zero enrollment, so the action is allowed. Does this answer your question/solve the bug? I might be missing something, so if I am, let me know!

Ahh, alright, alright, yes that does help, for sure. I would assume that once archived there's no going back in the sense of unarchiving? cool, that's good news then! Thank you for the clarification! 
author_association: MEMBER
user: login: calisio
created_at: 2025-03-27T21:29:21Z
body: @PThorpe92 should have the updated APIs now & other changes fixed (added helper func for archived)
author_association: CONTRIBUTOR
user: login: calisio
created_at: 2025-03-28T19:37:05Z
body: Before this gets merged, @jtucholski I had a question. It seems like we are only showing the sections for the selected facility (ie we are only showing the sections for Potosi Correctional, and then if we switch to MVCF, it shows only the sections for MVCF). That being said, do we think it is worth it to display the facility name in EACH row? It seems a bit redundant to me.
author_association: CONTRIBUTOR
user: login: jtucholski
created_at: 2025-03-28T19:39:31Z
body: IMO, I don't think it's worth it, but in this case we designed it similar to the Figma. I think we can move forward with it for now. Lisa is considering some options as to what we display on the Program Overview page when viewed as a Dept Admin and a Facility Admin.
author_association: CONTRIBUTOR
user: login: calisio
created_at: 2025-03-28T19:53:29Z
body: Sounds good, will leave it as is for now
author_association: CONTRIBUTOR
user: login: jtucholski
created_at: 2025-03-31T11:11:44Z
body: @PThorpe92 Can you give a second review of the changes?
author_association: CONTRIBUTOR