feat(discount-codes): update DiscountCodeFormPage with date picker and single user selection
This commit is contained in:
parent
f69e48dc0e
commit
fbffc716ba
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue