add horizontal calendar
This commit is contained in:
parent
d0efd133f2
commit
f4609eb8d1
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
10
apps/web/components/orders/orders-list/date-select.tsx
Normal file
10
apps/web/components/orders/orders-list/date-select.tsx
Normal 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} />;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
6
apps/web/stores/datetime/context.ts
Normal file
6
apps/web/stores/datetime/context.ts
Normal 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 };
|
||||
1
apps/web/stores/datetime/index.tsx
Normal file
1
apps/web/stores/datetime/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export * from './context';
|
||||
9
apps/web/stores/datetime/store.ts
Normal file
9
apps/web/stores/datetime/store.ts
Normal 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),
|
||||
}));
|
||||
}
|
||||
3
apps/web/stores/datetime/types.ts
Normal file
3
apps/web/stores/datetime/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { type DateTimeSlice } from '../lib/slices';
|
||||
|
||||
export type DateTimeStore = DateTimeSlice;
|
||||
@ -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);
|
||||
|
||||
@ -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: () => {
|
||||
|
||||
138
packages/ui/src/components/ui/horizontal-calendar.tsx
Normal file
138
packages/ui/src/components/ui/horizontal-calendar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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 */
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user