add horizontal calendar

This commit is contained in:
vchikalkin 2025-06-08 14:32:01 +03:00
parent d0efd133f2
commit f4609eb8d1
12 changed files with 233 additions and 23 deletions

View File

@ -1,17 +1,18 @@
'use client';
import { Container } from '@/components/layout';
import { ClientsOrdersList, OrdersList } from '@/components/orders';
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
export default async function ProfilePage() {
const queryClient = new QueryClient();
import { DateSelect } from '@/components/orders/orders-list/date-select';
import { DateTimeStoreProvider } from '@/stores/datetime';
export default function ProfilePage() {
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Container>
<Container>
<DateTimeStoreProvider>
<div />
<DateSelect />
<ClientsOrdersList />
<OrdersList />
</Container>
</HydrationBoundary>
</DateTimeStoreProvider>
</Container>
);
}

View File

@ -0,0 +1,10 @@
'use client';
import { useDateTimeStore } from '@/stores/datetime';
import { HorizontalCalendar } from '@repo/ui/components/ui/horizontal-calendar';
export function DateSelect() {
const setSelectedDate = useDateTimeStore((store) => store.setDate);
const selectedDate = useDateTimeStore((store) => store.date);
return <HorizontalCalendar onDateChange={setSelectedDate} selectedDate={selectedDate} />;
}

View File

