This commit is contained in:
henrygd
2025-11-21 17:49:17 -05:00
parent 56a9915b43
commit 56807dc5e4

View File

@@ -1,19 +1,11 @@
import { t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { CalendarClockIcon, MoreHorizontalIcon, PlusIcon, Trash2Icon } from "lucide-react" import { MoreHorizontalIcon, PlusIcon, Trash2Icon, ServerIcon, ClockIcon, CalendarIcon, ActivityIcon, PenSquareIcon } from "lucide-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import {
AlertDialog, import { Badge } from "@/components/ui/badge"
AlertDialogAction, import { Button } from "@/components/ui/button"
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Button, buttonVariants } from "@/components/ui/button"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -37,15 +29,13 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useToast } from "@/components/ui/use-toast" import { useToast } from "@/components/ui/use-toast"
import { pb } from "@/lib/api" import { pb } from "@/lib/api"
import { $systems } from "@/lib/stores" import { $systems } from "@/lib/stores"
import { cn, formatShortDate } from "@/lib/utils" import { formatShortDate } from "@/lib/utils"
import type { QuietHoursRecord } from "@/types" import type { QuietHoursRecord } from "@/types"
export function QuietHours() { export function QuietHours() {
const [data, setData] = useState<QuietHoursRecord[]>([]) const [data, setData] = useState<QuietHoursRecord[]>([])
const [dialogOpen, setDialogOpen] = useState(false) const [dialogOpen, setDialogOpen] = useState(false)
const [editingRecord, setEditingRecord] = useState<QuietHoursRecord | null>(null) const [editingRecord, setEditingRecord] = useState<QuietHoursRecord | null>(null)
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [recordToDelete, setRecordToDelete] = useState<string | null>(null)
const { toast } = useToast() const { toast } = useToast()
const systems = useStore($systems) const systems = useStore($systems)
@@ -87,11 +77,9 @@ export function QuietHours() {
return () => unsubscribe?.() return () => unsubscribe?.()
}, []) }, [])
const handleDelete = async () => { const handleDelete = async (id: string) => {
if (!recordToDelete) return
setDeleteDialogOpen(false)
try { try {
await pb.collection("quiet_hours").delete(recordToDelete) await pb.collection("quiet_hours").delete(id)
} catch (e: any) { } catch (e: any) {
toast({ toast({
variant: "destructive", variant: "destructive",
@@ -99,12 +87,6 @@ export function QuietHours() {
description: e.message || "Failed to delete quiet hours.", description: e.message || "Failed to delete quiet hours.",
}) })
} }
setRecordToDelete(null)
}
const openDeleteDialog = (id: string) => {
setRecordToDelete(id)
setDeleteDialogOpen(true)
} }
const openEditDialog = (record: QuietHoursRecord) => { const openEditDialog = (record: QuietHoursRecord) => {
@@ -130,6 +112,59 @@ export function QuietHours() {
return end ? `${start} - ${end}` : start return end ? `${start} - ${end}` : start
} }
const getWindowState = (record: QuietHoursRecord): "active" | "past" | "future" => {
const now = new Date()
if (record.type === "daily") {
// For daily windows, check if current time is within the window
const startDate = new Date(record.start)
const endDate = record.end ? new Date(record.end) : null
// Get current time in local timezone
const currentMinutes = now.getHours() * 60 + now.getMinutes()
const startMinutes = startDate.getUTCHours() * 60 + startDate.getUTCMinutes()
const endMinutes = endDate ? endDate.getUTCHours() * 60 + endDate.getUTCMinutes() : null
// Convert UTC to local time offset
const offset = now.getTimezoneOffset()
const localStartMinutes = (startMinutes - offset + 1440) % 1440
const localEndMinutes = endMinutes !== null ? (endMinutes - offset + 1440) % 1440 : null
if (localEndMinutes === null) {
// No end time, so it's always active from start time onwards each day
return "active"
}
// Handle cases where window spans midnight
if (localStartMinutes <= localEndMinutes) {
return currentMinutes >= localStartMinutes && currentMinutes < localEndMinutes ? "active" : "future"
} else {
return currentMinutes >= localStartMinutes || currentMinutes < localEndMinutes ? "active" : "future"
}
} else {
// For one-time windows
const startDate = new Date(record.start)
const endDate = record.end ? new Date(record.end) : null
if (endDate) {
if (now >= startDate && now <= endDate) {
return "active"
} else if (now > endDate) {
return "past"
} else {
return "future"
}
} else {
// No end date
if (now >= startDate) {
return "active"
} else {
return "future"
}
}
}
}
return ( return (
<> <>
<div className="grid grid-cols-1 sm:flex items-center justify-between gap-4 mb-3"> <div className="grid grid-cols-1 sm:flex items-center justify-between gap-4 mb-3">
@@ -164,13 +199,28 @@ export function QuietHours() {
<TableHeader> <TableHeader>
<TableRow className="border-border/50"> <TableRow className="border-border/50">
<TableHead className="px-4"> <TableHead className="px-4">
<Trans>System</Trans> <span className="flex items-center gap-2">
<ServerIcon className="size-4" />
<Trans>System</Trans>
</span>
</TableHead> </TableHead>
<TableHead className="px-4"> <TableHead className="px-4">
<Trans>Type</Trans> <span className="flex items-center gap-2">
<ClockIcon className="size-4" />
<Trans>Type</Trans>
</span>
</TableHead> </TableHead>
<TableHead className="px-4"> <TableHead className="px-4">
<Trans>Schedule</Trans> <span className="flex items-center gap-2">
<ActivityIcon className="size-4" />
<Trans>State</Trans>
</span>
</TableHead>
<TableHead className="px-4">
<span className="flex items-center gap-2">
<CalendarIcon className="size-4" />
<Trans>Schedule</Trans>
</span>
</TableHead> </TableHead>
<TableHead className="px-4 text-right sr-only"> <TableHead className="px-4 text-right sr-only">
<Trans>Actions</Trans> <Trans>Actions</Trans>
@@ -186,6 +236,22 @@ export function QuietHours() {
<TableCell className="px-4 py-3"> <TableCell className="px-4 py-3">
{record.type === "daily" ? <Trans>Daily</Trans> : <Trans>One-time</Trans>} {record.type === "daily" ? <Trans>Daily</Trans> : <Trans>One-time</Trans>}
</TableCell> </TableCell>
<TableCell className="px-4 py-3">
{(() => {
const state = getWindowState(record)
const stateConfig = {
active: { label: <Trans>Active</Trans>, variant: "success" as const },
past: { label: <Trans>Past</Trans>, variant: "danger" as const },
future: { label: <Trans>Future</Trans>, variant: "default" as const },
}
const config = stateConfig[state]
return (
<Badge variant={config.variant}>
{config.label}
</Badge>
)
})()}
</TableCell>
<TableCell className="px-4 py-3">{formatDateTime(record)}</TableCell> <TableCell className="px-4 py-3">{formatDateTime(record)}</TableCell>
<TableCell className="px-4 py-3 text-right"> <TableCell className="px-4 py-3 text-right">
<DropdownMenu> <DropdownMenu>
@@ -197,10 +263,10 @@ export function QuietHours() {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openEditDialog(record)}> <DropdownMenuItem onClick={() => openEditDialog(record)}>
<CalendarClockIcon className="me-2.5 size-4" /> <PenSquareIcon className="me-2.5 size-4" />
<Trans>Edit</Trans> <Trans>Edit</Trans>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => openDeleteDialog(record.id)}> <DropdownMenuItem onClick={() => handleDelete(record.id)}>
<Trash2Icon className="me-2.5 size-4" /> <Trash2Icon className="me-2.5 size-4" />
<Trans>Delete</Trans> <Trans>Delete</Trans>
</DropdownMenuItem> </DropdownMenuItem>
@@ -214,26 +280,7 @@ export function QuietHours() {
</div> </div>
)} )}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans>Are you sure?</Trans>
</AlertDialogTitle>
<AlertDialogDescription>
<Trans>This will permanently delete these quiet hours.</Trans>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans>Cancel</Trans>
</AlertDialogCancel>
<AlertDialogAction className={cn(buttonVariants({ variant: "destructive" }))} onClick={handleDelete}>
<Trans>Delete</Trans>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</> </>
) )
} }
@@ -289,7 +336,7 @@ function QuietHoursDialog({
} else { } else {
// Reset form // Reset form
setSelectedSystem("") setSelectedSystem("")
setIsGlobal(false) setIsGlobal(true)
setWindowType("one-time") setWindowType("one-time")
setStartDateTime("") setStartDateTime("")
setEndDateTime("") setEndDateTime("")
@@ -366,12 +413,12 @@ function QuietHoursDialog({
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<Tabs value={isGlobal ? "global" : "system"} onValueChange={(value) => setIsGlobal(value === "global")}> <Tabs value={isGlobal ? "global" : "system"} onValueChange={(value) => setIsGlobal(value === "global")}>
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="system">
<Trans>Specific System</Trans>
</TabsTrigger>
<TabsTrigger value="global"> <TabsTrigger value="global">
<Trans>All Systems</Trans> <Trans>All Systems</Trans>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="system">
<Trans>Specific System</Trans>
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="system" className="mt-4 space-y-4"> <TabsContent value="system" className="mt-4 space-y-4">