Compare commits
2 Commits
2ff325a132
...
d8b39979cd
| Author | SHA1 | Date | |
|---|---|---|---|
| d8b39979cd | |||
| 4776a112b6 |
File diff suppressed because it is too large
Load Diff
269
inventory/src/components/dashboard/PeriodSelectionPopover.tsx
Normal file
269
inventory/src/components/dashboard/PeriodSelectionPopover.tsx
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import { useMemo, useState, type KeyboardEventHandler } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Calendar } from "lucide-react";
|
||||||
|
import {
|
||||||
|
generateNaturalLanguagePreview,
|
||||||
|
parseNaturalLanguagePeriod,
|
||||||
|
type NaturalLanguagePeriodResult,
|
||||||
|
} from "@/utils/naturalLanguagePeriod";
|
||||||
|
|
||||||
|
export type QuickPreset =
|
||||||
|
| "last30days"
|
||||||
|
| "thisMonth"
|
||||||
|
| "lastMonth"
|
||||||
|
| "thisQuarter"
|
||||||
|
| "lastQuarter"
|
||||||
|
| "thisYear";
|
||||||
|
|
||||||
|
const SUGGESTIONS = [
|
||||||
|
"last 30 days",
|
||||||
|
"this month",
|
||||||
|
"last month",
|
||||||
|
"this quarter",
|
||||||
|
"last quarter",
|
||||||
|
"this year",
|
||||||
|
"last year",
|
||||||
|
"last 3 months",
|
||||||
|
"last 6 months",
|
||||||
|
"last 2 quarters",
|
||||||
|
"Q1 2024",
|
||||||
|
"q1-q3 24",
|
||||||
|
"q1 24 - q2 25",
|
||||||
|
"January 2024",
|
||||||
|
"jan-24",
|
||||||
|
"jan-may 24",
|
||||||
|
"2023",
|
||||||
|
"2021-2023",
|
||||||
|
"21-23",
|
||||||
|
"January to March 2024",
|
||||||
|
"jan 2023 - may 2024",
|
||||||
|
];
|
||||||
|
|
||||||
|
interface PeriodSelectionPopoverProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
selectedLabel: string;
|
||||||
|
referenceDate: Date;
|
||||||
|
isLast30DaysActive: boolean;
|
||||||
|
onQuickSelect: (preset: QuickPreset) => void;
|
||||||
|
onApplyResult: (result: NaturalLanguagePeriodResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PeriodSelectionPopover = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
selectedLabel,
|
||||||
|
referenceDate,
|
||||||
|
isLast30DaysActive,
|
||||||
|
onQuickSelect,
|
||||||
|
onApplyResult,
|
||||||
|
}: PeriodSelectionPopoverProps) => {
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
|
|
||||||
|
const filteredSuggestions = useMemo(() => {
|
||||||
|
if (!inputValue) {
|
||||||
|
return SUGGESTIONS;
|
||||||
|
}
|
||||||
|
return SUGGESTIONS.filter((suggestion) =>
|
||||||
|
suggestion.toLowerCase().includes(inputValue.toLowerCase()) &&
|
||||||
|
suggestion.toLowerCase() !== inputValue.toLowerCase()
|
||||||
|
);
|
||||||
|
}, [inputValue]);
|
||||||
|
|
||||||
|
const preview = useMemo(() => {
|
||||||
|
if (!inputValue) {
|
||||||
|
return { label: null, parsed: null } as const;
|
||||||
|
}
|
||||||
|
const parsed = parseNaturalLanguagePeriod(inputValue, referenceDate);
|
||||||
|
return {
|
||||||
|
parsed,
|
||||||
|
label: generateNaturalLanguagePreview(parsed),
|
||||||
|
} as const;
|
||||||
|
}, [inputValue, referenceDate]);
|
||||||
|
|
||||||
|
const closePopover = () => {
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetInput = () => {
|
||||||
|
setInputValue("");
|
||||||
|
setShowSuggestions(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyResult = (value: string) => {
|
||||||
|
const parsed = parseNaturalLanguagePeriod(value, referenceDate);
|
||||||
|
if (!parsed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onApplyResult(parsed);
|
||||||
|
resetInput();
|
||||||
|
closePopover();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (value: string) => {
|
||||||
|
setInputValue(value);
|
||||||
|
setShowSuggestions(value.length > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
applyResult(inputValue);
|
||||||
|
}
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
resetInput();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuggestionClick = (suggestion: string) => {
|
||||||
|
setInputValue(suggestion);
|
||||||
|
applyResult(suggestion);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuickSelect = (preset: QuickPreset) => {
|
||||||
|
onQuickSelect(preset);
|
||||||
|
resetInput();
|
||||||
|
closePopover();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={onOpenChange}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" className="h-9">
|
||||||
|
<Calendar className="w-4 h-4 mr-2" />
|
||||||
|
{selectedLabel}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-96 p-4" align="end">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-sm font-medium">Select Time Period</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
<Button
|
||||||
|
variant={isLast30DaysActive ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleQuickSelect("last30days")}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
>
|
||||||
|
Last 30 Days
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleQuickSelect("thisMonth")}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
>
|
||||||
|
This Month
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleQuickSelect("lastMonth")}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
>
|
||||||
|
Last Month
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleQuickSelect("thisQuarter")}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
>
|
||||||
|
This Quarter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleQuickSelect("lastQuarter")}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
>
|
||||||
|
Last Quarter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleQuickSelect("thisYear")}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
>
|
||||||
|
This Year
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-xs text-muted-foreground">Or type a custom period</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
placeholder="e.g., jan-may 24, 2021-2023, Q1-Q3 2024"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(event) => handleInputChange(event.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="h-8 text-sm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{inputValue && (
|
||||||
|
<div className="mt-2">
|
||||||
|
{preview.label ? (
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<span className="text-muted-foreground">Recognized as:</span>
|
||||||
|
<span className="font-medium text-green-600 dark:text-green-400">
|
||||||
|
{preview.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-amber-600 dark:text-amber-400">
|
||||||
|
Not recognized - try a different format
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showSuggestions && filteredSuggestions.length > 0 && (
|
||||||
|
<div className="absolute top-full left-0 right-0 z-50 mt-1 bg-white dark:bg-gray-800 border rounded-md shadow-lg max-h-32 overflow-y-auto">
|
||||||
|
{filteredSuggestions.slice(0, 6).map((suggestion) => (
|
||||||
|
<button
|
||||||
|
key={suggestion}
|
||||||
|
className="w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors"
|
||||||
|
onClick={() => handleSuggestionClick(suggestion)}
|
||||||
|
>
|
||||||
|
{suggestion}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{inputValue === "" && (
|
||||||
|
<div className="text-xs text-muted-foreground mt-2">
|
||||||
|
<div className="mb-1">Examples:</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{SUGGESTIONS.slice(0, 6).map((suggestion) => (
|
||||||
|
<button
|
||||||
|
key={suggestion}
|
||||||
|
className="px-2 py-0.5 bg-muted hover:bg-muted/80 rounded text-xs transition-colors"
|
||||||
|
onClick={() => handleSuggestionClick(suggestion)}
|
||||||
|
>
|
||||||
|
{suggestion}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PeriodSelectionPopover;
|
||||||
384
inventory/src/utils/naturalLanguagePeriod.ts
Normal file
384
inventory/src/utils/naturalLanguagePeriod.ts
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
export type MonthPeriod = {
|
||||||
|
type: "month";
|
||||||
|
startYear: number;
|
||||||
|
startMonth: number;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QuarterPeriod = {
|
||||||
|
type: "quarter";
|
||||||
|
startYear: number;
|
||||||
|
startQuarter: number;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type YearPeriod = {
|
||||||
|
type: "year";
|
||||||
|
startYear: number;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CustomPeriod = MonthPeriod | QuarterPeriod | YearPeriod;
|
||||||
|
|
||||||
|
export type NaturalLanguagePeriodResult = CustomPeriod | "last30days" | null;
|
||||||
|
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
"January",
|
||||||
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const QUARTER_NAMES = ["Q1", "Q2", "Q3", "Q4"] as const;
|
||||||
|
|
||||||
|
const MONTH_MAP: Record<string, number> = {
|
||||||
|
jan: 0,
|
||||||
|
january: 0,
|
||||||
|
feb: 1,
|
||||||
|
february: 1,
|
||||||
|
mar: 2,
|
||||||
|
march: 2,
|
||||||
|
apr: 3,
|
||||||
|
april: 3,
|
||||||
|
may: 4,
|
||||||
|
jun: 5,
|
||||||
|
june: 5,
|
||||||
|
jul: 6,
|
||||||
|
july: 6,
|
||||||
|
aug: 7,
|
||||||
|
august: 7,
|
||||||
|
sep: 8,
|
||||||
|
sept: 8,
|
||||||
|
september: 8,
|
||||||
|
oct: 9,
|
||||||
|
october: 9,
|
||||||
|
nov: 10,
|
||||||
|
november: 10,
|
||||||
|
dec: 11,
|
||||||
|
december: 11,
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripDelimiters = (value: string) => value.toLowerCase().trim().replace(/[,]/g, "");
|
||||||
|
|
||||||
|
const parseYear = (yearStr: string): number => {
|
||||||
|
const year = parseInt(yearStr, 10);
|
||||||
|
if (yearStr.length === 2) {
|
||||||
|
if (year <= 30) {
|
||||||
|
return 2000 + year;
|
||||||
|
}
|
||||||
|
return 1900 + year;
|
||||||
|
}
|
||||||
|
return year;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findMonth = (monthStr: string): number | null => {
|
||||||
|
const month = MONTH_MAP[monthStr.toLowerCase()];
|
||||||
|
return month ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseNaturalLanguagePeriod = (
|
||||||
|
input: string,
|
||||||
|
referenceDate: Date = new Date()
|
||||||
|
): NaturalLanguagePeriodResult => {
|
||||||
|
const lower = stripDelimiters(input);
|
||||||
|
const now = referenceDate;
|
||||||
|
const currentYear = now.getFullYear();
|
||||||
|
const currentMonth = now.getMonth();
|
||||||
|
const currentQuarter = Math.floor(currentMonth / 3);
|
||||||
|
|
||||||
|
if (/^(last\s*)?30\s*days?$/i.test(lower)) {
|
||||||
|
return "last30days";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^this\s+month$/i.test(lower)) {
|
||||||
|
return {
|
||||||
|
type: "month",
|
||||||
|
startYear: currentYear,
|
||||||
|
startMonth: currentMonth,
|
||||||
|
count: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^last\s+month$/i.test(lower)) {
|
||||||
|
const lastMonth = currentMonth === 0 ? 11 : currentMonth - 1;
|
||||||
|
const lastMonthYear = currentMonth === 0 ? currentYear - 1 : currentYear;
|
||||||
|
return {
|
||||||
|
type: "month",
|
||||||
|
startYear: lastMonthYear,
|
||||||
|
startMonth: lastMonth,
|
||||||
|
count: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^this\s+quarter$/i.test(lower)) {
|
||||||
|
return {
|
||||||
|
type: "quarter",
|
||||||
|
startYear: currentYear,
|
||||||
|
startQuarter: currentQuarter,
|
||||||
|
count: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^last\s+quarter$/i.test(lower)) {
|
||||||
|
const lastQuarter = currentQuarter === 0 ? 3 : currentQuarter - 1;
|
||||||
|
const lastQuarterYear = currentQuarter === 0 ? currentYear - 1 : currentYear;
|
||||||
|
return {
|
||||||
|
type: "quarter",
|
||||||
|
startYear: lastQuarterYear,
|
||||||
|
startQuarter: lastQuarter,
|
||||||
|
count: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^this\s+year$/i.test(lower)) {
|
||||||
|
return {
|
||||||
|
type: "year",
|
||||||
|
startYear: currentYear,
|
||||||
|
count: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^last\s+year$/i.test(lower)) {
|
||||||
|
return {
|
||||||
|
type: "year",
|
||||||
|
startYear: currentYear - 1,
|
||||||
|
count: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastMatch = lower.match(/^last\s+(\d+)\s*(months?|quarters?|years?|mos?|qtrs?|yrs?)$/i);
|
||||||
|
if (lastMatch) {
|
||||||
|
const count = parseInt(lastMatch[1], 10);
|
||||||
|
const unit = lastMatch[2].toLowerCase();
|
||||||
|
|
||||||
|
let type: "month" | "quarter" | "year";
|
||||||
|
if (/^(months?|mos?)$/i.test(unit)) {
|
||||||
|
type = "month";
|
||||||
|
} else if (/^(quarters?|qtrs?)$/i.test(unit)) {
|
||||||
|
type = "quarter";
|
||||||
|
} else {
|
||||||
|
type = "year";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "month") {
|
||||||
|
const startMonth = currentMonth - count + 1;
|
||||||
|
const startYear = currentYear + Math.floor(startMonth / 12);
|
||||||
|
return {
|
||||||
|
type: "month",
|
||||||
|
startYear,
|
||||||
|
startMonth: ((startMonth % 12) + 12) % 12,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "quarter") {
|
||||||
|
const startQuarter = currentQuarter - count + 1;
|
||||||
|
const startYear = currentYear + Math.floor(startQuarter / 4);
|
||||||
|
return {
|
||||||
|
type: "quarter",
|
||||||
|
startYear,
|
||||||
|
startQuarter: ((startQuarter % 4) + 4) % 4,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "year",
|
||||||
|
startYear: currentYear - count + 1,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const quarterMatch = lower.match(/^q([1-4])(?:\s+|-)(\d{2,4})$/);
|
||||||
|
if (quarterMatch) {
|
||||||
|
return {
|
||||||
|
type: "quarter",
|
||||||
|
startYear: parseYear(quarterMatch[2]),
|
||||||
|
startQuarter: parseInt(quarterMatch[1], 10) - 1,
|
||||||
|
count: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const quarterRangeMatch = lower.match(/^q([1-4])\s*(?:-|–|—|to|through|until|thru)\s*q([1-4])(?:\s+|-)(\d{2,4})$/);
|
||||||
|
if (quarterRangeMatch) {
|
||||||
|
const startQ = parseInt(quarterRangeMatch[1], 10) - 1;
|
||||||
|
const endQ = parseInt(quarterRangeMatch[2], 10) - 1;
|
||||||
|
const year = parseYear(quarterRangeMatch[3]);
|
||||||
|
|
||||||
|
let count = endQ - startQ + 1;
|
||||||
|
if (count <= 0) {
|
||||||
|
count += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "quarter",
|
||||||
|
startYear: year,
|
||||||
|
startQuarter: startQ,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const quarterRangeAcrossYearsMatch = lower.match(
|
||||||
|
/^q([1-4])(?:\s+|-)(\d{2,4})\s*(?:-|–|—|to|through|until|thru)\s*q([1-4])(?:\s+|-)(\d{2,4})$/
|
||||||
|
);
|
||||||
|
if (quarterRangeAcrossYearsMatch) {
|
||||||
|
const startQ = parseInt(quarterRangeAcrossYearsMatch[1], 10) - 1;
|
||||||
|
const startYear = parseYear(quarterRangeAcrossYearsMatch[2]);
|
||||||
|
const endQ = parseInt(quarterRangeAcrossYearsMatch[3], 10) - 1;
|
||||||
|
const endYear = parseYear(quarterRangeAcrossYearsMatch[4]);
|
||||||
|
|
||||||
|
const totalQuarters = (endYear - startYear) * 4 + (endQ - startQ) + 1;
|
||||||
|
|
||||||
|
if (totalQuarters > 0 && totalQuarters <= 20) {
|
||||||
|
return {
|
||||||
|
type: "quarter",
|
||||||
|
startYear,
|
||||||
|
startQuarter: startQ,
|
||||||
|
count: totalQuarters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearMatch = lower.match(/^(\d{2,4})$/);
|
||||||
|
if (yearMatch) {
|
||||||
|
return {
|
||||||
|
type: "year",
|
||||||
|
startYear: parseYear(yearMatch[1]),
|
||||||
|
count: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearRangeMatch = lower.match(/^(\d{2,4})\s*(?:-|–|—|to|through|until|thru)\s*(\d{2,4})$/);
|
||||||
|
if (yearRangeMatch) {
|
||||||
|
const startYear = parseYear(yearRangeMatch[1]);
|
||||||
|
const endYear = parseYear(yearRangeMatch[2]);
|
||||||
|
const count = endYear - startYear + 1;
|
||||||
|
|
||||||
|
if (count > 0 && count <= 10) {
|
||||||
|
return {
|
||||||
|
type: "year",
|
||||||
|
startYear,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleMonthMatch = lower.match(/^([a-z]+)(?:\s+|-)(\d{2,4})$/);
|
||||||
|
if (singleMonthMatch) {
|
||||||
|
const monthIndex = findMonth(singleMonthMatch[1]);
|
||||||
|
if (monthIndex !== null) {
|
||||||
|
return {
|
||||||
|
type: "month",
|
||||||
|
startYear: parseYear(singleMonthMatch[2]),
|
||||||
|
startMonth: monthIndex,
|
||||||
|
count: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthRangeWithYearMatch = lower.match(
|
||||||
|
/^([a-z]+)\s*(?:-|–|—|to|through|until|thru)\s*([a-z]+)(?:\s+|-)(\d{2,4})$/
|
||||||
|
);
|
||||||
|
if (monthRangeWithYearMatch) {
|
||||||
|
const startMonthIndex = findMonth(monthRangeWithYearMatch[1]);
|
||||||
|
const endMonthIndex = findMonth(monthRangeWithYearMatch[2]);
|
||||||
|
const year = parseYear(monthRangeWithYearMatch[3]);
|
||||||
|
|
||||||
|
if (startMonthIndex !== null && endMonthIndex !== null) {
|
||||||
|
let count = endMonthIndex - startMonthIndex + 1;
|
||||||
|
if (count <= 0) {
|
||||||
|
count += 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "month",
|
||||||
|
startYear: year,
|
||||||
|
startMonth: startMonthIndex,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthRangeAcrossYearsMatch = lower.match(
|
||||||
|
/^([a-z]+)(?:\s+|-)(\d{2,4})\s*(?:-|–|—|to|through|until|thru)\s*([a-z]+)(?:\s+|-)(\d{2,4})$/
|
||||||
|
);
|
||||||
|
if (monthRangeAcrossYearsMatch) {
|
||||||
|
const startMonthIndex = findMonth(monthRangeAcrossYearsMatch[1]);
|
||||||
|
const startYear = parseYear(monthRangeAcrossYearsMatch[2]);
|
||||||
|
const endMonthIndex = findMonth(monthRangeAcrossYearsMatch[3]);
|
||||||
|
const endYear = parseYear(monthRangeAcrossYearsMatch[4]);
|
||||||
|
|
||||||
|
if (startMonthIndex !== null && endMonthIndex !== null) {
|
||||||
|
const totalMonths = (endYear - startYear) * 12 + (endMonthIndex - startMonthIndex) + 1;
|
||||||
|
|
||||||
|
if (totalMonths > 0 && totalMonths <= 60) {
|
||||||
|
return {
|
||||||
|
type: "month",
|
||||||
|
startYear,
|
||||||
|
startMonth: startMonthIndex,
|
||||||
|
count: totalMonths,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateNaturalLanguagePreview = (
|
||||||
|
parsed: NaturalLanguagePeriodResult
|
||||||
|
): string | null => {
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed === "last30days") {
|
||||||
|
return "Last 30 days";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.type === "year") {
|
||||||
|
if (parsed.count === 1) {
|
||||||
|
return `${parsed.startYear}`;
|
||||||
|
}
|
||||||
|
const endYear = parsed.startYear + parsed.count - 1;
|
||||||
|
return `${parsed.startYear} to ${endYear}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.type === "quarter") {
|
||||||
|
if (parsed.count === 1) {
|
||||||
|
return `${QUARTER_NAMES[parsed.startQuarter]} ${parsed.startYear}`;
|
||||||
|
}
|
||||||
|
const endQuarter = (parsed.startQuarter + parsed.count - 1) % 4;
|
||||||
|
const endYear = parsed.startYear + Math.floor((parsed.startQuarter + parsed.count - 1) / 4);
|
||||||
|
|
||||||
|
if (parsed.startYear === endYear) {
|
||||||
|
return `${QUARTER_NAMES[parsed.startQuarter]} to ${QUARTER_NAMES[endQuarter]} ${parsed.startYear}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${QUARTER_NAMES[parsed.startQuarter]} ${parsed.startYear} to ${QUARTER_NAMES[endQuarter]} ${endYear}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.type === "month") {
|
||||||
|
if (parsed.count === 1) {
|
||||||
|
return `${MONTH_NAMES[parsed.startMonth]} ${parsed.startYear}`;
|
||||||
|
}
|
||||||
|
const endMonth = (parsed.startMonth + parsed.count - 1) % 12;
|
||||||
|
const endYear = parsed.startYear + Math.floor((parsed.startMonth + parsed.count - 1) / 12);
|
||||||
|
|
||||||
|
if (parsed.startYear === endYear) {
|
||||||
|
return `${MONTH_NAMES[parsed.startMonth]} to ${MONTH_NAMES[endMonth]} ${parsed.startYear}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${MONTH_NAMES[parsed.startMonth]} ${parsed.startYear} to ${MONTH_NAMES[endMonth]} ${endYear}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user