Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
20c2c5fce2
commit
9cf0b14030
@@ -19,6 +19,7 @@ import { cn } from "@/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
import { useBubbleCounts } from "@/lib/use-bubble-counts";
|
||||
import { NotificationBell } from "@/components/notifications/notification-bell";
|
||||
import { DateCalculator } from "@/components/date-calculator";
|
||||
|
||||
function initials(name: string, email: string) {
|
||||
const src = (name || email || "").trim();
|
||||
@@ -102,6 +103,7 @@ export function AppShell({ children }: { children: ReactNode }) {
|
||||
<div className="font-serif text-lg truncate">Stage Law Firm, PLLC</div>
|
||||
<div className="text-[10px] uppercase tracking-widest text-sidebar-foreground/60">Law Firm Suite</div>
|
||||
</div>
|
||||
<DateCalculator />
|
||||
<NotificationBell />
|
||||
</div>
|
||||
|
||||
@@ -168,6 +170,7 @@ export function AppShell({ children }: { children: ReactNode }) {
|
||||
<span className="font-serif text-lg">Counsel</span>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<DateCalculator />
|
||||
<NotificationBell />
|
||||
<Button variant="ghost" size="sm" onClick={handleSignOut} className="text-sidebar-foreground">
|
||||
<LogOut className="h-4 w-4" />
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { CalendarClock, CalendarIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
|
||||
type Unit = "days" | "weeks" | "months" | "years";
|
||||
|
||||
function addUnits(base: Date, amount: number, unit: Unit, businessOnly: boolean): Date {
|
||||
const d = new Date(base);
|
||||
if (unit === "days") {
|
||||
if (!businessOnly) {
|
||||
d.setDate(d.getDate() + amount);
|
||||
return d;
|
||||
}
|
||||
let remaining = Math.abs(amount);
|
||||
const step = amount >= 0 ? 1 : -1;
|
||||
while (remaining > 0) {
|
||||
d.setDate(d.getDate() + step);
|
||||
const day = d.getDay();
|
||||
if (day !== 0 && day !== 6) remaining--;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
if (unit === "weeks") {
|
||||
d.setDate(d.getDate() + amount * 7);
|
||||
return d;
|
||||
}
|
||||
if (unit === "months") {
|
||||
d.setMonth(d.getMonth() + amount);
|
||||
return d;
|
||||
}
|
||||
d.setFullYear(d.getFullYear() + amount);
|
||||
return d;
|
||||
}
|
||||
|
||||
function daysBetween(a: Date, b: Date, businessOnly: boolean) {
|
||||
const start = new Date(a);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const end = new Date(b);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
const sign = end >= start ? 1 : -1;
|
||||
if (!businessOnly) {
|
||||
return Math.round((end.getTime() - start.getTime()) / 86400000);
|
||||
}
|
||||
let count = 0;
|
||||
const cur = new Date(start);
|
||||
while (cur.getTime() !== end.getTime()) {
|
||||
cur.setDate(cur.getDate() + sign);
|
||||
const day = cur.getDay();
|
||||
if (day !== 0 && day !== 6) count += sign;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export function DateCalculator({ trigger }: { trigger?: React.ReactNode }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Add/subtract state
|
||||
const [baseDate, setBaseDate] = useState<Date>(new Date());
|
||||
const [amount, setAmount] = useState<string>("30");
|
||||
const [unit, setUnit] = useState<Unit>("days");
|
||||
const [businessOnly, setBusinessOnly] = useState(false);
|
||||
const [direction, setDirection] = useState<"add" | "sub">("add");
|
||||
|
||||
// Diff state
|
||||
const [fromDate, setFromDate] = useState<Date>(new Date());
|
||||
const [toDate, setToDate] = useState<Date>(() => {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + 30);
|
||||
return d;
|
||||
});
|
||||
const [diffBusinessOnly, setDiffBusinessOnly] = useState(false);
|
||||
|
||||
const result = useMemo(() => {
|
||||
const n = parseInt(amount || "0", 10);
|
||||
if (Number.isNaN(n)) return null;
|
||||
return addUnits(baseDate, direction === "add" ? n : -n, unit, businessOnly);
|
||||
}, [baseDate, amount, unit, direction, businessOnly]);
|
||||
|
||||
const diff = useMemo(() => daysBetween(fromDate, toDate, diffBusinessOnly), [
|
||||
fromDate,
|
||||
toDate,
|
||||
diffBusinessOnly,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
{trigger ?? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-sidebar-foreground hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground"
|
||||
aria-label="Date calculator"
|
||||
>
|
||||
<CalendarClock className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-[340px] p-0">
|
||||
<div className="px-4 py-3 border-b">
|
||||
<div className="font-serif text-sm">Date calculator</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
Add/subtract time or count days between dates.
|
||||
</div>
|
||||
</div>
|
||||
<Tabs defaultValue="add" className="w-full">
|
||||
<TabsList className="grid grid-cols-2 m-3">
|
||||
<TabsTrigger value="add">Add / subtract</TabsTrigger>
|
||||
<TabsTrigger value="diff">Difference</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="add" className="px-4 pb-4 space-y-3 mt-0">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Start date</Label>
|
||||
<DatePopover value={baseDate} onChange={setBaseDate} />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Op</Label>
|
||||
<Select value={direction} onValueChange={(v) => setDirection(v as "add" | "sub")}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="add">+</SelectItem>
|
||||
<SelectItem value="sub">−</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Amount</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Unit</Label>
|
||||
<Select value={unit} onValueChange={(v) => setUnit(v as Unit)}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
<SelectItem value="weeks">Weeks</SelectItem>
|
||||
<SelectItem value="months">Months</SelectItem>
|
||||
<SelectItem value="years">Years</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{unit === "days" && (
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={businessOnly}
|
||||
onChange={(e) => setBusinessOnly(e.target.checked)}
|
||||
className="rounded border-input"
|
||||
/>
|
||||
Business days only (Mon–Fri)
|
||||
</label>
|
||||
)}
|
||||
<div className="rounded-md border bg-muted/40 px-3 py-2.5">
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">Result</div>
|
||||
<div className="font-serif text-base mt-0.5">
|
||||
{result ? format(result, "EEEE, MMM d, yyyy") : "—"}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="diff" className="px-4 pb-4 space-y-3 mt-0">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">From</Label>
|
||||
<DatePopover value={fromDate} onChange={setFromDate} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">To</Label>
|
||||
<DatePopover value={toDate} onChange={setToDate} />
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={diffBusinessOnly}
|
||||
onChange={(e) => setDiffBusinessOnly(e.target.checked)}
|
||||
className="rounded border-input"
|
||||
/>
|
||||
Business days only (Mon–Fri)
|
||||
</label>
|
||||
<div className="rounded-md border bg-muted/40 px-3 py-2.5">
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">
|
||||
{diffBusinessOnly ? "Business days" : "Calendar days"}
|
||||
</div>
|
||||
<div className="font-serif text-base mt-0.5">
|
||||
{diff} day{Math.abs(diff) === 1 ? "" : "s"}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function DatePopover({ value, onChange }: { value: Date; onChange: (d: Date) => void }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("w-full justify-start text-left font-normal h-9")}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4 opacity-60" />
|
||||
{format(value, "PPP")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={value}
|
||||
onSelect={(d) => {
|
||||
if (d) {
|
||||
onChange(d);
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
initialFocus
|
||||
className={cn("p-3 pointer-events-auto")}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user