Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slot": "^1.1.1",
"@refinedev/core": "^4.57.5",
"@refinedev/simple-rest": "^5.0.10",
"@tanstack/react-query": "^5.65.1",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
Expand Down
38 changes: 38 additions & 0 deletions src/app/(refine)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { Refine } from '@refinedev/core';
import dataProvider from '@refinedev/simple-rest';
import axios from 'axios';
import { config } from '@/app/config';

// Create axios instance with auth headers
const axiosInstance = axios.create({
baseURL: config.baseURL + '/api',
headers: {
'Content-Type': 'application/json',
},
});

axiosInstance.interceptors.request.use((config) => {
const token =
typeof window !== 'undefined' ? localStorage.getItem('auth-data') : null;
if (token) {
const parsedToken = JSON.parse(token);
config.headers.Authorization = `Bearer ${parsedToken.accessToken}`;
}
return config;
});

// Create authenticated data provider
const customDataProvider = dataProvider(config.baseURL + '/api', axiosInstance);


export default function Layout({ children }: React.PropsWithChildren) {
return (
<Refine
dataProvider={customDataProvider}
>
{children}
</Refine>
);
}
70 changes: 70 additions & 0 deletions src/app/(refine)/refine/_components/checkInList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// components/CheckInList.tsx
'use client';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import dayjs from 'dayjs';
import { User } from '../libs/interface';

export const CheckInList = ({
checkIns,
page,
pageSize,
setPage,
}: {
checkIns: User[];
page: number;
pageSize: number;
setPage: (page: number) => void;
}) => {
const totalPages = Math.ceil(checkIns.length / pageSize);
const paginatedCheckIns = checkIns.slice((page - 1) * pageSize, page * pageSize);

return (
<div className="rounded-lg border border-gray-200">
<div className="border-b border-gray-200 bg-gray-50 p-4">
<span className="text-sm font-medium text-gray-700">
เช็คอินวันนี้: {checkIns.length} คน
</span>
</div>

<ul className="divide-y divide-gray-200">
{paginatedCheckIns.map(user => (
<li
key={user.id}
className="flex items-center justify-between px-4 py-3 transition-colors hover:bg-gray-50"
>
<div className="text-sm font-medium text-gray-900">{user.name}</div>
<div className="text-sm font-semibold text-pink-600">
{dayjs(user.lastEntered).format('HH:mm')}
</div>
</li>
))}
</ul>

{checkIns.length > pageSize && (
<div className="border-t border-gray-200 p-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-500">
หน้า {page} จาก {totalPages}
</div>
<div className="flex gap-2">
<button
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page === 1}
className="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 disabled:opacity-50"
>
<ChevronLeft className="h-5 w-5" />
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page >= totalPages}
className="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 disabled:opacity-50"
>
<ChevronRight className="h-5 w-5" />
</button>
</div>
</div>
</div>
)}
</div>
);
};
15 changes: 15 additions & 0 deletions src/app/(refine)/refine/_components/doughnutChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// components/DoughnutChart.tsx
'use client';
import { Doughnut } from 'react-chartjs-2';
import { ChartOptions } from 'chart.js';

export const DoughnutChart = ({
data,
options,
}: {
data: any;
options: ChartOptions<'doughnut'>;
}) => {
return <Doughnut data={data} options={options} />;
};
35 changes: 35 additions & 0 deletions src/app/(refine)/refine/_components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// components/DashboardHeader.tsx
'use client';
import Link from 'next/link';
import { ChevronLeft, Users } from 'lucide-react';

export const DashboardHeader = ({
total,
children,
}: {
total: number;
children: React.ReactNode;
}) => (
<div className="rounded-xl bg-white p-6 shadow-sm">
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-center">
<div>
<Link
href="/staff/qr"
className="group mb-4 flex items-center gap-2 text-gray-700 hover:text-pink-600"
>
<ChevronLeft className="h-5 w-5 transition-transform group-hover:-translate-x-1" />
<h1 className="text-2xl font-semibold text-gray-900">
แดชบอร์ดลงทะเบียน
</h1>
</Link>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Users className="h-5 w-5" />
<span>
ผู้ลงทะเบียนทั้งหมด: <strong className="text-pink-600">{total}</strong> คน
</span>
</div>
</div>
<div className="mt-4 flex gap-3 md:mt-0">{children}</div>
</div>
</div>
);
15 changes: 15 additions & 0 deletions src/app/(refine)/refine/_components/lineChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// components/LineChart.tsx
'use client';
import { Line } from 'react-chartjs-2';
import { ChartOptions } from 'chart.js';

export const LineChart = ({
data,
options,
}: {
data: any;
options: ChartOptions<'line'>;
}) => {
return <Line data={data} options={options} />;
};
74 changes: 74 additions & 0 deletions src/app/(refine)/refine/dashboard/_components/exportButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// components/ExportButton.tsx
'use client';
import { Download } from 'lucide-react';
import { FieldEntry, User } from '../../libs/interface';
import dayjs from 'dayjs';

