feat(phase5): scheduling/availability - DB, API, UI in server settings
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type availabilitySlot struct {
|
||||
DayOfWeek string `json:"day_of_week"`
|
||||
StartTime string `json:"start_time"` // HH:MM
|
||||
EndTime string `json:"end_time"` // HH:MM
|
||||
}
|
||||
|
||||
func (h *Handler) GetMyAvailability(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
serverID := r.URL.Query().Get("server_id")
|
||||
if serverID == "" {
|
||||
http.Error(w, "server_id query param required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.db.Query(
|
||||
`SELECT day_of_week, start_time, end_time FROM availability
|
||||
WHERE user_id = $1 AND server_id = $2
|
||||
ORDER BY array_position(ARRAY['monday','tuesday','wednesday','thursday','friday','saturday','sunday'], day_of_week), start_time`,
|
||||
userID, serverID,
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var slots []availabilitySlot
|
||||
for rows.Next() {
|
||||
var s availabilitySlot
|
||||
var st, et time.Time
|
||||
if err := rows.Scan(&s.DayOfWeek, &st, &et); err != nil {
|
||||
continue
|
||||
}
|
||||
s.StartTime = st.Format("15:04")
|
||||
s.EndTime = et.Format("15:04")
|
||||
slots = append(slots, s)
|
||||
}
|
||||
if slots == nil {
|
||||
slots = []availabilitySlot{}
|
||||
}
|
||||
json.NewEncoder(w).Encode(slots)
|
||||
}
|
||||
|
||||
func (h *Handler) PutMyAvailability(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
serverID := r.URL.Query().Get("server_id")
|
||||
if serverID == "" {
|
||||
http.Error(w, "server_id query param required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var slots []availabilitySlot
|
||||
if err := json.NewDecoder(r.Body).Decode(&slots); err != nil {
|
||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := h.db.Begin()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM availability WHERE user_id = $1 AND server_id = $2`, userID, serverID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range slots {
|
||||
_, err := tx.Exec(
|
||||
`INSERT INTO availability (user_id, server_id, day_of_week, start_time, end_time) VALUES ($1,$2,$3,$4::time,$5::time)`,
|
||||
userID, serverID, s.DayOfWeek, s.StartTime, s.EndTime,
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(slots)
|
||||
}
|
||||
|
||||
// GetServerAvailability returns all members' availability for a server, aggregated by user.
|
||||
func (h *Handler) GetServerAvailability(w http.ResponseWriter, r *http.Request) {
|
||||
serverID := chi.URLParam(r, "serverID")
|
||||
|
||||
rows, err := h.db.Query(
|
||||
`SELECT a.user_id, u.display_name, a.day_of_week, a.start_time, a.end_time
|
||||
FROM availability a
|
||||
JOIN users u ON u.id = a.user_id
|
||||
WHERE a.server_id = $1
|
||||
ORDER BY u.display_name, array_position(ARRAY['monday','tuesday','wednesday','thursday','friday','saturday','sunday'], a.day_of_week), a.start_time`,
|
||||
serverID,
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type slot struct {
|
||||
DayOfWeek string `json:"day_of_week"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
}
|
||||
type userAvail struct {
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Slots []slot `json:"slots"`
|
||||
}
|
||||
|
||||
userMap := make(map[string]*userAvail)
|
||||
for rows.Next() {
|
||||
var uid, uname, dow string
|
||||
var st, et time.Time
|
||||
if err := rows.Scan(&uid, &uname, &dow, &st, &et); err != nil {
|
||||
continue
|
||||
}
|
||||
ua, ok := userMap[uid]
|
||||
if !ok {
|
||||
ua = &userAvail{UserID: uid, Username: uname}
|
||||
userMap[uid] = ua
|
||||
}
|
||||
ua.Slots = append(ua.Slots, slot{DayOfWeek: dow, StartTime: st.Format("15:04"), EndTime: et.Format("15:04")})
|
||||
}
|
||||
|
||||
result := make([]userAvail, 0, len(userMap))
|
||||
for _, ua := range userMap {
|
||||
result = append(result, *ua)
|
||||
}
|
||||
if result == nil {
|
||||
result = []userAvail{}
|
||||
}
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
@@ -46,6 +46,8 @@ func (h *Handler) RegisterProtectedRoutes(r chi.Router) {
|
||||
r.Get("/users/me/blocks", h.ListBlocks)
|
||||
r.Post("/users/me/blocks", h.BlockUser)
|
||||
r.Delete("/users/me/blocks/{userID}", h.UnblockUser)
|
||||
r.Get("/users/me/availability", h.GetMyAvailability)
|
||||
r.Put("/users/me/availability", h.PutMyAvailability)
|
||||
}
|
||||
|
||||
// @Summary Get public user profile
|
||||
|
||||
@@ -435,6 +435,18 @@ CREATE TABLE IF NOT EXISTS list_items (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_list_items_channel ON list_items(channel_id, position);
|
||||
|
||||
-- Weekly availability
|
||||
CREATE TABLE IF NOT EXISTS availability (
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
||||
day_of_week VARCHAR(9) NOT NULL CHECK (day_of_week IN ('monday','tuesday','wednesday','thursday','friday','saturday','sunday')),
|
||||
start_time TIME NOT NULL,
|
||||
end_time TIME NOT NULL,
|
||||
PRIMARY KEY (user_id, server_id, day_of_week, start_time)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_availability_server ON availability(server_id, day_of_week);
|
||||
|
||||
-- Message full-text search
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS search_vector tsvector;
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_search ON messages USING GIN(search_vector);
|
||||
|
||||
Reference in New Issue
Block a user