@ -4,6 +4,7 @@
import { OrderCard } from '@/components/shared/order-card';
import { useCustomerQuery } from '@/hooks/api/customers';
import { useOrdersQuery } from '@/hooks/api/orders';
import { useDateTimeStore } from '@/stores/datetime';
import { Enum_Customer_Role } from '@repo/graphql/types';
export function ClientsOrdersList() {
@ -11,17 +12,25 @@ export function ClientsOrdersList() {
const isMaster = customer?.role === Enum_Customer_Role.Master;
const { data: { orders } = {}, isLoading } = useOrdersQuery({
filters: {
slot: {
master: {
documentId: {
eq: isMaster ? customer?.documentId : undefined,
const selectedDate = useDateTimeStore((store) => store.date);
const { data: { orders } = {}, isLoading } = useOrdersQuery(
{
filters: {
slot: {
date: {
eq: selectedDate,
},
master: {
documentId: {
eq: isMaster ? customer?.documentId : undefined,
},
},
},
},
},
});
Boolean(customer?.documentId) && Boolean(selectedDate),
);
if (!orders?.length || isLoading) return null;
@ -36,15 +45,25 @@ export function ClientsOrdersList() {
export function OrdersList() {
const { data: { customer } = {} } = useCustomerQuery();
const { data: { orders } = {}, isLoading } = useOrdersQuery({
filters: {
client: {
documentId: {
eq: customer?.documentId,
const selectedDate = useDateTimeStore((store) => store.date);
const { data: { orders } = {}, isLoading } = useOrdersQuery(
{
filters: {
client: {
documentId: {
eq: customer?.documentId,
},
},
slot: {
date: {
eq: selectedDate,
},
},
},
},
});
Boolean(customer?.documentId) && Boolean(selectedDate),
);
if (!orders?.length || isLoading) return null;

View File

@ -21,8 +21,9 @@ export const useOrderCreate = () => {
});
};
export const useOrdersQuery = (variables: Parameters<typeof getOrders>[0]) =>
export const useOrdersQuery = (variables: Parameters<typeof getOrders>[0], enabled?: boolean) =>
useQuery({
enabled,
queryFn: () => getOrders(variables),
queryKey: ['orders', variables],
staleTime: 60 * 1_000,

View File

@ -0,0 +1,6 @@
import { createDateTimeStore } from './store';
import { createZustandStore } from '@/utils/zustand/context';
const { Provider, useZustandStore } = createZustandStore(createDateTimeStore);
export { Provider as DateTimeStoreProvider, useZustandStore as useDateTimeStore };

View File

@ -0,0 +1 @@
export * from './context';

View File

@ -0,0 +1,9 @@
import { createDateTimeSlice } from '../lib/slices';
import { type DateTimeStore } from './types';
import { createStore } from 'zustand';
export function createDateTimeStore() {
return createStore<DateTimeStore>((...args) => ({
...createDateTimeSlice(...args),
}));
}

View File

@ -0,0 +1,3 @@
import { type DateTimeSlice } from '../lib/slices';
export type DateTimeStore = DateTimeSlice;

View File

@ -107,7 +107,16 @@ export class OrdersService extends BaseService {
const result = await query({
query: GQL.GetOrdersDocument,
variables,
variables: {
filters: {
...variables.filters,
date: { eq: formatDate(variables?.filters?.date?.eq).db() },
slot: {
...variables.filters?.slot,
date: { eq: formatDate(variables?.filters?.slot?.date?.eq).db() },
},
},
},
});
if (result.error) throw new Error(result.error.message);

View File

@ -15,6 +15,8 @@ export function combineDateTime(date: Date, time: string) {
}
export function formatDate(date: Date | string) {
if (!date) return { db: () => undefined, user: () => undefined };
return {
db: () => dayjs(date).format('YYYY-MM-DD'),
user: () => {

View File

@ -0,0 +1,138 @@
'use client';
import { cn } from '../../lib/utils';
import { Button } from '../ui/button';
import { addDays, format, isSameDay, subDays } from 'date-fns';
import { ru } from 'date-fns/locale';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
type HorizontalCalendarProps = {
readonly className?: string;
readonly numberOfDays?: number;
readonly onDateChange: (date: Date) => void;
readonly selectedDate: Date;
};
export function HorizontalCalendar({
className,
numberOfDays = 14,
onDateChange,
selectedDate,
}: HorizontalCalendarProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const [baseDate, setBaseDate] = useState(new Date());
const [currentMonthDate, setCurrentMonthDate] = useState(new Date());
const dates = useMemo(() => {
return Array.from({ length: numberOfDays }, (_, index) => {
return addDays(baseDate, index);
});
}, [baseDate, numberOfDays]);
const updateCurrentMonth = (newBaseDate: Date) => {
setBaseDate(newBaseDate);
setCurrentMonthDate(newBaseDate);
setTimeout(() => {
if (scrollRef.current) {
scrollRef.current.scrollLeft = 0;
}
}, 0);
};
const scrollPrevious = () => {
const newDate = subDays(baseDate, numberOfDays);
updateCurrentMonth(newDate);
};
const scrollNext = () => {
const newDate = addDays(baseDate, numberOfDays);
updateCurrentMonth(newDate);
};
// Обработчик scroll — вычисляет дату ближайшую к левому краю
const handleScroll = () => {
const container = scrollRef.current;
if (!container) return;
const children = Array.from(container.children) as HTMLElement[];
let closestChild: HTMLElement | null = null;
let minOffset = Infinity;
for (const child of children) {
const offset = Math.abs(child.offsetLeft - container.scrollLeft);
if (offset < minOffset) {
minOffset = offset;
closestChild = child;
}
}
if (closestChild) {
const dateString = closestChild.dataset.date;
if (dateString) {
const date = new Date(dateString);
setCurrentMonthDate(date);
}
}
};
useEffect(() => {
const scrollElement = scrollRef.current;
if (scrollElement) {
scrollElement.addEventListener('scroll', handleScroll, { passive: true });
}
return () => {
if (scrollElement) {
scrollElement.removeEventListener('scroll', handleScroll);
}
};
}, []);
return (
<div className={cn('w-full', className)}>
<div className="mb-2 flex items-center justify-between">
<h2 className="text-lg font-medium">
{format(currentMonthDate, 'LLLL yyyy', { locale: ru })}
</h2>
<div className="flex space-x-2">
<Button
aria-label="Предыдущий месяц"
onClick={scrollPrevious}
size="icon"
variant="outline"
>
<ChevronLeft className="size-4" />
</Button>
<Button aria-label="Следующий месяц" onClick={scrollNext} size="icon" variant="outline">
<ChevronRight className="size-4" />
</Button>
</div>
</div>
<div className="relative">
<div className="scrollbar-hide flex snap-x overflow-x-auto pb-2" ref={scrollRef}>
{dates.map((date) => (
<div
className="shrink-0 snap-start px-1 first:pl-0 last:pr-0"
data-date={date.toISOString()}
key={date.toISOString()}
>
<Button
className={cn(
'w-14 flex-col h-auto py-2 px-0',
isSameDay(date, selectedDate) && 'bg-primary text-primary-foreground',
)}
onClick={() => onDateChange(date)}
variant={isSameDay(date, selectedDate) ? 'default' : 'outline'}
>
<span className="text-xs font-normal">{format(date, 'EEE', { locale: ru })}</span>
<span className="text-lg">{format(date, 'd')}</span>
</Button>
</div>
))}
</div>
</div>
</div>
);
}

View File

@ -69,3 +69,14 @@
@apply bg-background text-foreground;
}
}
/* Hide scrollbar for Chrome, Safari and Opera */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}