export const ExportButton = ({ data }: { data: User[] }) => {
// Define fields to export (all User fields except imageUrl)
const fieldEntries: FieldEntry[] = [
{ label: 'ID', field: 'id' },
{ label: 'UID', field: 'uid' },
{ label: 'Name', field: 'name' },
{ label: 'Email', field: 'email' },
{ label: 'Phone', field: 'phone' },
{ label: 'University', field: 'university' },
{ label: 'Size Jersey', field: 'sizeJersey' },
{ label: 'Food Limitation', field: 'foodLimitation' },
{ label: 'Invitation Code', field: 'invitationCode' },
{ label: 'Age', field: 'age' },
{ label: 'Chronic Disease', field: 'chronicDisease' },
{ label: 'Drug Allergy', field: 'drugAllergy' },
{ label: 'Status', field: 'status' },
{ label: 'Graduated Year', field: 'graduatedYear' },
{ label: 'Faculty', field: 'faculty' },
{ label: 'Last Entered', field: 'lastEntered' },
{ label: 'Registered At', field: 'registeredAt' },
{ label: 'Role', field: 'role' },
{ label: 'Education', field: 'education' },
{ label: 'Is Acrophobic', field: 'isAcrophobic' },
];

const handleExportCSV = () => {
const headers = fieldEntries.map(f => f.label).join(',');

const csvContent = [
headers,
...data.map(user =>
fieldEntries.map(({ field }) => {
const value = user[field];
// Handle date fields
if (field === 'lastEntered' || field === 'registeredAt') {
return value && dayjs(value.toString()).isValid()
? dayjs(value.toString()).format('YYYY-MM-DD HH:mm')
: '';
}
// Escape quotes in string fields
return typeof value === 'string'
? `"${value.replace(/"/g, '""')}"`
: value;
}).join(',')
)
].join('\n');

const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `users_export_${dayjs().format('YYYYMMDD_HHmmss')}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};

return (
<button
onClick={handleExportCSV}
className="flex items-center gap-2 rounded-lg bg-pink-600 px-4 py-2 text-white transition-colors hover:bg-pink-700"
>
<Download className="h-5 w-5" />
<span className="text-sm">Export</span>
</button>
);
}
126 changes: 126 additions & 0 deletions src/app/(refine)/refine/dashboard/_components/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';

export const Pagination = ({
current,
totalPages,
pageSize,
totalItems,
setCurrent,
setPageSize,
}: {
current: number;
totalPages: number;
pageSize: number;
totalItems: number;
setCurrent: (page: number) => void;
setPageSize: (size: number) => void;
}) => {
const hasNext = current < totalPages;
const hasPrev = current > 1;

return (
<div className="mt-6 flex flex-col items-center justify-between gap-4 sm:flex-row">
<div className="text-sm text-gray-500">
แสดง {(current - 1) * pageSize + 1} -{' '}
{Math.min(current * pageSize, totalItems)} จาก {totalItems}
</div>

<div className="flex items-center gap-2">
<PaginationControls
current={current}
totalPages={totalPages}
hasPrev={hasPrev}
hasNext={hasNext}
setCurrent={setCurrent}
/>
</div>

<PageSizeSelect pageSize={pageSize} setPageSize={setPageSize} />
</div>
);
};

const PaginationControls = ({
current,
totalPages,
hasPrev,
hasNext,
setCurrent,
}: {
current: number;
totalPages: number;
hasPrev: boolean;
hasNext: boolean;
setCurrent: (page: number) => void;
}) => (
<>
<button
onClick={() => setCurrent(1)}
disabled={!hasPrev}
className="rounded-lg bg-white p-2 text-gray-500 hover:bg-gray-100 disabled:opacity-50"
>
First
</button>
<button
onClick={() => setCurrent(current - 1)}
disabled={!hasPrev}
className="rounded-lg bg-white p-2 text-gray-500 hover:bg-gray-100 disabled:opacity-50"
>
<ChevronLeft className="h-5 w-5" />
</button>

<div className="flex gap-1">
{Array.from({ length: totalPages }, (_, i) => (
<button
key={i + 1}
onClick={() => setCurrent(i + 1)}
className={`rounded-lg px-3 py-1 text-sm ${
current === i + 1
? 'bg-pink-600 text-white'
: 'text-gray-500 hover:bg-gray-100'
}`}
>
{i + 1}
</button>
))}
</div>

<button
onClick={() => setCurrent(current + 1)}
disabled={!hasNext}
className="rounded-lg bg-white p-2 text-gray-500 hover:bg-gray-100 disabled:opacity-50"
>
<ChevronRight className="h-5 w-5" />
</button>
<button
onClick={() => setCurrent(totalPages)}
disabled={!hasNext}
className="rounded-lg bg-white p-2 text-gray-500 hover:bg-gray-100 disabled:opacity-50"
>
Last
</button>
</>
);

const PageSizeSelect = ({
pageSize,
setPageSize,
}: {
pageSize: number;
setPageSize: (size: number) => void;
}) => (
<div className="flex items-center gap-2">
<span>แสดงต่อหน้า:</span>
<select
value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))}
className="rounded-lg border border-gray-200 bg-gray-50 px-2 py-1 text-sm"
>
{[10, 20, 30, 40, 50].map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
</div>
);
Loading