Implement time off management (Issue #3)
All checks were successful
CI / Go tests & lint (push) Successful in 10s
CI / Frontend tests & type-check (push) Successful in 41s
CI / Go tests & lint (pull_request) Successful in 8s
CI / Frontend tests & type-check (pull_request) Successful in 46s

Add full time-off lifecycle: create/edit/delete with shift conflict
detection, auto-removal from conflicting shifts with admin notification,
shift restoration on admin delete, and hard block on assigning volunteers
with approved time off to shifts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 10:03:47 -03:00
parent bb2c2cbc52
commit 6427595c62
11 changed files with 1500 additions and 57 deletions

View File

@@ -200,7 +200,7 @@ func do(t *testing.T, router http.Handler, method, path, body, token string) *ht
func TestListTemplates_ReturnsEmpty(t *testing.T) {
store := &fakeStore{}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 1, "admin")
@@ -219,7 +219,7 @@ func TestListTemplates_ReturnsEmpty(t *testing.T) {
func TestCreateTemplate_AdminOnly(t *testing.T) {
store := &fakeStore{}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 2, "volunteer")
@@ -235,7 +235,7 @@ func TestCreateTemplate_AdminOnly(t *testing.T) {
func TestCreateTemplate_Success(t *testing.T) {
store := &fakeStore{}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 1, "admin")
@@ -256,7 +256,7 @@ func TestCreateTemplate_Success(t *testing.T) {
func TestCreateTemplate_MissingFields(t *testing.T) {
store := &fakeStore{}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 1, "admin")
@@ -271,7 +271,7 @@ func TestCreateTemplate_MissingFields(t *testing.T) {
func TestDeleteTemplate_AdminOnly(t *testing.T) {
store := &fakeStore{}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 2, "volunteer")
@@ -285,7 +285,7 @@ func TestDeleteTemplate_AdminOnly(t *testing.T) {
func TestDeleteTemplate_Success(t *testing.T) {
store := &fakeStore{}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 1, "admin")
@@ -303,7 +303,7 @@ func TestDeleteTemplate_Success(t *testing.T) {
func TestGenerateInstances_AdminOnly(t *testing.T) {
store := &fakeStore{}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 2, "volunteer")
@@ -318,7 +318,7 @@ func TestGenerateInstances_AdminOnly(t *testing.T) {
func TestGenerateInstances_AlreadyExists(t *testing.T) {
store := &fakeStore{genErr: schedule.ErrAlreadyExists}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 1, "admin")
@@ -337,7 +337,7 @@ func TestGenerateInstances_Success(t *testing.T) {
},
}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 1, "admin")
@@ -362,7 +362,7 @@ func TestPublishMonth_SendsNotifications(t *testing.T) {
},
}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 1, "admin")
@@ -380,7 +380,7 @@ func TestPublishMonth_SendsNotifications(t *testing.T) {
func TestUnpublishMonth_SendsNotifications(t *testing.T) {
store := &fakeStore{unpubResult: []int64{10, 20}}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 1, "admin")
@@ -398,7 +398,7 @@ func TestUnpublishMonth_SendsNotifications(t *testing.T) {
func TestUpdateInstance_AdminOnly(t *testing.T) {
store := &fakeStore{}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 2, "volunteer")
@@ -424,7 +424,7 @@ func TestUpdateInstance_PublishedResetsAndNotifies(t *testing.T) {
addedVols: []int64{6}, // Bob is newly added
}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 1, "admin")
@@ -446,7 +446,7 @@ func TestUpdateInstance_PublishedResetsAndNotifies(t *testing.T) {
func TestConfirmShift_NotAssigned(t *testing.T) {
store := &fakeStore{confirmErr: schedule.ErrNotFound}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 5, "volunteer")
@@ -460,7 +460,7 @@ func TestConfirmShift_NotAssigned(t *testing.T) {
func TestConfirmShift_Success(t *testing.T) {
store := &fakeStore{}
notifier := &fakeNotifier{}
h := schedule.NewHandlerFromInterfaces(store, notifier)
h := schedule.NewHandlerFromInterfaces(store, notifier, nil)
router := newRouter(h)
token := jwtForRole(t, 5, "volunteer")