feat(discount-codes): update DiscountCodeFormPage with date picker and single user selection

This commit is contained in:
hosseintaromi 2025-10-01 16:43:38 +08:00
parent f69e48dc0e
commit fbffc716ba
5 changed files with 369 additions and 45 deletions

32
package-lock.json generated
View File

@ -18,9 +18,11 @@
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.263.1", "lucide-react": "^0.263.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-date-object": "2.1.9",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.57.0", "react-hook-form": "^7.57.0",
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-multi-date-picker": "4.5.2",
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",
"recharts": "^2.8.0", "recharts": "^2.8.0",
"yup": "^1.6.1", "yup": "^1.6.1",
@ -5796,6 +5798,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-date-object": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.9.tgz",
"integrity": "sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==",
"license": "MIT"
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@ -5809,6 +5817,16 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-element-popper": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/react-element-popper/-/react-element-popper-2.1.7.tgz",
"integrity": "sha512-tuM2OxKlW32h+6uFSK6EENHPeZ2OGgOipHfOAl+VLWEv9/j3QkSGbD+ADX3A9uJlmq24i37n28RjJmAbGTfpEg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-hook-form": { "node_modules/react-hook-form": {
"version": "7.57.0", "version": "7.57.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz",
@ -5848,6 +5866,20 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-multi-date-picker": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/react-multi-date-picker/-/react-multi-date-picker-4.5.2.tgz",
"integrity": "sha512-FgWjZB3Z6IA6XpcWiLPk85PwcRUhOiYhKK42o5k672gD/n2I6rzPfQ8bUrldOIiF/Z7FfOCdH7a6FeubzqteLg==",
"license": "MIT",
"dependencies": {
"react-date-object": "^2.1.8",
"react-element-popper": "^2.1.6"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.17.0", "version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",

View File

@ -26,9 +26,11 @@
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.263.1", "lucide-react": "^0.263.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-date-object": "2.1.9",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.57.0", "react-hook-form": "^7.57.0",
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-multi-date-picker": "4.5.2",
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",
"recharts": "^2.8.0", "recharts": "^2.8.0",
"yup": "^1.6.1", "yup": "^1.6.1",

View File

@ -0,0 +1,75 @@
import React from 'react';
import DatePicker from 'react-multi-date-picker';
import TimePicker from 'react-multi-date-picker/plugins/time_picker';
import persian from 'react-date-object/calendars/persian';
import persian_fa from 'react-date-object/locales/persian_fa';
import DateObject from 'react-date-object';
import { Label } from './Typography';
interface JalaliDateTimePickerProps {
label?: string;
value?: string | null;
onChange: (value: string | undefined) => void;
error?: string;
placeholder?: string;
}
const toIsoLike = (date?: DateObject | null): string | undefined => {
if (!date) return undefined;
try {
const g = date.convert();
const yyyy = g.year.toString().padStart(4, '0');
const mm = g.month.toString().padStart(2, '0');
const dd = g.day.toString().padStart(2, '0');
const hh = g.hour.toString().padStart(2, '0');
const mi = g.minute.toString().padStart(2, '0');
return `${yyyy}-${mm}-${dd}T${hh}:${mi}:00Z`;
} catch {
return undefined;
}
};
const fromIsoToDateObject = (value?: string | null): DateObject | undefined => {
if (!value) return undefined;
try {
const d = new Date(value);
if (isNaN(d.getTime())) return undefined;
return new DateObject(d).convert(persian, persian_fa);
} catch {
return undefined;
}
};
export const JalaliDateTimePicker: React.FC<JalaliDateTimePickerProps> = ({ label, value, onChange, error, placeholder }) => {
const selected = fromIsoToDateObject(value);
return (
<div className="space-y-1">
{label && <Label>{label}</Label>}
<DatePicker
value={selected}
onChange={(val) => onChange(toIsoLike(val as DateObject | null))}
format="YYYY/MM/DD HH:mm"
calendar={persian}
locale={persian_fa}
calendarPosition="bottom-center"
disableDayPicker={false}
inputClass={`w-full border rounded-lg px-3 py-3 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 ${error ? 'border-red-300 focus:border-red-500 focus:ring-red-500' : 'border-gray-300 dark:border-gray-600 focus:border-primary-500 focus:ring-primary-500'}`}
containerClassName="w-full"
placeholder={placeholder || 'تاریخ و ساعت'}
editable={false}
plugins={[<TimePicker key="time" position="bottom" />]}
disableMonthPicker={false}
disableYearPicker={false}
showOtherDays
/>
{error && (
<p className="text-xs text-red-600 dark:text-red-400" role="alert">{error}</p>
)}
</div>
);
};
export default JalaliDateTimePicker;

View File

@ -0,0 +1,207 @@
import React, { useState, useRef, useEffect } from 'react';
import { ChevronDown, X } from 'lucide-react';
export interface Option {
id: number;
title: string;
description?: string;
}
interface SingleSelectAutocompleteProps {
options: Option[];
selectedValue?: number;
onChange: (value?: number) => void;
placeholder?: string;
label?: string;
error?: string;
isLoading?: boolean;
disabled?: boolean;
onSearchChange?: (query: string) => void;
onLoadMore?: () => void;
hasMore?: boolean;
loadingMore?: boolean;
}
export const SingleSelectAutocomplete: React.FC<SingleSelectAutocompleteProps> = ({
options,
selectedValue,
onChange,
placeholder = "انتخاب کنید...",
label,
error,
isLoading = false,
disabled = false,
onSearchChange,
onLoadMore,
hasMore = false,
loadingMore = false,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const dropdownRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const filteredOptions = options.filter(option =>
option.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
(option.description && option.description.toLowerCase().includes(searchTerm.toLowerCase()))
);
const displayedOptions = onSearchChange ? options : filteredOptions;
const selectedOption = options.find(option => option.id === selectedValue);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
setSearchTerm('');
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleSelectOption = (optionId: number) => {
onChange(optionId);
setIsOpen(false);
setSearchTerm('');
};
const handleClearSelection = (e: React.MouseEvent) => {
e.stopPropagation();
onChange(undefined);
};
const handleToggleDropdown = () => {
if (disabled) return;
setIsOpen(!isOpen);
if (!isOpen) {
setTimeout(() => inputRef.current?.focus(), 100);
}
};
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setSearchTerm(value);
if (onSearchChange) {
onSearchChange(value);
}
};
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
if (scrollHeight - scrollTop <= clientHeight + 5 && hasMore && !loadingMore && onLoadMore) {
onLoadMore();
}
};
return (
<div className="relative" ref={dropdownRef}>
{label && (
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{label}
</label>
)}
<div
className={`
w-full min-h-[42px] px-3 py-2 border rounded-md
focus-within:outline-none focus-within:ring-1 focus-within:ring-primary-500
cursor-pointer
${error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
${disabled ? 'bg-gray-100 cursor-not-allowed' : 'bg-white dark:bg-gray-700'}
dark:text-gray-100
`}
onClick={handleToggleDropdown}
>
<div className="flex items-center justify-between">
<div className="flex-1">
{selectedOption ? (
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-medium">{selectedOption.title}</span>
{selectedOption.description && (
<span className="text-xs text-gray-500 dark:text-gray-400 block">
{selectedOption.description}
</span>
)}
</div>
<button
type="button"
onClick={handleClearSelection}
className="ml-2 p-1 hover:bg-gray-200 dark:hover:bg-gray-600 rounded"
>
<X className="h-3 w-3" />
</button>
</div>
) : (
<span className="text-gray-500 dark:text-gray-400">{placeholder}</span>
)}
</div>
<ChevronDown className={`h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</div>
</div>
{isOpen && (
<div className="absolute z-50 w-full mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg">
<div className="p-2 border-b border-gray-200 dark:border-gray-600">
<input
ref={inputRef}
type="text"
value={searchTerm}
onChange={handleSearchChange}
placeholder="جستجو..."
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-600 dark:text-gray-100"
/>
</div>
<div
ref={listRef}
className="max-h-60 overflow-y-auto"
onScroll={handleScroll}
>
{isLoading && displayedOptions.length === 0 ? (
<div className="p-3 text-center text-gray-500 dark:text-gray-400">
در حال بارگذاری...
</div>
) : displayedOptions.length === 0 ? (
<div className="p-3 text-center text-gray-500 dark:text-gray-400">
موردی یافت نشد
</div>
) : (
<>
{displayedOptions.map((option) => (
<div
key={option.id}
className={`
p-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-600
${selectedValue === option.id ? 'bg-blue-50 dark:bg-blue-900' : ''}
`}
onClick={() => handleSelectOption(option.id)}
>
<div className="font-medium text-sm">{option.title}</div>
{option.description && (
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{option.description}
</div>
)}
</div>
))}
{loadingMore && (
<div className="p-3 text-center text-gray-500 dark:text-gray-400">
در حال بارگذاری بیشتر...
</div>
)}
</>
)}
</div>
</div>
)}
{error && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{error}</p>
)}
</div>
);
};

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useForm } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
import { parseFormattedNumber } from '@/utils/numberUtils'; import { parseFormattedNumber } from '@/utils/numberUtils';
@ -10,6 +10,8 @@ import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input"; import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { MultiSelectAutocomplete, Option } from "@/components/ui/MultiSelectAutocomplete"; import { MultiSelectAutocomplete, Option } from "@/components/ui/MultiSelectAutocomplete";
import { SingleSelectAutocomplete } from "@/components/ui/SingleSelectAutocomplete";
import { JalaliDateTimePicker } from "@/components/ui/JalaliDateTimePicker";
import { FormHeader, PageContainer, Label, SectionTitle } from '../../../components/ui/Typography'; import { FormHeader, PageContainer, Label, SectionTitle } from '../../../components/ui/Typography';
import { ArrowRight, BadgePercent, Calendar, Settings, Users, Tag, Info } from 'lucide-react'; import { ArrowRight, BadgePercent, Calendar, Settings, Users, Tag, Info } from 'lucide-react';
import { useUsers, useSearchUsers } from '../../users-admin/core/_hooks'; import { useUsers, useSearchUsers } from '../../users-admin/core/_hooks';
@ -29,7 +31,7 @@ const schema = yup.object({
.min(0.01, 'مقدار باید بیشتر از صفر باشد') .min(0.01, 'مقدار باید بیشتر از صفر باشد')
.max(999999, 'مقدار نباید بیشتر از ۹۹۹,۹۹۹ باشد'), .max(999999, 'مقدار نباید بیشتر از ۹۹۹,۹۹۹ باشد'),
status: yup.mixed<'active' | 'inactive'>().oneOf(['active', 'inactive']).required('وضعیت الزامی است'), status: yup.mixed<'active' | 'inactive'>().oneOf(['active', 'inactive']).required('وضعیت الزامی است'),
application_level: yup.array().of(yup.mixed<'invoice' | 'category' | 'product' | 'shipping' | 'product_fee'>().oneOf(['invoice', 'category', 'product', 'shipping', 'product_fee'])).min(1, 'حداقل یک سطح اعمال انتخاب کنید').required('سطح اعمال الزامی است'), application_level: yup.mixed<'invoice' | 'category' | 'product' | 'shipping' | 'product_fee'>().oneOf(['invoice', 'category', 'product', 'shipping', 'product_fee']).required('سطح اعمال الزامی است'),
min_purchase_amount: yup min_purchase_amount: yup
.number() .number()
.transform((val, original) => parseFormattedNumber(original) as any) .transform((val, original) => parseFormattedNumber(original) as any)
@ -77,10 +79,10 @@ const DiscountCodeFormPage = () => {
const { id } = useParams(); const { id } = useParams();
const isEdit = !!id; const isEdit = !!id;
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
const [selectedProductIds, setSelectedProductIds] = useState<number[]>([]); const [selectedProductIds, setSelectedProductIds] = useState<number[]>([]);
const [selectedCategoryIds, setSelectedCategoryIds] = useState<number[]>([]); const [selectedCategoryIds, setSelectedCategoryIds] = useState<number[]>([]);
const [selectedApplicationLevels, setSelectedApplicationLevels] = useState<string[]>(['invoice']); const [selectedApplicationLevel, setSelectedApplicationLevel] = useState<string>('invoice');
const [selectedUserId, setSelectedUserId] = useState<number | undefined>();
const { data: dc, isLoading: dcLoading } = useDiscountCode(id || ''); const { data: dc, isLoading: dcLoading } = useDiscountCode(id || '');
const { mutate: create, isPending: creating } = useCreateDiscountCode(); const { mutate: create, isPending: creating } = useCreateDiscountCode();
@ -177,10 +179,10 @@ const DiscountCodeFormPage = () => {
description: category.description || `دسته‌بندی #${category.id}` description: category.description || `دسته‌بندی #${category.id}`
})); }));
const { register, handleSubmit, formState: { errors, isValid }, reset, watch } = useForm<CreateDiscountCodeRequest>({ const { register, handleSubmit, control, formState: { errors, isValid }, reset, watch } = useForm<CreateDiscountCodeRequest>({
resolver: yupResolver(schema as any), resolver: yupResolver(schema as any),
mode: 'onChange', mode: 'onChange',
defaultValues: { status: 'active', type: 'percentage', application_level: ['invoice'], single_use: false } defaultValues: { status: 'active', type: 'percentage', application_level: 'invoice', single_use: false }
}); });
const applicationLevel = watch('application_level'); const applicationLevel = watch('application_level');
@ -206,17 +208,17 @@ const DiscountCodeFormPage = () => {
meta: dc.meta, meta: dc.meta,
}); });
// Set selected user IDs // Set selected user ID (first one if multiple)
if (dc.user_restrictions?.user_ids) { if (dc.user_restrictions?.user_ids && dc.user_restrictions.user_ids.length > 0) {
setSelectedUserIds(dc.user_restrictions.user_ids); setSelectedUserId(dc.user_restrictions.user_ids[0]);
} }
// Set selected application levels // Set selected application level
if (dc.application_level) { if (dc.application_level) {
setSelectedApplicationLevels( setSelectedApplicationLevel(
Array.isArray(dc.application_level) Array.isArray(dc.application_level)
? dc.application_level ? dc.application_level[0]
: [dc.application_level] : dc.application_level
); );
} }
@ -238,14 +240,12 @@ const DiscountCodeFormPage = () => {
const formData: CreateDiscountCodeRequest = { const formData: CreateDiscountCodeRequest = {
...data, ...data,
application_level: selectedApplicationLevels.length === 1 application_level: selectedApplicationLevel as any,
? selectedApplicationLevels[0] as any
: selectedApplicationLevels as any,
valid_from: toApiDateTime(data.valid_from), valid_from: toApiDateTime(data.valid_from),
valid_to: toApiDateTime(data.valid_to), valid_to: toApiDateTime(data.valid_to),
user_restrictions: selectedUserIds.length > 0 ? { user_restrictions: selectedUserId ? {
...cleanRestrictions, ...cleanRestrictions,
user_ids: selectedUserIds, user_ids: [selectedUserId],
} : cleanRestrictions.user_group ? cleanRestrictions : undefined, } : cleanRestrictions.user_group ? cleanRestrictions : undefined,
product_ids: selectedProductIds.length > 0 ? selectedProductIds : undefined, product_ids: selectedProductIds.length > 0 ? selectedProductIds : undefined,
category_ids: selectedCategoryIds.length > 0 ? selectedCategoryIds : undefined, category_ids: selectedCategoryIds.length > 0 ? selectedCategoryIds : undefined,
@ -414,7 +414,7 @@ const DiscountCodeFormPage = () => {
color: 'red' color: 'red'
} }
].map(option => { ].map(option => {
const isSelected = selectedApplicationLevels.includes(option.value); const isSelected = selectedApplicationLevel === option.value;
const getColorClasses = (color: string, selected: boolean) => { const getColorClasses = (color: string, selected: boolean) => {
const baseClasses = 'relative flex flex-col p-4 border-2 rounded-xl cursor-pointer transition-all duration-200 hover:shadow-lg transform hover:-translate-y-1'; const baseClasses = 'relative flex flex-col p-4 border-2 rounded-xl cursor-pointer transition-all duration-200 hover:shadow-lg transform hover:-translate-y-1';
@ -468,15 +468,11 @@ const DiscountCodeFormPage = () => {
className={getColorClasses(option.color, isSelected)} className={getColorClasses(option.color, isSelected)}
> >
<input <input
type="checkbox" type="radio"
name="application_level"
value={option.value}
checked={isSelected} checked={isSelected}
onChange={(e) => { onChange={() => setSelectedApplicationLevel(option.value)}
if (e.target.checked) {
setSelectedApplicationLevels(prev => [...prev, option.value]);
} else {
setSelectedApplicationLevels(prev => prev.filter(level => level !== option.value));
}
}}
className="sr-only" className="sr-only"
/> />
@ -511,7 +507,7 @@ const DiscountCodeFormPage = () => {
</div> </div>
{/* Conditional Product Selection */} {/* Conditional Product Selection */}
{selectedApplicationLevels.includes('product') && ( {selectedApplicationLevel === 'product' && (
<div className="lg:col-span-3"> <div className="lg:col-span-3">
<div className="bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-xl p-6 border border-purple-200 dark:border-purple-700"> <div className="bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-xl p-6 border border-purple-200 dark:border-purple-700">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
@ -553,7 +549,7 @@ const DiscountCodeFormPage = () => {
)} )}
{/* Conditional Category Selection */} {/* Conditional Category Selection */}
{selectedApplicationLevels.includes('category') && ( {selectedApplicationLevel === 'category' && (
<div className="lg:col-span-3"> <div className="lg:col-span-3">
<div className="bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-xl p-6 border border-green-200 dark:border-green-700"> <div className="bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-xl p-6 border border-green-200 dark:border-green-700">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
@ -656,17 +652,29 @@ const DiscountCodeFormPage = () => {
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Input <Controller
name="valid_from"
control={control}
render={({ field }) => (
<JalaliDateTimePicker
label="شروع اعتبار" label="شروع اعتبار"
type="datetime-local" value={field.value}
onChange={field.onChange}
error={errors.valid_from?.message as string} error={errors.valid_from?.message as string}
{...register('valid_from')}
/> />
<Input )}
/>
<Controller
name="valid_to"
control={control}
render={({ field }) => (
<JalaliDateTimePicker
label="پایان اعتبار" label="پایان اعتبار"
type="datetime-local" value={field.value}
onChange={field.onChange}
error={errors.valid_to?.message as string} error={errors.valid_to?.message as string}
{...register('valid_to')} />
)}
/> />
</div> </div>
</div> </div>
@ -700,12 +708,12 @@ const DiscountCodeFormPage = () => {
{/* User Selection */} {/* User Selection */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-6"> <div className="border-t border-gray-200 dark:border-gray-700 pt-6">
<MultiSelectAutocomplete <SingleSelectAutocomplete
label="انتخاب کاربران خاص" label="انتخاب کاربر خاص"
options={userOptions} options={userOptions}
selectedValues={selectedUserIds} selectedValue={selectedUserId}
onChange={setSelectedUserIds} onChange={setSelectedUserId}
placeholder="جستجو و انتخاب کاربران..." placeholder="جستجو و انتخاب کاربر..."
isLoading={usersLoading && userOffset === 0} isLoading={usersLoading && userOffset === 0}
disabled={false} disabled={false}
onSearchChange={(q) => { setUserSearch(q); setUserOffset(0); }} onSearchChange={(q) => { setUserSearch(q); setUserOffset(0); }}
@ -718,7 +726,7 @@ const DiscountCodeFormPage = () => {
loadingMore={usersLoading && userOffset > 0} loadingMore={usersLoading && userOffset > 0}
/> />
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2"> <p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
در صورت انتخاب کاربران، کد تخفیف فقط برای آنها قابل استفاده خواهد بود. در صورت انتخاب کاربر، کد تخفیف فقط برای آن کاربر قابل استفاده خواهد بود.
</p> </p>
</div> </div>
</div> </div>