Skip to content

Commit 57225db

Browse files
authored
Merge pull request #97 from DEEIX-AI/fix_admin_subscribe
refactor: enhance AdminDateTimePicker with date granularity
2 parents 3ddfd79 + 5067b12 commit 57225db

5 files changed

Lines changed: 109 additions & 151 deletions

File tree

frontend/features/admin/components/admin-date-time-picker.tsx

Lines changed: 67 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { format } from "date-fns";
44
import { enUS, zhCN } from "date-fns/locale";
55
import { CalendarIcon } from "lucide-react";
66
import { useLocale, useTranslations } from "next-intl";
7+
import type { Matcher } from "react-day-picker";
78

89
import { Button } from "@/components/ui/button";
910
import { Calendar } from "@/components/ui/calendar";
@@ -18,6 +19,8 @@ type AdminDateTimePickerProps = {
1819
label: string;
1920
placeholder: string;
2021
defaultTime?: string;
22+
granularity?: "date" | "datetime";
23+
disabledDate?: Matcher | Matcher[];
2124
onChange: (value: string) => void;
2225
};
2326

@@ -77,12 +80,15 @@ export function AdminDateTimePicker({
7780
label,
7881
placeholder,
7982
defaultTime = "23:59:59",
83+
granularity = "datetime",
84+
disabledDate,
8085
onChange,
8186
}: AdminDateTimePickerProps) {
8287
const locale = useLocale();
8388
const tDateRange = useTranslations("common.dateRange");
8489
const selectedDate = parseDatePart(value);
8590
const normalizedTime = selectedDate ? normalizeTimeValue(value, defaultTime) : "";
91+
const dateOnly = granularity === "date";
8692

8793
const handleTimePartChange = (index: number, nextPart: string) => {
8894
if (!selectedDate) return;
@@ -92,7 +98,7 @@ export function AdminDateTimePicker({
9298
return (
9399
<div className="space-y-1">
94100
<p className="text-xs text-muted-foreground">{label}</p>
95-
<div className="grid gap-5 md:grid-cols-2">
101+
<div className={cn("grid gap-5", !dateOnly && "md:grid-cols-2")}>
96102
<Popover>
97103
<PopoverTrigger asChild>
98104
<Button
@@ -116,53 +122,70 @@ export function AdminDateTimePicker({
116122
mode="single"
117123
selected={selectedDate}
118124
locale={locale === "zh-CN" ? zhCN : enUS}
119-
onSelect={(date) => onChange(date ? buildDateTimeValue(date, normalizeTimeValue(value, defaultTime), defaultTime) : "")}
125+
onSelect={(date) => onChange(date ? (dateOnly ? format(date, "yyyy-MM-dd") : buildDateTimeValue(date, normalizeTimeValue(value, defaultTime), defaultTime)) : "")}
126+
disabled={disabledDate}
120127
autoFocus
121128
/>
129+
{dateOnly && selectedDate ? (
130+
<div className="px-2 pb-2">
131+
<Button
132+
type="button"
133+
size="sm"
134+
variant="secondary"
135+
className="h-6 w-full text-xs"
136+
disabled={disabled}
137+
onClick={() => onChange("")}
138+
>
139+
{tDateRange("clear")}
140+
</Button>
141+
</div>
142+
) : null}
122143
</PopoverContent>
123144
</Popover>
124-
<div className="grid grid-cols-4 gap-2">
125-
<Input
126-
type="number"
127-
min={0}
128-
max={23}
129-
value={selectedDate ? timePart(normalizedTime, 0, defaultTime) : ""}
130-
placeholder="HH"
131-
disabled={disabled || !selectedDate}
132-
className="px-2 text-center tabular-nums"
133-
onChange={(event) => handleTimePartChange(0, event.target.value)}
134-
/>
135-
<Input
136-
type="number"
137-
min={0}
138-
max={59}
139-
value={selectedDate ? timePart(normalizedTime, 1, defaultTime) : ""}
140-
placeholder="MM"
141-
disabled={disabled || !selectedDate}
142-
className="px-2 text-center tabular-nums"
143-
onChange={(event) => handleTimePartChange(1, event.target.value)}
144-
/>
145-
<Input
146-
type="number"
147-
min={0}
148-
max={59}
149-
value={selectedDate ? timePart(normalizedTime, 2, defaultTime) : ""}
150-
placeholder="SS"
151-
disabled={disabled || !selectedDate}
152-
className="px-2 text-center tabular-nums"
153-
onChange={(event) => handleTimePartChange(2, event.target.value)}
154-
/>
155-
<Button
156-
type="button"
157-
variant="outline"
158-
className="h-8 w-full px-0 text-xs text-muted-foreground"
159-
disabled={disabled || !selectedDate}
160-
onClick={() => onChange("")}
161-
aria-label={tDateRange("clear")}
162-
>
163-
{tDateRange("clear")}
164-
</Button>
165-
</div>
145+
{dateOnly ? null : (
146+
<div className="grid grid-cols-4 gap-2">
147+
<Input
148+
type="number"
149+
min={0}
150+
max={23}
151+
value={selectedDate ? timePart(normalizedTime, 0, defaultTime) : ""}
152+
placeholder="HH"
153+
disabled={disabled || !selectedDate}
154+
className="px-2 text-center tabular-nums"
155+
onChange={(event) => handleTimePartChange(0, event.target.value)}
156+
/>
157+
<Input
158+
type="number"
159+
min={0}
160+
max={59}
161+
value={selectedDate ? timePart(normalizedTime, 1, defaultTime) : ""}
162+
placeholder="MM"
163+
disabled={disabled || !selectedDate}
164+
className="px-2 text-center tabular-nums"
165+
onChange={(event) => handleTimePartChange(1, event.target.value)}
166+
/>
167+
<Input
168+
type="number"
169+
min={0}
170+
max={59}
171+
value={selectedDate ? timePart(normalizedTime, 2, defaultTime) : ""}
172+
placeholder="SS"
173+
disabled={disabled || !selectedDate}
174+
className="px-2 text-center tabular-nums"
175+
onChange={(event) => handleTimePartChange(2, event.target.value)}
176+
/>
177+
<Button
178+
type="button"
179+
variant="outline"
180+
className="h-8 w-full px-0 text-xs text-muted-foreground"
181+
disabled={disabled || !selectedDate}
182+
onClick={() => onChange("")}
183+
aria-label={tDateRange("clear")}
184+
>
185+
{tDateRange("clear")}
186+
</Button>
187+
</div>
188+
)}
166189
</div>
167190
</div>
168191
);

frontend/features/admin/components/sections/accounts/account-user-editor.tsx

Lines changed: 29 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"use client";
22

33
import * as React from "react";
4-
import { format } from "date-fns";
5-
import { CalendarIcon } from "lucide-react";
64
import { motion } from "motion/react";
75
import { useLocale, useTranslations } from "next-intl";
86

@@ -33,10 +31,8 @@ import {
3331
DropdownMenuSeparator,
3432
DropdownMenuTrigger,
3533
} from "@/components/ui/dropdown-menu";
36-
import { Calendar } from "@/components/ui/calendar";
3734
import { Input } from "@/components/ui/input";
3835
import { Label } from "@/components/ui/label";
39-
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
4036
import {
4137
Sheet,
4238
SheetContent,
@@ -54,7 +50,7 @@ import {
5450
import { resolveAvatarImageSrc } from "@/shared/lib/avatar";
5551
import { TimeZoneSelect } from "@/shared/components/time-zone-select";
5652
import { cn } from "@/lib/utils";
57-
import { ADMIN_DATE_PICKER_TRIGGER_CLASSNAME } from "@/features/admin/components/admin-date-range-filter";
53+
import { AdminDateTimePicker } from "@/features/admin/components/admin-date-time-picker";
5854
import type { UserDTO } from "@/shared/api/auth.types";
5955
import type { AdminUserRole, AdminUserStatus } from "@/features/admin/api/admin.types";
6056
import {
@@ -134,7 +130,6 @@ type CreateUserDialogProps = {
134130
username: string;
135131
displayName: string;
136132
};
137-
createSubscriptionExpiryDate?: Date;
138133
onOpenCreateAvatarDialog: () => void;
139134
onCreateSubmit: React.FormEventHandler<HTMLFormElement>;
140135
resolveCreateUserInitial: (username: string, displayName: string) => string;
@@ -150,7 +145,6 @@ export function CreateUserDialog({
150145
billingMode,
151146
billingPlans,
152147
createAvatarSource,
153-
createSubscriptionExpiryDate,
154148
onOpenCreateAvatarDialog,
155149
onCreateSubmit,
156150
resolveCreateUserInitial,
@@ -252,40 +246,20 @@ export function CreateUserDialog({
252246
</div>
253247

254248
<DialogCollapsible open={createPayload.subscriptionTier !== "free"}>
255-
<div className="space-y-1 pt-0.5">
256-
<p className="text-xs text-muted-foreground">{t("editor.expiryTime")}</p>
257-
<Popover>
258-
<PopoverTrigger asChild>
259-
<Button
260-
type="button"
261-
variant="outline"
262-
className={cn(
263-
ADMIN_DATE_PICKER_TRIGGER_CLASSNAME,
264-
"justify-between",
265-
!createSubscriptionExpiryDate && "text-muted-foreground",
266-
)}
267-
disabled={createPayload.subscriptionTier === "free"}
268-
>
269-
{createSubscriptionExpiryDate ? format(createSubscriptionExpiryDate, "yyyy-MM-dd") : t("editor.selectExpiryDate")}
270-
<CalendarIcon className="size-3.5 opacity-70" />
271-
</Button>
272-
</PopoverTrigger>
273-
<PopoverContent className="w-auto p-0" align="start">
274-
<Calendar
275-
mode="single"
276-
selected={createSubscriptionExpiryDate}
277-
onSelect={(date) =>
278-
setCreatePayload((current) => ({
279-
...current,
280-
subscriptionExpiresAt: date ? format(date, "yyyy-MM-dd") : "",
281-
}))
282-
}
283-
disabled={{ before: new Date() }}
284-
autoFocus
285-
/>
286-
</PopoverContent>
287-
</Popover>
288-
</div>
249+
<AdminDateTimePicker
250+
value={createPayload.subscriptionExpiresAt}
251+
label={t("editor.expiryTime")}
252+
placeholder={t("editor.selectExpiryDate")}
253+
granularity="date"
254+
disabled={createPayload.subscriptionTier === "free"}
255+
disabledDate={{ before: new Date() }}
256+
onChange={(value) =>
257+
setCreatePayload((current) => ({
258+
...current,
259+
subscriptionExpiresAt: value,
260+
}))
261+
}
262+
/>
289263
</DialogCollapsible>
290264
</div>
291265
) : null}
@@ -313,7 +287,6 @@ type EditUserSheetProps = {
313287
setEditPayload: React.Dispatch<React.SetStateAction<EditUserPayload>>;
314288
billingMode: AdminBillingMode;
315289
billingPlans: AdminBillingPlanDTO[];
316-
editSubscriptionExpiryDate?: Date;
317290
statusChanged: boolean;
318291
timeZoneOptions: string[];
319292
roleOptions: AdminUserRole[];
@@ -380,7 +353,6 @@ export function EditUserSheet({
380353
setEditPayload,
381354
billingMode,
382355
billingPlans,
383-
editSubscriptionExpiryDate,
384356
statusChanged,
385357
timeZoneOptions,
386358
roleOptions,
@@ -638,40 +610,20 @@ export function EditUserSheet({
638610
</Combobox>
639611
</div>
640612
<DialogCollapsible open={editPayload.subscriptionTier !== "free"}>
641-
<div className="space-y-1">
642-
<Label className="text-xs text-muted-foreground">{t("editor.expiryTime")}</Label>
643-
<Popover>
644-
<PopoverTrigger asChild>
645-
<Button
646-
type="button"
647-
variant="outline"
648-
className={cn(
649-
ADMIN_DATE_PICKER_TRIGGER_CLASSNAME,
650-
"justify-between",
651-
!editSubscriptionExpiryDate && "text-muted-foreground",
652-
)}
653-
disabled={pending || editPayload.subscriptionTier === "free"}
654-
>
655-
{editSubscriptionExpiryDate ? format(editSubscriptionExpiryDate, "yyyy-MM-dd") : t("editor.selectExpiryDate")}
656-
<CalendarIcon className="size-3.5 opacity-70" />
657-
</Button>
658-
</PopoverTrigger>
659-
<PopoverContent className="w-auto p-0" align="start">
660-
<Calendar
661-
mode="single"
662-
selected={editSubscriptionExpiryDate}
663-
onSelect={(date) =>
664-
setEditPayload((current) => ({
665-
...current,
666-
subscriptionExpiresAt: date ? format(date, "yyyy-MM-dd") : "",
667-
}))
668-
}
669-
disabled={{ before: new Date() }}
670-
autoFocus
671-
/>
672-
</PopoverContent>
673-
</Popover>
674-
</div>
613+
<AdminDateTimePicker
614+
value={editPayload.subscriptionExpiresAt}
615+
label={t("editor.expiryTime")}
616+
placeholder={t("editor.selectExpiryDate")}
617+
granularity="date"
618+
disabled={pending || editPayload.subscriptionTier === "free"}
619+
disabledDate={{ before: new Date() }}
620+
onChange={(value) =>
621+
setEditPayload((current) => ({
622+
...current,
623+
subscriptionExpiresAt: value,
624+
}))
625+
}
626+
/>
675627
</DialogCollapsible>
676628
</div>
677629
) : null}

frontend/features/admin/components/sections/accounts/accounts-users.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -448,8 +448,6 @@ export function AccountsUsers({
448448
billingPlans,
449449
createAvatarSource,
450450
avatarDialogPreviewSrc,
451-
createSubscriptionExpiryDate,
452-
editSubscriptionExpiryDate,
453451
editStatusChanged,
454452
pageCount,
455453
filteredItems,
@@ -806,7 +804,6 @@ export function AccountsUsers({
806804
billingMode={billingMode}
807805
billingPlans={billingPlans}
808806
createAvatarSource={createAvatarSource}
809-
createSubscriptionExpiryDate={createSubscriptionExpiryDate}
810807
onOpenCreateAvatarDialog={handleOpenCreateAvatarDialog}
811808
onCreateSubmit={onCreateUser}
812809
resolveCreateUserInitial={resolveCreateUserInitial}
@@ -827,7 +824,6 @@ export function AccountsUsers({
827824
setEditPayload={setEditPayload}
828825
billingMode={billingMode}
829826
billingPlans={billingPlans}
830-
editSubscriptionExpiryDate={editSubscriptionExpiryDate}
831827
statusChanged={editStatusChanged}
832828
timeZoneOptions={timeZoneOptions}
833829
roleOptions={roleOptions}

0 commit comments

Comments
 (0)