Files
oslstats/internal/view/seasonsview/series_schedule.templ
2026-03-15 12:05:47 +11:00

505 lines
17 KiB
Plaintext

package seasonsview
import "git.haelnorr.com/h/oslstats/internal/db"
import "fmt"
// ==================== Schedule Tab ====================
templ seriesScheduleTab(
series *db.PlayoffSeries,
currentSchedule *db.PlayoffSeriesSchedule,
history []*db.PlayoffSeriesSchedule,
canSchedule bool,
canManage bool,
userTeamID int,
) {
<div class="space-y-6">
@seriesScheduleStatus(series, currentSchedule, canSchedule, canManage, userTeamID)
@seriesScheduleActions(series, currentSchedule, canSchedule, canManage, userTeamID)
@seriesScheduleHistory(series, history)
</div>
}
templ seriesScheduleStatus(
series *db.PlayoffSeries,
current *db.PlayoffSeriesSchedule,
canSchedule bool,
canManage bool,
userTeamID int,
) {
{{
bothTeamsAssigned := series.Team1ID != nil && series.Team2ID != nil
}}
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
<div class="bg-surface0 border-b border-surface1 px-4 py-3">
<h2 class="text-lg font-bold text-text">Schedule Status</h2>
</div>
<div class="p-6">
if !bothTeamsAssigned {
<div class="text-center py-4">
<div class="text-4xl mb-3">⏳</div>
<p class="text-lg text-text font-medium">Waiting for Teams</p>
<p class="text-sm text-subtext1 mt-1">
Both teams must be determined before scheduling can begin.
</p>
</div>
} else if current == nil {
<div class="text-center py-4">
<div class="text-4xl mb-3">📅</div>
<p class="text-lg text-text font-medium">No time scheduled</p>
<p class="text-sm text-subtext1 mt-1">
if canSchedule {
Use the form to propose a time for this series.
} else {
A team manager needs to propose a time for this series.
}
</p>
</div>
} else if current.Status == db.ScheduleStatusPending && current.ScheduledTime != nil {
<div class="text-center py-4">
<div class="text-4xl mb-3">⏳</div>
<p class="text-lg text-text font-medium">
Proposed:
@localtime(current.ScheduledTime, "datetime")
</p>
<p class="text-sm text-subtext1 mt-1">
Proposed by
<span class="text-text font-medium">{ current.ProposedBy.Name }</span>
— awaiting response from the other team
</p>
if canSchedule && userTeamID != current.ProposedByTeamID {
<div class="flex justify-center gap-3 mt-4">
<form
hx-post={ fmt.Sprintf("/series/%d/schedule/%d/accept", series.ID, current.ID) }
hx-swap="none"
>
<button
type="submit"
class="px-4 py-2 bg-green hover:bg-green/75 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Accept
</button>
</form>
<form
hx-post={ fmt.Sprintf("/series/%d/schedule/%d/reject", series.ID, current.ID) }
hx-swap="none"
>
<button
type="submit"
class="px-4 py-2 bg-red hover:bg-red/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Reject
</button>
</form>
</div>
}
if canSchedule && userTeamID == current.ProposedByTeamID {
<div class="flex justify-center mt-4">
<button
type="button"
@click={ "window.dispatchEvent(new CustomEvent('confirm-action', { detail: { title: 'Withdraw Proposal', message: 'Are you sure you want to withdraw your proposed time?', action: () => htmx.ajax('POST', '/series/" + fmt.Sprint(series.ID) + "/schedule/" + fmt.Sprint(current.ID) + "/withdraw', { swap: 'none' }) } }))" }
class="px-4 py-2 bg-surface1 hover:bg-surface2 text-text rounded-lg
font-medium transition hover:cursor-pointer"
>
Withdraw Proposal
</button>
</div>
}
</div>
} else if current.Status == db.ScheduleStatusAccepted {
<div class="text-center py-4">
<div class="text-4xl mb-3">✅</div>
<p class="text-lg text-green font-medium">
Confirmed:
@localtime(current.ScheduledTime, "datetime")
</p>
<p class="text-sm text-subtext1 mt-1">
Both teams have agreed on this time.
</p>
</div>
} else if current.Status == db.ScheduleStatusRejected {
<div class="text-center py-4">
<div class="text-4xl mb-3">❌</div>
<p class="text-lg text-red font-medium">Proposal Rejected</p>
<p class="text-sm text-subtext1 mt-1">
The proposed time was rejected. A new time needs to be proposed.
</p>
</div>
} else if current.Status == db.ScheduleStatusCancelled {
<div class="text-center py-4">
<div class="text-4xl mb-3">🚫</div>
<p class="text-lg text-red font-medium">Schedule Cancelled</p>
if current.RescheduleReason != nil {
<p class="text-sm text-subtext1 mt-1">
{ *current.RescheduleReason }
</p>
}
</div>
} else if current.Status == db.ScheduleStatusRescheduled {
<div class="text-center py-4">
<div class="text-4xl mb-3">🔄</div>
<p class="text-lg text-yellow font-medium">Rescheduled</p>
if current.RescheduleReason != nil {
<p class="text-sm text-subtext1 mt-1">
Reason: { *current.RescheduleReason }
</p>
}
<p class="text-sm text-subtext1 mt-1">
A new time needs to be proposed.
</p>
</div>
} else if current.Status == db.ScheduleStatusPostponed {
<div class="text-center py-4">
<div class="text-4xl mb-3">⏸️</div>
<p class="text-lg text-peach font-medium">Postponed</p>
if current.RescheduleReason != nil {
<p class="text-sm text-subtext1 mt-1">
Reason: { *current.RescheduleReason }
</p>
}
<p class="text-sm text-subtext1 mt-1">
A new time needs to be proposed.
</p>
</div>
} else if current.Status == db.ScheduleStatusWithdrawn {
<div class="text-center py-4">
<div class="text-4xl mb-3">↩️</div>
<p class="text-lg text-subtext0 font-medium">Proposal Withdrawn</p>
<p class="text-sm text-subtext1 mt-1">
The proposed time was withdrawn. A new time needs to be proposed.
</p>
</div>
}
</div>
</div>
}
templ seriesScheduleActions(
series *db.PlayoffSeries,
current *db.PlayoffSeriesSchedule,
canSchedule bool,
canManage bool,
userTeamID int,
) {
{{
bothTeamsAssigned := series.Team1ID != nil && series.Team2ID != nil
showPropose := false
showReschedule := false
showPostpone := false
showCancel := false
if bothTeamsAssigned && canSchedule {
if current == nil {
showPropose = true
} else if current.Status == db.ScheduleStatusRejected {
showPropose = true
} else if current.Status == db.ScheduleStatusRescheduled {
showPropose = true
} else if current.Status == db.ScheduleStatusPostponed {
showPropose = true
} else if current.Status == db.ScheduleStatusWithdrawn {
showPropose = true
} else if current.Status == db.ScheduleStatusAccepted {
showReschedule = true
showPostpone = true
}
}
if bothTeamsAssigned && canManage && current != nil && !current.Status.IsTerminal() {
showCancel = true
}
}}
if showPropose || showReschedule || showPostpone || showCancel {
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Propose Time -->
if showPropose {
<div class="bg-mantle border border-surface1 rounded-lg">
<div class="bg-surface0 border-b border-surface1 px-4 py-3 rounded-t-lg">
<h3 class="text-md font-bold text-text">Propose Time</h3>
</div>
<div class="p-4">
<form
hx-post={ fmt.Sprintf("/series/%d/schedule", series.ID) }
hx-swap="none"
class="space-y-4"
>
<div>
<label class="block text-sm font-medium text-subtext0 mb-1">
Date & Time
<span class="relative group inline-block ml-1">
<span class="px-1.5 py-0.5 bg-blue/20 text-blue rounded text-xs font-medium cursor-help">AEST/AEDT</span>
<span
class="absolute left-0 top-full mt-1 w-56 bg-crust border border-surface1 rounded-lg
p-2 text-xs text-subtext0 shadow-lg opacity-0 invisible
group-hover:opacity-100 group-hover:visible transition-all z-50"
>
Enter the time in Australian Eastern time (AEST/AEDT). It will be displayed in each user's local timezone.
</span>
</span>
</label>
<input
type="datetime-local"
name="scheduled_time"
required
class="w-full px-3 py-2 bg-base border border-surface1 rounded-lg text-text
focus:border-blue focus:outline-none"
/>
</div>
<button
type="submit"
class="w-full px-4 py-2 bg-blue hover:bg-blue/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Propose Time
</button>
</form>
</div>
</div>
}
<!-- Reschedule -->
if showReschedule {
<div class="bg-mantle border border-surface1 rounded-lg">
<div class="bg-surface0 border-b border-surface1 px-4 py-3 rounded-t-lg">
<h3 class="text-md font-bold text-text">Reschedule</h3>
</div>
<div class="p-4">
<form
hx-post={ fmt.Sprintf("/series/%d/schedule/reschedule", series.ID) }
hx-swap="none"
class="space-y-4"
>
<div>
<label class="block text-sm font-medium text-subtext0 mb-1">
New Date & Time
<span class="relative group inline-block ml-1">
<span class="px-1.5 py-0.5 bg-blue/20 text-blue rounded text-xs font-medium cursor-help">AEST/AEDT</span>
<span
class="absolute left-0 top-full mt-1 w-56 bg-crust border border-surface1 rounded-lg
p-2 text-xs text-subtext0 shadow-lg opacity-0 invisible
group-hover:opacity-100 group-hover:visible transition-all z-50"
>
Enter the time in Australian Eastern time (AEST/AEDT). It will be displayed in each user's local timezone.
</span>
</span>
</label>
<input
type="datetime-local"
name="scheduled_time"
required
class="w-full px-3 py-2 bg-base border border-surface1 rounded-lg text-text
focus:border-blue focus:outline-none"
/>
</div>
<div>
<label class="block text-sm font-medium text-subtext0 mb-1">Reason</label>
@seriesRescheduleReasonSelect(series)
</div>
<button
type="submit"
class="w-full px-4 py-2 bg-yellow hover:bg-yellow/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Reschedule
</button>
</form>
</div>
</div>
}
<!-- Postpone -->
if showPostpone {
<div class="bg-mantle border border-surface1 rounded-lg">
<div class="bg-surface0 border-b border-surface1 px-4 py-3 rounded-t-lg">
<h3 class="text-md font-bold text-text">Postpone</h3>
</div>
<div class="p-4">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-subtext0 mb-1">Reason</label>
@seriesRescheduleReasonSelect(series)
</div>
<button
type="button"
@click={ fmt.Sprintf("if (!$el.parentElement.querySelector('select').value) { return; } window.dispatchEvent(new CustomEvent('confirm-action', { detail: { title: 'Postpone Series', message: 'Are you sure you want to postpone this series? The current schedule will be cleared.', action: () => htmx.ajax('POST', '/series/%d/schedule/postpone', { swap: 'none', values: { reschedule_reason: $el.parentElement.querySelector('select').value } }) } }))", series.ID) }
class="w-full px-4 py-2 bg-peach hover:bg-peach/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Postpone Series
</button>
</div>
</div>
</div>
}
<!-- Cancel (moderator only) -->
if showCancel {
<div class="bg-mantle border border-red/30 rounded-lg">
<div class="bg-red/10 border-b border-red/30 px-4 py-3 rounded-t-lg">
<h3 class="text-md font-bold text-red">Cancel Schedule</h3>
</div>
<div class="p-4">
<p class="text-xs text-red/80 mb-3 font-medium">
This action will cancel the current series schedule.
</p>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-subtext0 mb-1">Reason</label>
<input
type="text"
name="reschedule_reason"
placeholder="Enter reason..."
required
class="w-full px-3 py-2 bg-base border border-surface1 rounded-lg text-text
focus:border-blue focus:outline-none"
/>
</div>
<button
type="button"
@click={ fmt.Sprintf("const reason = $el.parentElement.querySelector('input[name=reschedule_reason]').value; if (!reason) { return; } window.dispatchEvent(new CustomEvent('confirm-action', { detail: { title: 'Cancel Schedule', message: 'Are you sure you want to cancel this schedule?', action: () => htmx.ajax('POST', '/series/%d/schedule/cancel', { swap: 'none', values: { reschedule_reason: reason } }) } }))", series.ID) }
class="w-full px-4 py-2 bg-red hover:bg-red/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Cancel Schedule
</button>
</div>
</div>
</div>
}
</div>
} else {
if !canSchedule && !canManage {
<div class="bg-mantle border border-surface1 rounded-lg p-6 text-center">
<p class="text-subtext1 text-sm">
Only team managers can manage series scheduling.
</p>
</div>
}
}
}
templ seriesRescheduleReasonSelect(series *db.PlayoffSeries) {
{{
team1Name := seriesTeamName(series.Team1)
team2Name := seriesTeamName(series.Team2)
}}
<select
name="reschedule_reason"
required
class="w-full px-3 py-2 bg-base border border-surface1 rounded-lg text-text
focus:border-blue focus:outline-none hover:cursor-pointer"
>
<option value="" disabled selected>Select a reason</option>
<option value="Mutually Agreed">Mutually Agreed</option>
<option value={ fmt.Sprintf("%s Unavailable", team1Name) }>
{ team1Name } Unavailable
</option>
<option value={ fmt.Sprintf("%s Unavailable", team2Name) }>
{ team2Name } Unavailable
</option>
<option value={ fmt.Sprintf("%s No-show", team1Name) }>
{ team1Name } No-show
</option>
<option value={ fmt.Sprintf("%s No-show", team2Name) }>
{ team2Name } No-show
</option>
</select>
}
templ seriesScheduleHistory(series *db.PlayoffSeries, history []*db.PlayoffSeriesSchedule) {
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
<div class="bg-surface0 border-b border-surface1 px-4 py-3">
<h2 class="text-lg font-bold text-text">Schedule History</h2>
</div>
<div class="p-4">
if len(history) == 0 {
<p class="text-subtext1 text-sm text-center py-4">No scheduling activity yet.</p>
} else {
<div class="space-y-3">
for i := len(history) - 1; i >= 0; i-- {
@seriesScheduleHistoryItem(history[i], i == len(history)-1)
}
</div>
}
</div>
</div>
}
templ seriesScheduleHistoryItem(schedule *db.PlayoffSeriesSchedule, isCurrent bool) {
{{
statusColor := "text-subtext0"
statusBg := "bg-surface1"
statusLabel := string(schedule.Status)
switch schedule.Status {
case db.ScheduleStatusPending:
statusColor = "text-blue"
statusBg = "bg-blue/20"
statusLabel = "Pending"
case db.ScheduleStatusAccepted:
statusColor = "text-green"
statusBg = "bg-green/20"
statusLabel = "Accepted"
case db.ScheduleStatusRejected:
statusColor = "text-red"
statusBg = "bg-red/20"
statusLabel = "Rejected"
case db.ScheduleStatusRescheduled:
statusColor = "text-yellow"
statusBg = "bg-yellow/20"
statusLabel = "Rescheduled"
case db.ScheduleStatusPostponed:
statusColor = "text-peach"
statusBg = "bg-peach/20"
statusLabel = "Postponed"
case db.ScheduleStatusCancelled:
statusColor = "text-red"
statusBg = "bg-red/20"
statusLabel = "Cancelled"
case db.ScheduleStatusWithdrawn:
statusColor = "text-subtext0"
statusBg = "bg-surface1"
statusLabel = "Withdrawn"
}
}}
<div class={ "border rounded-lg p-3", templ.KV("border-surface1", !isCurrent), templ.KV("border-blue/30 bg-blue/5", isCurrent) }>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
if isCurrent {
<span class="text-xs px-1.5 py-0.5 bg-blue/20 text-blue rounded font-medium">
CURRENT
</span>
}
<span class={ "text-xs px-2 py-0.5 rounded font-medium", statusBg, statusColor }>
{ statusLabel }
</span>
</div>
<span class="text-xs text-subtext1">
@localtimeUnix(schedule.CreatedAt, "histdate")
</span>
</div>
<div class="space-y-1">
<div class="flex items-center gap-2 text-sm">
<span class="text-subtext0">Proposed by:</span>
<span class="text-text font-medium">{ schedule.ProposedBy.Name }</span>
</div>
if schedule.ScheduledTime != nil {
<div class="flex items-center gap-2 text-sm">
<span class="text-subtext0">Time:</span>
<span class="text-text">
@localtime(schedule.ScheduledTime, "datetime")
</span>
</div>
} else {
<div class="flex items-center gap-2 text-sm">
<span class="text-subtext0">Time:</span>
<span class="text-subtext1 italic">No time set</span>
</div>
}
if schedule.RescheduleReason != nil {
<div class="flex items-center gap-2 text-sm">
<span class="text-subtext0">Reason:</span>
<span class="text-subtext1">{ *schedule.RescheduleReason }</span>
</div>
}
</div>
</div>
}