505 lines
17 KiB
Plaintext
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>
|
|
}
|