SearchDateRangePicker.jsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. "use client";
  2. import * as React from "react";
  3. import { Calendar as CalendarIcon, X } from "lucide-react";
  4. import { cn } from "@/lib/utils";
  5. import { Calendar } from "@/components/ui/calendar";
  6. import { Badge } from "@/components/ui/badge";
  7. import { Button } from "@/components/ui/button";
  8. import { Input } from "@/components/ui/input";
  9. import { Label } from "@/components/ui/label";
  10. import {
  11. Popover,
  12. PopoverContent,
  13. PopoverTrigger,
  14. } from "@/components/ui/popover";
  15. import { useSearchDateRangePicker } from "@/lib/frontend/search/useSearchDateRangePicker";
  16. export default function SearchDateRangePicker({
  17. from,
  18. to,
  19. onDateRangeChange,
  20. isSubmitting,
  21. className,
  22. }) {
  23. const {
  24. disabled,
  25. open,
  26. setOpen,
  27. activeField,
  28. setActiveField,
  29. fromRef,
  30. toRef,
  31. month,
  32. setMonth,
  33. summary,
  34. fromDisplay,
  35. toDisplay,
  36. presetsRow1,
  37. presetsRow2,
  38. calendarKey,
  39. calendarSelected,
  40. calendarModifiers,
  41. calendarModifiersClassNames,
  42. handlePickDay,
  43. handleClearFrom,
  44. handleClearTo,
  45. handleReset,
  46. applyPreset,
  47. } = useSearchDateRangePicker({
  48. from,
  49. to,
  50. onDateRangeChange,
  51. isSubmitting,
  52. });
  53. const activeInputClass =
  54. "border-blue-600 bg-blue-50 dark:border-blue-900 dark:bg-blue-950";
  55. return (
  56. <div className={cn("grid gap-2", className)}>
  57. <Label>Zeitraum</Label>
  58. <Popover open={open} onOpenChange={setOpen}>
  59. <PopoverTrigger asChild>
  60. <Button
  61. type="button"
  62. variant="outline"
  63. disabled={disabled}
  64. className={cn(
  65. "w-[240px] justify-between font-normal",
  66. !from && !to ? "text-muted-foreground" : ""
  67. )}
  68. title="Zeitraum auswählen"
  69. >
  70. <span className="truncate">{summary}</span>
  71. <CalendarIcon className="ml-2 h-4 w-4 opacity-70" />
  72. </Button>
  73. </PopoverTrigger>
  74. <PopoverContent align="start" className="w-fit p-0">
  75. <div className="w-fit space-y-4 p-4">
  76. <div className="grid grid-cols-2 gap-4">
  77. <div className="space-y-1">
  78. <Label>Von</Label>
  79. <div className="relative">
  80. <Input
  81. ref={fromRef}
  82. readOnly
  83. disabled={disabled}
  84. value={fromDisplay}
  85. placeholder="TT.MM.JJJJ"
  86. className={cn(
  87. "pr-8",
  88. activeField === "from" ? activeInputClass : ""
  89. )}
  90. onFocus={() => setActiveField("from")}
  91. onClick={() => setActiveField("from")}
  92. />
  93. {from ? (
  94. <button
  95. type="button"
  96. className="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-1 opacity-70 hover:opacity-100"
  97. onClick={handleClearFrom}
  98. aria-label="Startdatum löschen"
  99. title="Startdatum löschen"
  100. >
  101. <X className="h-4 w-4" />
  102. </button>
  103. ) : null}
  104. </div>
  105. </div>
  106. <div className="space-y-1">
  107. <Label>Bis</Label>
  108. <div className="relative">
  109. <Input
  110. ref={toRef}
  111. readOnly
  112. disabled={disabled}
  113. value={toDisplay}
  114. placeholder="TT.MM.JJJJ"
  115. className={cn(
  116. "pr-8",
  117. activeField === "to" ? activeInputClass : ""
  118. )}
  119. onFocus={() => setActiveField("to")}
  120. onClick={() => setActiveField("to")}
  121. />
  122. {to ? (
  123. <button
  124. type="button"
  125. className="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-1 opacity-70 hover:opacity-100"
  126. onClick={handleClearTo}
  127. aria-label="Enddatum löschen"
  128. title="Enddatum löschen"
  129. >
  130. <X className="h-4 w-4" />
  131. </button>
  132. ) : null}
  133. </div>
  134. </div>
  135. </div>
  136. <Calendar
  137. key={calendarKey}
  138. mode="range"
  139. numberOfMonths={2}
  140. captionLayout="dropdown"
  141. month={month}
  142. onMonthChange={setMonth}
  143. selected={calendarSelected}
  144. modifiers={calendarModifiers}
  145. modifiersClassNames={calendarModifiersClassNames}
  146. onDayClick={handlePickDay}
  147. />
  148. <div className="space-y-2">
  149. <div className="text-sm text-muted-foreground">Schnellwahl</div>
  150. <div className="flex flex-wrap gap-2">
  151. {presetsRow1.map((p) => (
  152. <Badge
  153. key={p.key}
  154. asChild
  155. className={[
  156. "bg-white text-black border-border",
  157. "hover:bg-white/90",
  158. "dark:bg-white dark:text-black",
  159. ].join(" ")}
  160. >
  161. <button
  162. type="button"
  163. className="cursor-pointer select-none disabled:opacity-60"
  164. disabled={disabled}
  165. onClick={() => applyPreset(p)}
  166. title={p.label}
  167. >
  168. {p.label}
  169. </button>
  170. </Badge>
  171. ))}
  172. </div>
  173. <div className="flex flex-wrap gap-2">
  174. {presetsRow2.map((p) => (
  175. <Badge key={p.key} asChild>
  176. <button
  177. type="button"
  178. className="cursor-pointer select-none disabled:opacity-60"
  179. disabled={disabled}
  180. onClick={() => applyPreset(p)}
  181. title={p.label}
  182. >
  183. {p.label}
  184. </button>
  185. </Badge>
  186. ))}
  187. </div>
  188. </div>
  189. <div className="flex justify-end">
  190. <Button
  191. type="button"
  192. variant="outline"
  193. disabled={disabled}
  194. onClick={handleReset}
  195. >
  196. Zurücksetzen
  197. </Button>
  198. </div>
  199. <p className="text-xs text-muted-foreground text-center">
  200. Tipp: Für einen einzelnen Tag setzen Sie <b>Von</b> und <b>Bis</b>{" "}
  201. auf dasselbe Datum.
  202. </p>
  203. </div>
  204. </PopoverContent>
  205. </Popover>
  206. </div>
  207. );
  208. }