@tuturuuu/ui 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.checksum +1 -0
- package/README.md +46 -0
- package/components.json +20 -0
- package/eslint.config.mjs +20 -0
- package/jsr.json +10 -0
- package/package.json +120 -0
- package/postcss.config.mjs +8 -0
- package/rollup.config.js +40 -0
- package/src/components/ui/accordion.tsx +70 -0
- package/src/components/ui/alert-dialog.tsx +156 -0
- package/src/components/ui/alert.tsx +58 -0
- package/src/components/ui/aspect-ratio.tsx +11 -0
- package/src/components/ui/avatar.tsx +52 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/breadcrumb.tsx +108 -0
- package/src/components/ui/button.tsx +61 -0
- package/src/components/ui/calendar.tsx +212 -0
- package/src/components/ui/card.tsx +74 -0
- package/src/components/ui/carousel.tsx +240 -0
- package/src/components/ui/chart.tsx +365 -0
- package/src/components/ui/checkbox.tsx +31 -0
- package/src/components/ui/codeblock.tsx +161 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/color-picker.tsx +143 -0
- package/src/components/ui/command.tsx +176 -0
- package/src/components/ui/context-menu.tsx +251 -0
- package/src/components/ui/custom/autosize-textarea.tsx +111 -0
- package/src/components/ui/custom/calendar/core.tsx +61 -0
- package/src/components/ui/custom/calendar/day-cell.tsx +74 -0
- package/src/components/ui/custom/calendar/month-header.tsx +59 -0
- package/src/components/ui/custom/calendar/month-view.tsx +110 -0
- package/src/components/ui/custom/calendar/utils.ts +76 -0
- package/src/components/ui/custom/calendar/year-calendar.tsx +64 -0
- package/src/components/ui/custom/calendar/year-view.tsx +58 -0
- package/src/components/ui/custom/combobox.tsx +197 -0
- package/src/components/ui/custom/common-footer.tsx +215 -0
- package/src/components/ui/custom/compared-date-range-picker.tsx +561 -0
- package/src/components/ui/custom/date-input.tsx +279 -0
- package/src/components/ui/custom/empty-card.tsx +39 -0
- package/src/components/ui/custom/feature-summary.tsx +135 -0
- package/src/components/ui/custom/file-uploader.tsx +349 -0
- package/src/components/ui/custom/input-field.tsx +29 -0
- package/src/components/ui/custom/loading-indicator.tsx +28 -0
- package/src/components/ui/custom/modifiable-dialog-trigger.tsx +83 -0
- package/src/components/ui/custom/month-picker.tsx +157 -0
- package/src/components/ui/custom/report-preview.tsx +175 -0
- package/src/components/ui/custom/search-bar.tsx +56 -0
- package/src/components/ui/custom/select-field.tsx +78 -0
- package/src/components/ui/custom/tables/data-table-column-header.tsx +72 -0
- package/src/components/ui/custom/tables/data-table-create-button.tsx +31 -0
- package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +142 -0
- package/src/components/ui/custom/tables/data-table-pagination.tsx +243 -0
- package/src/components/ui/custom/tables/data-table-refresh-button.tsx +45 -0
- package/src/components/ui/custom/tables/data-table-toolbar.tsx +133 -0
- package/src/components/ui/custom/tables/data-table-view-options.tsx +112 -0
- package/src/components/ui/custom/tables/data-table.tsx +228 -0
- package/src/components/ui/custom/uploaded-files-card.tsx +50 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/drawer.tsx +131 -0
- package/src/components/ui/dropdown-menu.tsx +256 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/hover-card.tsx +41 -0
- package/src/components/ui/icons.tsx +506 -0
- package/src/components/ui/input-otp.tsx +78 -0
- package/src/components/ui/input.tsx +18 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/markdown.tsx +7 -0
- package/src/components/ui/menubar.tsx +275 -0
- package/src/components/ui/navigation-menu.tsx +169 -0
- package/src/components/ui/pagination.tsx +126 -0
- package/src/components/ui/popover.tsx +47 -0
- package/src/components/ui/progress.tsx +30 -0
- package/src/components/ui/radio-group.tsx +44 -0
- package/src/components/ui/resizable.tsx +55 -0
- package/src/components/ui/scroll-area.tsx +57 -0
- package/src/components/ui/select.tsx +180 -0
- package/src/components/ui/separator.tsx +27 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +734 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +62 -0
- package/src/components/ui/sonner.tsx +29 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +112 -0
- package/src/components/ui/tabs.tsx +68 -0
- package/src/components/ui/tag-input.tsx +141 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/time-picker-input.tsx +117 -0
- package/src/components/ui/time-picker-utils.tsx +146 -0
- package/src/components/ui/toast.tsx +128 -0
- package/src/components/ui/toaster.tsx +35 -0
- package/src/components/ui/toggle-group.tsx +72 -0
- package/src/components/ui/toggle.tsx +46 -0
- package/src/components/ui/tooltip.tsx +60 -0
- package/src/globals.css +252 -0
- package/src/hooks/use-callback-ref.ts +28 -0
- package/src/hooks/use-controllable-state.ts +68 -0
- package/src/hooks/use-copy-to-clipboard.ts +46 -0
- package/src/hooks/use-form.ts +23 -0
- package/src/hooks/use-forwarded-ref.ts +17 -0
- package/src/hooks/use-mobile.tsx +21 -0
- package/src/hooks/use-toast.ts +191 -0
- package/src/resolvers.ts +3 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { MonthView } from './month-view';
|
|
4
|
+
import { WorkspaceUserAttendance } from './utils';
|
|
5
|
+
import { YearView } from './year-view';
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
interface CalendarProps {
|
|
9
|
+
locale: string;
|
|
10
|
+
initialDate?: Date;
|
|
11
|
+
attendanceData?: WorkspaceUserAttendance[];
|
|
12
|
+
// eslint-disable-next-line no-unused-vars
|
|
13
|
+
onDateClick?: (date: Date) => void;
|
|
14
|
+
hideControls?: boolean;
|
|
15
|
+
hideYear?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Calendar: React.FC<CalendarProps> = ({
|
|
19
|
+
locale,
|
|
20
|
+
initialDate,
|
|
21
|
+
attendanceData,
|
|
22
|
+
onDateClick,
|
|
23
|
+
hideControls = false,
|
|
24
|
+
hideYear = false,
|
|
25
|
+
}) => {
|
|
26
|
+
const [currentDate, setCurrentDate] = useState(initialDate || new Date());
|
|
27
|
+
const [viewMode, setViewMode] = useState<'month' | 'year'>('month');
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setCurrentDate(initialDate || new Date());
|
|
31
|
+
}, [initialDate]);
|
|
32
|
+
|
|
33
|
+
const handleMonthClick = (month: number) => {
|
|
34
|
+
setCurrentDate(new Date(currentDate.setMonth(month)));
|
|
35
|
+
setViewMode('month');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleYearViewClick = () => {
|
|
39
|
+
setViewMode('year');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return viewMode === 'month' ? (
|
|
43
|
+
<MonthView
|
|
44
|
+
locale={locale}
|
|
45
|
+
currentDate={currentDate}
|
|
46
|
+
setCurrentDate={setCurrentDate}
|
|
47
|
+
attendanceData={attendanceData}
|
|
48
|
+
onDateClick={onDateClick}
|
|
49
|
+
onYearViewClick={handleYearViewClick}
|
|
50
|
+
hideControls={hideControls}
|
|
51
|
+
hideYear={hideYear}
|
|
52
|
+
/>
|
|
53
|
+
) : (
|
|
54
|
+
<YearView
|
|
55
|
+
locale={locale}
|
|
56
|
+
currentDate={currentDate}
|
|
57
|
+
setCurrentDate={setCurrentDate}
|
|
58
|
+
handleMonthClick={handleMonthClick}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../../tooltip';
|
|
2
|
+
import {
|
|
3
|
+
WorkspaceUserAttendance,
|
|
4
|
+
getAttendanceGroupNames,
|
|
5
|
+
isCurrentMonth,
|
|
6
|
+
isDateAbsent,
|
|
7
|
+
isDateAttended,
|
|
8
|
+
} from './utils';
|
|
9
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
10
|
+
import { isAfter } from 'date-fns';
|
|
11
|
+
import { Fragment } from 'react';
|
|
12
|
+
|
|
13
|
+
export const DayCell: React.FC<{
|
|
14
|
+
day: Date;
|
|
15
|
+
currentDate: Date;
|
|
16
|
+
today: Date;
|
|
17
|
+
attendanceData?: WorkspaceUserAttendance[];
|
|
18
|
+
// eslint-disable-next-line no-unused-vars
|
|
19
|
+
onDateClick?: (date: Date) => void;
|
|
20
|
+
}> = ({ day, currentDate, today, attendanceData, onDateClick }) => {
|
|
21
|
+
if (!isCurrentMonth(day, currentDate))
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex flex-none cursor-default justify-center rounded border border-transparent p-2 font-semibold text-foreground/20 transition duration-300 md:rounded-lg">
|
|
24
|
+
{day.getDate()}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
!isDateAttended(day, attendanceData) &&
|
|
30
|
+
!isDateAbsent(day, attendanceData)
|
|
31
|
+
)
|
|
32
|
+
return (
|
|
33
|
+
<button
|
|
34
|
+
onClick={onDateClick ? () => onDateClick(day) : undefined}
|
|
35
|
+
className={cn(
|
|
36
|
+
'flex flex-none cursor-default justify-center rounded border bg-foreground/5 p-2 font-semibold text-foreground/40 transition duration-300 hover:cursor-pointer md:rounded-lg dark:bg-foreground/10',
|
|
37
|
+
isAfter(day, today) &&
|
|
38
|
+
'cursor-not-allowed opacity-50 hover:cursor-not-allowed'
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{day.getDate()}
|
|
42
|
+
</button>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Fragment>
|
|
47
|
+
<Tooltip>
|
|
48
|
+
<TooltipTrigger asChild>
|
|
49
|
+
<button
|
|
50
|
+
onClick={onDateClick ? () => onDateClick(day) : undefined}
|
|
51
|
+
className={`flex flex-none cursor-pointer justify-center rounded border p-2 font-semibold transition duration-300 md:rounded-lg ${
|
|
52
|
+
isDateAttended(day, attendanceData)
|
|
53
|
+
? 'border-green-500/30 bg-green-500/10 text-green-600 dark:border-green-300/20 dark:bg-green-300/20 dark:text-green-300'
|
|
54
|
+
: isDateAbsent(day, attendanceData)
|
|
55
|
+
? 'border-red-500/30 bg-red-500/10 text-red-600 dark:border-red-300/20 dark:bg-red-300/20 dark:text-red-300'
|
|
56
|
+
: 'bg-foreground/5 text-foreground/40 dark:bg-foreground/10'
|
|
57
|
+
}`}
|
|
58
|
+
>
|
|
59
|
+
{day.getDate()}
|
|
60
|
+
</button>
|
|
61
|
+
</TooltipTrigger>
|
|
62
|
+
<TooltipContent>
|
|
63
|
+
{getAttendanceGroupNames(day, attendanceData).map(
|
|
64
|
+
(groupName, idx) => (
|
|
65
|
+
<div key={groupName + idx} className="flex items-center gap-1">
|
|
66
|
+
<span className="text-xs font-semibold">{groupName}</span>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
)}
|
|
70
|
+
</TooltipContent>
|
|
71
|
+
</Tooltip>
|
|
72
|
+
</Fragment>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Button } from '../../button';
|
|
2
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
3
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export const MonthHeader: React.FC<{
|
|
6
|
+
thisYear: number;
|
|
7
|
+
thisMonth: string;
|
|
8
|
+
handlePrev: () => void;
|
|
9
|
+
handleNext: () => void;
|
|
10
|
+
currentDate: Date;
|
|
11
|
+
onYearViewClick: () => void;
|
|
12
|
+
hideControls?: boolean;
|
|
13
|
+
hideYear?: boolean;
|
|
14
|
+
}> = ({
|
|
15
|
+
thisYear,
|
|
16
|
+
thisMonth,
|
|
17
|
+
handlePrev,
|
|
18
|
+
handleNext,
|
|
19
|
+
currentDate,
|
|
20
|
+
onYearViewClick,
|
|
21
|
+
hideControls = false,
|
|
22
|
+
hideYear = false,
|
|
23
|
+
}) => (
|
|
24
|
+
<div
|
|
25
|
+
className={cn(
|
|
26
|
+
'flex flex-wrap items-center justify-between gap-x-4 gap-y-1 text-xl font-bold md:text-2xl',
|
|
27
|
+
hideControls || 'mb-4'
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
<div className="flex items-center gap-1">
|
|
31
|
+
{hideYear || thisYear}
|
|
32
|
+
{hideYear || (
|
|
33
|
+
<div className="mx-2 h-4 w-[1px] rotate-[30deg] bg-foreground/20" />
|
|
34
|
+
)}
|
|
35
|
+
<span className="text-lg font-semibold md:text-xl">{thisMonth}</span>
|
|
36
|
+
</div>
|
|
37
|
+
{!hideControls && (
|
|
38
|
+
<div className="flex items-center gap-1">
|
|
39
|
+
<Button size="xs" variant="secondary" onClick={onYearViewClick}>
|
|
40
|
+
Year View
|
|
41
|
+
</Button>
|
|
42
|
+
<Button size="xs" variant="secondary" onClick={handlePrev}>
|
|
43
|
+
<ChevronLeft className="h-6 w-6" />
|
|
44
|
+
</Button>
|
|
45
|
+
<Button
|
|
46
|
+
size="xs"
|
|
47
|
+
variant="secondary"
|
|
48
|
+
onClick={handleNext}
|
|
49
|
+
disabled={
|
|
50
|
+
currentDate.getMonth() === new Date().getMonth() &&
|
|
51
|
+
currentDate.getFullYear() === new Date().getFullYear()
|
|
52
|
+
}
|
|
53
|
+
>
|
|
54
|
+
<ChevronRight className="h-6 w-6" />
|
|
55
|
+
</Button>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Separator } from '../../separator';
|
|
2
|
+
import { TooltipProvider } from '../../tooltip';
|
|
3
|
+
import { DayCell } from './day-cell';
|
|
4
|
+
import { MonthHeader } from './month-header';
|
|
5
|
+
import { WorkspaceUserAttendance } from './utils';
|
|
6
|
+
import { startOfDay } from 'date-fns';
|
|
7
|
+
import { useMemo } from 'react';
|
|
8
|
+
|
|
9
|
+
export const MonthView: React.FC<{
|
|
10
|
+
locale: string;
|
|
11
|
+
currentDate: Date;
|
|
12
|
+
setCurrentDate: React.Dispatch<React.SetStateAction<Date>>;
|
|
13
|
+
attendanceData?: WorkspaceUserAttendance[];
|
|
14
|
+
// eslint-disable-next-line no-unused-vars
|
|
15
|
+
onDateClick?: (date: Date) => void;
|
|
16
|
+
onYearViewClick: () => void;
|
|
17
|
+
hideControls?: boolean;
|
|
18
|
+
hideYear?: boolean;
|
|
19
|
+
}> = ({
|
|
20
|
+
locale,
|
|
21
|
+
currentDate,
|
|
22
|
+
setCurrentDate,
|
|
23
|
+
attendanceData,
|
|
24
|
+
onDateClick,
|
|
25
|
+
onYearViewClick,
|
|
26
|
+
hideControls = false,
|
|
27
|
+
hideYear = false,
|
|
28
|
+
}) => {
|
|
29
|
+
const thisYear = currentDate.getFullYear();
|
|
30
|
+
const thisMonth = currentDate.toLocaleString(locale, {
|
|
31
|
+
month: hideYear ? 'long' : '2-digit',
|
|
32
|
+
});
|
|
33
|
+
const today = startOfDay(new Date());
|
|
34
|
+
|
|
35
|
+
const days = useMemo(
|
|
36
|
+
() =>
|
|
37
|
+
Array.from({ length: 7 }, (_, i) => {
|
|
38
|
+
let newDay = new Date(currentDate);
|
|
39
|
+
newDay.setDate(currentDate.getDate() - currentDate.getDay() + i + 1);
|
|
40
|
+
return newDay.toLocaleString(locale, { weekday: 'narrow' });
|
|
41
|
+
}),
|
|
42
|
+
[currentDate, locale]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const daysInMonth = useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
Array.from({ length: 42 }, (_, i) => {
|
|
48
|
+
let newDay = new Date(
|
|
49
|
+
currentDate.getFullYear(),
|
|
50
|
+
currentDate.getMonth(),
|
|
51
|
+
1
|
|
52
|
+
);
|
|
53
|
+
let dayOfWeek = newDay.getDay();
|
|
54
|
+
let adjustment = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
|
55
|
+
newDay.setDate(newDay.getDate() - adjustment + i);
|
|
56
|
+
return newDay;
|
|
57
|
+
}),
|
|
58
|
+
[currentDate]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const handlePrev = () =>
|
|
62
|
+
setCurrentDate(new Date(currentDate.setMonth(currentDate.getMonth() - 1)));
|
|
63
|
+
const handleNext = () =>
|
|
64
|
+
setCurrentDate(new Date(currentDate.setMonth(currentDate.getMonth() + 1)));
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div>
|
|
68
|
+
<MonthHeader
|
|
69
|
+
thisYear={thisYear}
|
|
70
|
+
thisMonth={thisMonth}
|
|
71
|
+
handlePrev={handlePrev}
|
|
72
|
+
handleNext={handleNext}
|
|
73
|
+
currentDate={currentDate}
|
|
74
|
+
onYearViewClick={onYearViewClick}
|
|
75
|
+
hideControls={hideControls}
|
|
76
|
+
hideYear={hideYear}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
{hideControls && <Separator className="my-2" />}
|
|
80
|
+
|
|
81
|
+
<div className="relative grid gap-1 text-xs md:gap-2 md:text-base">
|
|
82
|
+
<div className="grid grid-cols-7 gap-1 md:gap-2">
|
|
83
|
+
{days.map((day, idx) => (
|
|
84
|
+
<div
|
|
85
|
+
key={`day-${idx}`}
|
|
86
|
+
className="flex flex-none cursor-default justify-center rounded bg-foreground/5 p-2 font-semibold transition duration-300 md:rounded-lg"
|
|
87
|
+
>
|
|
88
|
+
{day}
|
|
89
|
+
</div>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="grid grid-cols-7 gap-1 md:gap-2">
|
|
94
|
+
<TooltipProvider delayDuration={0}>
|
|
95
|
+
{daysInMonth.map((day, idx) => (
|
|
96
|
+
<DayCell
|
|
97
|
+
key={`day-${idx}`}
|
|
98
|
+
day={day}
|
|
99
|
+
currentDate={currentDate}
|
|
100
|
+
today={today}
|
|
101
|
+
attendanceData={attendanceData}
|
|
102
|
+
onDateClick={onDateClick}
|
|
103
|
+
/>
|
|
104
|
+
))}
|
|
105
|
+
</TooltipProvider>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface WorkspaceUserAttendance {
|
|
2
|
+
date: string;
|
|
3
|
+
status: 'PRESENT' | 'ABSENT';
|
|
4
|
+
groups?: {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const isCurrentMonth = (date: Date, currentDate: Date) =>
|
|
11
|
+
date.getMonth() === currentDate.getMonth() &&
|
|
12
|
+
date.getFullYear() === currentDate.getFullYear();
|
|
13
|
+
|
|
14
|
+
export const isDateAttended = (
|
|
15
|
+
date: Date,
|
|
16
|
+
attendanceData?: WorkspaceUserAttendance[]
|
|
17
|
+
) =>
|
|
18
|
+
attendanceData
|
|
19
|
+
? attendanceData.some((attendance) => {
|
|
20
|
+
const attendanceDate = new Date(attendance.date);
|
|
21
|
+
return (
|
|
22
|
+
attendanceDate.getDate() === date.getDate() &&
|
|
23
|
+
attendanceDate.getMonth() === date.getMonth() &&
|
|
24
|
+
attendanceDate.getFullYear() === date.getFullYear() &&
|
|
25
|
+
attendance.status === 'PRESENT'
|
|
26
|
+
);
|
|
27
|
+
})
|
|
28
|
+
: false;
|
|
29
|
+
|
|
30
|
+
export const isDateAbsent = (
|
|
31
|
+
date: Date,
|
|
32
|
+
attendanceData?: WorkspaceUserAttendance[]
|
|
33
|
+
) =>
|
|
34
|
+
attendanceData
|
|
35
|
+
? attendanceData.some((attendance) => {
|
|
36
|
+
const attendanceDate = new Date(attendance.date);
|
|
37
|
+
return (
|
|
38
|
+
attendanceDate.getDate() === date.getDate() &&
|
|
39
|
+
attendanceDate.getMonth() === date.getMonth() &&
|
|
40
|
+
attendanceDate.getFullYear() === date.getFullYear() &&
|
|
41
|
+
attendance.status === 'ABSENT'
|
|
42
|
+
);
|
|
43
|
+
})
|
|
44
|
+
: false;
|
|
45
|
+
|
|
46
|
+
export const getAttendanceGroupNames = (
|
|
47
|
+
date: Date,
|
|
48
|
+
attendanceData?: WorkspaceUserAttendance[]
|
|
49
|
+
): string[] => {
|
|
50
|
+
if (!attendanceData) return [];
|
|
51
|
+
const filteredAttendance = attendanceData.filter((attendance) => {
|
|
52
|
+
const attendanceDate = new Date(attendance.date);
|
|
53
|
+
return (
|
|
54
|
+
attendanceDate.getDate() === date.getDate() &&
|
|
55
|
+
attendanceDate.getMonth() === date.getMonth() &&
|
|
56
|
+
attendanceDate.getFullYear() === date.getFullYear()
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const uniqueGroups = filteredAttendance.reduce(
|
|
61
|
+
(acc, curr) => {
|
|
62
|
+
Array.isArray(curr.groups)
|
|
63
|
+
? curr.groups.forEach((group) => {
|
|
64
|
+
if (!acc.some((g) => g.id === group.id)) {
|
|
65
|
+
acc.push(group);
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
: curr.groups && acc.push(curr.groups);
|
|
69
|
+
|
|
70
|
+
return acc;
|
|
71
|
+
},
|
|
72
|
+
[] as { id: string; name: string }[]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return uniqueGroups.map((group) => group.name);
|
|
76
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Calendar } from './core';
|
|
4
|
+
import { WorkspaceUserAttendance } from './utils';
|
|
5
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
6
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
|
|
9
|
+
interface YearCalendarProps {
|
|
10
|
+
locale: string;
|
|
11
|
+
initialDate?: Date;
|
|
12
|
+
attendanceData?: WorkspaceUserAttendance[];
|
|
13
|
+
// eslint-disable-next-line no-unused-vars
|
|
14
|
+
onDateClick?: (date: Date) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const YearCalendar: React.FC<YearCalendarProps> = ({
|
|
18
|
+
locale,
|
|
19
|
+
initialDate,
|
|
20
|
+
attendanceData,
|
|
21
|
+
onDateClick,
|
|
22
|
+
}) => {
|
|
23
|
+
const [currentYear, setCurrentYear] = useState(
|
|
24
|
+
initialDate?.getFullYear() || new Date().getFullYear()
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const handlePrevYear = () => setCurrentYear(currentYear - 1);
|
|
28
|
+
const handleNextYear = () => setCurrentYear(currentYear + 1);
|
|
29
|
+
|
|
30
|
+
const months = Array.from(
|
|
31
|
+
{ length: 12 },
|
|
32
|
+
(_, i) => new Date(currentYear, i, 1)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div>
|
|
37
|
+
<div className="mb-4 flex flex-wrap items-center justify-between gap-x-4 gap-y-1 rounded-lg border bg-foreground/5 p-4 text-xl font-bold md:text-2xl">
|
|
38
|
+
<div className="flex items-center gap-1">{currentYear}</div>
|
|
39
|
+
<div className="flex items-center gap-1">
|
|
40
|
+
<Button size="xs" variant="secondary" onClick={handlePrevYear}>
|
|
41
|
+
<ChevronLeft className="h-6 w-6" />
|
|
42
|
+
</Button>
|
|
43
|
+
<Button size="xs" variant="secondary" onClick={handleNextYear}>
|
|
44
|
+
<ChevronRight className="h-6 w-6" />
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
|
|
50
|
+
{months.map((month, idx) => (
|
|
51
|
+
<div key={idx} className="rounded-lg border p-2">
|
|
52
|
+
<Calendar
|
|
53
|
+
locale={locale}
|
|
54
|
+
initialDate={month}
|
|
55
|
+
attendanceData={attendanceData}
|
|
56
|
+
onDateClick={onDateClick}
|
|
57
|
+
hideControls
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Button } from '../../button';
|
|
2
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
export const YearView: React.FC<{
|
|
6
|
+
locale: string;
|
|
7
|
+
currentDate: Date;
|
|
8
|
+
setCurrentDate: React.Dispatch<React.SetStateAction<Date>>;
|
|
9
|
+
// eslint-disable-next-line no-unused-vars
|
|
10
|
+
handleMonthClick: (month: number) => void;
|
|
11
|
+
}> = ({ locale, currentDate, setCurrentDate, handleMonthClick }) => {
|
|
12
|
+
const thisYear = currentDate.getFullYear();
|
|
13
|
+
const months = useMemo(
|
|
14
|
+
() =>
|
|
15
|
+
Array.from(
|
|
16
|
+
{ length: 12 },
|
|
17
|
+
(_, i) => new Date(currentDate.getFullYear(), i, 1)
|
|
18
|
+
),
|
|
19
|
+
[currentDate]
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const handlePrev = () =>
|
|
23
|
+
setCurrentDate(
|
|
24
|
+
new Date(currentDate.setFullYear(currentDate.getFullYear() - 1))
|
|
25
|
+
);
|
|
26
|
+
const handleNext = () =>
|
|
27
|
+
setCurrentDate(
|
|
28
|
+
new Date(currentDate.setFullYear(currentDate.getFullYear() + 1))
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
<div className="mb-4 flex flex-wrap items-center justify-between gap-x-4 gap-y-1 text-xl font-bold md:text-2xl">
|
|
34
|
+
<div className="flex items-center gap-1">{thisYear}</div>
|
|
35
|
+
<div className="flex items-center gap-1">
|
|
36
|
+
<Button size="xs" variant="secondary" onClick={handlePrev}>
|
|
37
|
+
<ChevronLeft className="h-6 w-6" />
|
|
38
|
+
</Button>
|
|
39
|
+
<Button size="xs" variant="secondary" onClick={handleNext}>
|
|
40
|
+
<ChevronRight className="h-6 w-6" />
|
|
41
|
+
</Button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="grid grid-cols-3 gap-4">
|
|
46
|
+
{months.map((month, idx) => (
|
|
47
|
+
<button
|
|
48
|
+
key={`month-${idx}`}
|
|
49
|
+
onClick={() => handleMonthClick(month.getMonth())}
|
|
50
|
+
className="flex flex-none cursor-pointer justify-center rounded bg-foreground/5 p-4 font-semibold transition duration-300 hover:bg-foreground/10"
|
|
51
|
+
>
|
|
52
|
+
{month.toLocaleString(locale, { month: 'long' })}
|
|
53
|
+
</button>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
};
|