feat: accounting added
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { DayPicker } from '@daypicker/persian';
|
||||
import '@daypicker/react/style.css';
|
||||
import { CalendarDaysIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
interface PersianDatePickerProps {
|
||||
/** Gregorian YYYY-MM-DD, or '' when empty. */
|
||||
value: string;
|
||||
/** Called with the selected Gregorian YYYY-MM-DD, or '' when cleared. */
|
||||
onChange: (value: string) => void;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
// Parses a Gregorian "YYYY-MM-DD" into a local Date (pinned at noon to avoid
|
||||
// any date shift from timezone offsets).
|
||||
const parseGregorian = (value: string): Date | null => {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||||
if (!match) return null;
|
||||
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), 12, 0, 0);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
};
|
||||
|
||||
const toGregorianValue = (date: Date): string => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// Renders the given Gregorian date as a Persian (Jalali) string with Persian
|
||||
// digits, e.g. "۱۴۰۴/۰۵/۱۱".
|
||||
const jalaliFormat = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
});
|
||||
|
||||
const formatJalali = (date: Date): string => jalaliFormat.format(date);
|
||||
|
||||
/**
|
||||
* A Persian (Jalali) date input built on @daypicker/persian.
|
||||
*
|
||||
* The calendar operates in the Jalali calendar, but the public value is a
|
||||
* Gregorian "YYYY-MM-DD" string — the same format the native <input
|
||||
* type="date"> produced — so the accounting filters keep working unchanged.
|
||||
*
|
||||
* Note: with the default dateLib (no `timeZone` prop), @daypicker/persian
|
||||
* works with plain JS `Date` objects whose native getters already return the
|
||||
* Gregorian date; the Jalali conversion happens inside the calendar library.
|
||||
* That's why we can read `getFullYear()/getMonth()/getDate()` straight off the
|
||||
* picked date.
|
||||
*/
|
||||
export default function PersianDatePicker({
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
placeholder,
|
||||
id,
|
||||
}: PersianDatePickerProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const selectedDate = value ? parseGregorian(value) : null;
|
||||
|
||||
// Controlled month so the popover opens on the selected month (or now).
|
||||
const [viewMonth, setViewMonth] = useState<Date>(() => selectedDate ?? new Date());
|
||||
|
||||
// Close on outside click or Escape.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handlePointerDown = (event: MouseEvent) => {
|
||||
if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handlePointerDown);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handlePointerDown);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const handleSelect = (date: Date | undefined) => {
|
||||
if (date) onChange(toGregorianValue(date));
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
onChange('');
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const toggle = () => {
|
||||
setViewMonth(selectedDate ?? new Date());
|
||||
setOpen((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="relative">
|
||||
{label && (
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
id={id}
|
||||
onClick={toggle}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
className={`flex w-full items-center justify-between gap-2 px-3 py-2.5 text-right text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent sm:text-sm dark:bg-gray-800/80 transition-colors ${
|
||||
selectedDate
|
||||
? 'text-gray-900 dark:text-gray-100'
|
||||
: 'text-gray-400 dark:text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<span className="truncate">{selectedDate ? formatJalali(selectedDate) : placeholder || 'انتخاب تاریخ'}</span>
|
||||
{selectedDate ? (
|
||||
<XMarkIcon
|
||||
className="h-4 w-4 shrink-0 text-gray-400 hover:text-red-500 transition-colors"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleClear();
|
||||
}}
|
||||
aria-label="پاک کردن تاریخ"
|
||||
/>
|
||||
) : (
|
||||
<CalendarDaysIcon className="h-4 w-4 shrink-0 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute right-0 z-50 mt-2 bg-white dark:bg-gray-900 ring-1 ring-gray-200 dark:ring-gray-800 rounded-2xl shadow-xl p-2">
|
||||
<DayPicker
|
||||
mode="single"
|
||||
selected={selectedDate ?? undefined}
|
||||
onSelect={handleSelect}
|
||||
month={viewMonth}
|
||||
onMonthChange={setViewMonth}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
PowerIcon,
|
||||
ShoppingBagIcon,
|
||||
HomeIcon,
|
||||
CalculatorIcon,
|
||||
Bars3Icon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
@@ -23,6 +24,7 @@ const navItems = [
|
||||
{ href: '/admin/dashboard', label: 'داشبورد', Icon: HomeIcon },
|
||||
{ href: '/admin/users', label: 'کاربران', Icon: UsersIcon },
|
||||
{ href: '/admin/purchases', label: 'خریدها', Icon: ShoppingBagIcon },
|
||||
{ href: '/admin/accounting', label: 'حسابداری', Icon: CalculatorIcon },
|
||||
{ href: '/admin/packages', label: 'پکیجها', Icon: CubeIcon },
|
||||
{ href: '/admin/products', label: 'محصولات', Icon: TagIcon },
|
||||
{ href: '/admin/reminders', label: 'یادآوریها', Icon: BellIcon },
|
||||
|
||||
Reference in New Issue
Block a user