از آنجایی که فرانتیوم علاقه‌ی زیادی به گربه‌ها داشت، تعداد زیادی گربه در خانه‌اش نگه می‌داشت.

او برای آن‌ها یک زمین بازی در حیاط خانه‌اش ساخته بود. در طول روز، همیشه تعدادی از گربه‌ها در زمین بازی بودند، اما گاهی فرانتیوم نمی‌دانست کدام گربه‌ها مشغول بازی هستند.

به همین دلیل به ذهنش رسید که چرا برای گربه‌ها یک سامانه‌ی ورود و خروج درست نکند؟

توضیح تصویر

پروژه‌ی اولیه

برای دانلود پروژه‌ی اولیه روی این لینک کلیک کنید.

ساختار فایل‌ها

samane/
├─ public/
│  └─ vite.svg
├─ src/
│  ├─ api/
│  │  ├─ attendance.api.ts
│  │  ├─ client.ts
│  │  ├─ products.api.ts
│  │  └─ users.api.ts
│  ├─ app/
│  │  ├─ App.tsx
│  │  └─ routes.tsx
│  ├─ components/
│  │  ├─ Layout/
│  │  │  ├─ MainLayout.tsx
│  │  │  └─ Sidebar.tsx
│  │  ├─ Modal/
│  │  │  └─ GenericModal.tsx
│  │  └─ Table/
│  │     └─ GenericTable.tsx
│  ├─ features/
│  │  ├─ dashboard/
│  │  │  ├─ DashboardPage.tsx
│  │  │  ├─ EntryModal.tsx
│  │  │  ├─ ExitModal.tsx
│  │  │  └─ useAttendance.ts
│  │  ├─ products/
│  │  │  ├─ ProductFormModal.tsx
│  │  │  ├─ ProductPage.tsx
│  │  │  └─ useProducts.ts
│  │  └─ users/
│  │     ├─ starter/
│  │     │  ├─ UserFormModal_starter.tsx
│  │     │  └─ useUsers_starter.ts
│  │     ├─ UserFormModal.tsx
│  │     ├─ UsersPage.tsx
│  │     └─ useUsers.ts
│  ├─ types/
│  │  └─ index.ts
│  ├─ index.css
│  └─ main.tsx
├─ eslint.config.js
├─ index.html
├─ package.json
├─ README.md
├─ tsconfig.json
├─ tsconfig.node.json
└─ vite.config.ts

پیشنمایش‌های پیاده‌سازی

در گیف‌های زیر، می‌توانید تمام عملکرد برنامه را مشاهده کنید: توضیح تصویر

توضیح تصویر

توضیح تصویر

جزئیات پیاده‌سازی

فایل index.ts

در این فایل شما باید تایپ‌های مورد نیاز پروژه را تعریف کنید، چرا که در ادامه در تمامی کامپوننت‌ها، فرم‌ها و APIها از این تایپ‌ها استفاده خواهد شد.

کامنت‌های موجود در فایل به طور دقیق مشخص کرده‌اند که هر تایپ چه پراپرتی‌هایی نیاز دارد و نوع داده‌ی آن‌ها چیست. بنابراین با دقت و صرف زمان مناسب، تایپ‌ها را به درستی و کامل تعریف کنید تا در مراحل بعدی پروژه با خطاهای تایپ‌اسکریپتی مواجه نشوید.

نکات مهم هنگام پیاده‌سازی:

  • از نام‌های پیشنهادی در کامنت‌ها استفاده کنید تا هماهنگی با سایر بخش‌ها حفظ شود.

  • برای مقادیر محدود، مثل جنسیت، حتماً نوع محدود (union type) تعریف کنید تا مقدار دیگری پذیرفته نشود.

  • پراپرتی‌های اختیاری را با ? مشخص کنید.

  • تایپ‌های مرتبط با پاسخ API یا payload ها باید دقیقاً مشابه کامنت‌ها تعریف شوند تا سایر بخش‌ها بتوانند بدون خطا با آن‌ها کار کنند.

توجه: پس از تکمیل تایپ‌ها حتما تمام کامنت‌ها را پاک کنید!

کامپوننت main.tsx

  • یک QueryClient بسازید و آن را با QueryClientProvider در بالاترین سطح پروژه قرار دهید.

  • بعد از اینکار، می‌توانید در کل پروژه از useQuery, useMutation و سایر hooks های React Query استفاده کنید.

  • این ساختار پایه برای مدیریت state های async و داده‌های API است و نیازی به تغییر App یا فایل‌های دیگر نیست.

پیاده‌سازی Routes.ts و App.tsx

فایل Routes.ts

وظیفه:

  • پروژه را به router وصل می‌کند تا مسیریابی در اپلیکیشن کار کند.

  • بالاترین سطح اپلیکیشن را مدیریت می‌کند و RouterProvider از react-router-dom را رندر می‌کند.

نکات مهم:

  • router که از فایل routes.tsx صادر شده، باید به RouterProvider داده شود.

کامپوننت App.tsx

وظیفه:

  • تعریف مسیرهای اپلیکیشن و تعیین component هایی که در هر مسیر نمایش داده می‌شوند.

  • استفاده از createBrowserRouter برای مدیریت مسیرهای SPA.

نکات مهم:

  • مسیر اصلی / با MainLayout نمایش داده می‌شود.

  • MainLayout شامل نوار کناری، header و فضای نمایش صفحات است.

  • مسیرهای داخلی:

    • /DashboardPage

    • /usersUsersPage

    • /productsProductPage

  • مسیرها باید با children زیر MainLayout تعریف شوند تا layout ثابت بماند.

دایرکتوری api

فایل client.ts

پیاده‌سازی axiosClient

هدف

ساخت یک کلاینت استاندارد Axios برای مدیریت یکپارچه‌ی درخواست‌ها و پاسخ‌های API. این کلاینت باید در تمام بخش‌های پروژه برای ارتباط با سرور استفاده شود و مسئول مدیریت خطاها نیز باشد.

مقدمه

axios یک کتابخانه‌ی HTTP Client برای مرورگر و Node.js است که:

  • از Promise پشتیبانی می‌کند.

  • امکان ارسال درخواست‌های GET, POST, PUT, DELETE و ... را فراهم می‌کند.

  • با TypeScript سازگار است و قابلیت تایپ کردن request و response دارد.

گام‌های پیاده‌سازی

  1. ایجاد کلاینت Axios

ابتدا یک کلاینت Axios با تنظیمات پایه بسازید:

baseURL تمام مسیرهای API باید با مقدار /api آغاز شوند. مثلاً اگر axiosClient.get("/users") فراخوانی شود، مسیر نهایی /api/users خواهد بود.

headers هدر پیش‌فرض تمام درخواست‌ها باید شامل "Content-Type": "application/json" باشد.

  1. افزودن Interceptor برای پاسخ‌ها

    • در axios می‌توانید از interceptors استفاده کنید تا قبل یا بعد از ارسال درخواست، منطق خاصی اجرا کنید.

    • در این پروژه، فقط نیاز است interceptor پاسخ (response interceptor) را پیاده‌سازی کنید.

    • این interceptor باید:

      • در حالت موفق، پاسخ را بدون تغییر بازگرداند.

      • در حالت خطا، جزئیات خطا (مثلاً error.response) را در کنسول چاپ کند تا در زمان توسعه قابل بررسی باشد.

      • سپس، خطا را بازگرداند تا در لایه‌های بالاتر (مثلاً در React Query) بتوان آن را مدیریت کرد.

  2. برگرداندن مقدار نهایی

    • در پایان، axiosClient را export کنید تا در سایر بخش‌های پروژه قابل استفاده باشد.

نکته

در این فایل نیازی به مدیریت توکن یا احراز هویت نیست. هدف تنها ایجاد یک ساختار اولیه‌ی استاندارد برای ارسال درخواست‌ها و هندل کردن خطاهاست.

فایل users.api.ts

این فایل شامل سه تابع اصلی برای مدیریت داده‌های کاربر به‌صورت mock (شبیه‌سازی‌شده) است. هدف از این فایل، فراهم کردن رابطی ساده برای دریافت، اضافه‌کردن و ریست‌کردن لیست کاربران است.

تابع getUsers

توضیح: این تابع برای دریافت لیست کاربران استفاده می‌شود. در صورت ارسال پارامتر q، کاربران بر اساس نام یا نام خانوادگی فیلتر می‌شوند.

ورودی:

  • q (اختیاری): رشته‌ای برای جست‌وجو در نام یا نام خانوادگی.

خروجی:

  • Promise<User[]> → لیستی از کاربران (به‌صورت async).

تابع createUser

توضیح: برای افزودن کاربر جدید به لیست استفاده می‌شود. تابع به‌صورت خودکار شناسه (id) تولید کرده و کاربر جدید را به آرایه اضافه می‌کند.

ورودی:

  • newUser: شیئی شامل مشخصات کاربر به‌جز فیلد id (زیرا id به‌صورت خودکار ساخته می‌شود)

خروجی:

  • Promise<User> → کاربر ایجاد شده همراه با id جدید.

تابع resetUsers

توضیح: لیست کاربران را به حالت اولیه (۱۰ کاربر اول) برمی‌گرداند. در واقع تمام کاربران جدید اضافه‌شده حذف می‌شوند.

نکته: تمام توابع به‌صورت شبیه‌سازی‌شده دارای تأخیر (delay) هستند تا رفتار واقعی API را تداعی کنند.

فایل products.api.ts

این فایل شامل توابعی برای مدیریت داده‌های محصولات به‌صورت mock است.

تابع getProducts

توضیح: این تابع لیست کامل محصولات را برمی‌گرداند. همه داده‌ها از یک آرایه‌ی شبیه‌سازی‌شده (mockProducts) خوانده می‌شوند.

ورودی: ندارد.

خروجی:

  • Promise<Product[]> → لیستی از تمام محصولات.

تابع createProduct

توضیح: برای افزودن محصول جدید استفاده می‌شود. شناسه (id) به‌صورت خودکار تولید شده و محصول جدید به لیست افزوده می‌شود.

ورودی:

  • newProduct: شیئی شامل نام و قیمت محصول (بدون id).

خروجی:

  • Promise<Product> → محصول ایجادشده همراه با id تصادفی.

تابع resetProducts

توضیح: لیست محصولات را به حالت اولیه بازمی‌گرداند. تمام محصولاتی که بعداً اضافه شده‌اند حذف می‌شوند و داده‌ها به آرایه‌ی اولیه برمی‌گردند.

نکته: همه‌ی توابع شامل تأخیر مصنوعی هستند تا رفتار واقعی درخواست‌های شبکه را شبیه‌سازی کنند

فایل attendance.api.ts

این فایل مسئول مدیریت ورود و خروج کاربران و محاسبه‌ی هزینه‌ی حضور آن‌هاست. همچنین ارتباط بین کاربران (user)، محصولات (product) و سوابق حضور (attendance) را شبیه‌سازی می‌کند.

ثابت‌ها و تنظیمات

const BASE_FEE_PER_HOUR = 10000;

هر کاربر به‌ازای هر ساعت حضور باید مبلغ ۱۰٬۰۰۰ تومان پرداخت کند. هزینه بر اساس دقیقه محاسبه می‌شود (ratePerMinute = BASE_FEE_PER_HOUR / 60).

تابع getPresentUsers

توضیح: لیست تمام کاربرانی را برمی‌گرداند که وارد شده‌اند اما هنوز خارج نشده‌اند.

ورودی: ندارد.

خروجی:

  • Promise<AttendanceRecord[]> → لیست کاربران حاضر.

تابع enterUser

توضیح: ثبت ورود یک کاربر جدید به سیستم. اگر کاربر هنوز خارج نشده باشد، خطا می‌دهد.

ورودی:

  • payload.userId → شناسه‌ی کاربر.

  • payload.enteredAt (اختیاری) → زمان ورود (در صورت عدم ارسال، زمان فعلی ثبت می‌شود).

خروجی:

  • Promise<AttendanceRecord> → رکورد ثبت‌شده‌ی ورود.

تابع exitUser

توضیح: ثبت خروج کاربر از سیستم و محاسبه‌ی هزینه‌ی نهایی. در زمان خروج، مدت حضور و محصولات خریداری‌شده لحاظ می‌شوند.

مراحل عملکرد تابع:

  1. بررسی می‌کند که رکورد حضور وجود داشته باشد و خروج قبلاً ثبت نشده باشد.

  2. مدت حضور (برحسب دقیقه) محاسبه می‌شود.

  3. هزینه‌ی پایه بر اساس زمان محاسبه می‌شود.

  4. قیمت محصولات انتخاب‌شده (از getProducts()) جمع زده می‌شود.

  5. مجموع کل (total) = baseFee + productsTotal.

  6. مقدار خروج (exitedAt) و محصولات (productIds) در رکورد ذخیره می‌شوند.

تابع resetAttendance

توضیح: تمام سوابق حضور را پاک کرده و سیستم را به حالت اولیه بازمی‌گرداند.

نکته: مثل سایر فایل‌های API، همه‌ی توابع شامل تأخیر شبیه‌سازی‌شده هستند

دایرکتوری components

فایل Layout

کامپوننت MainLayout

در این فایل، ساختار اصلی صفحه تعریف شده است که شامل دو بخش اصلی می‌باشد:

  • Sidebar برای نمایش منوی کناری

  • Content برای نمایش محتوای متغیر (داینامیک)

در قسمت مشخص‌شده با کامنت Content، باید محتوای هر صفحه (مثلاً داشبورد، کاربران، تنظیمات و ...) به‌صورت داینامیک نمایش داده شود.

هدف این است که هنگام تغییر مسیر (Route)، فقط بخش محتوایی به‌روز شود و ساختار کلی صفحه (مثل Sidebar) ثابت بماند.

کامپوننت Sidebar.tsx

در این فایل باید منوی کناری (Sidebar) را از نظر منطق عملکرد پیاده‌سازی کنید. تمام ساختار و استایل مورد نیاز از قبل آماده است، اما هیچ رفتار یا داده‌ی منطقی وجود ندارد.

هدف:

نمایش یک منوی ناوبری در سمت راست صفحه که کاربر بتواند با کلیک بر روی هر آیتم، بین صفحات مختلف جابه‌جا شود و آیتم فعال مشخص باشد.

گام‌های پیاده‌سازی

1. ایمپورت‌های مورد نیاز

از کتابخانه‌های زیر استفاده کنید:

  • useNavigate و useLocation از react-router-dom برای مدیریت مسیرها

  • آیکون‌های مورد نیاز از @mui/icons-material (مثلاً DashboardIcon, PeopleIcon, InventoryIcon)

2. ایجاد آرایه آیتم‌ها

در داخل کامپوننت، آرایه‌ای به نام items بسازید که شامل آبجکت‌هایی با ساختار زیر باشد:

{
  text: string;     // عنوان آیتم (مثلاً "کاربران")
  icon: JSX.Element; // آیکون مربوطه
  path: string;      // مسیر صفحه (مثلاً "/users")
}

به عنوان مثال، سه آیتم با مسیرهای زیر نیاز است:

  • / برای صفحه‌ی داشبورد

  • /users برای صفحه‌ی کاربران

  • /products برای صفحه‌ی هزینه‌های مازاد

3. رندر کردن آیتم‌ها

درون تگ <List>، از متد map برای نمایش تمام آیتم‌ها استفاده کنید.

در هر ListItemButton:

  • مسیر فعلی را با location.pathname مقایسه کنید تا آیتم فعال (selected) مشخص شود.

  • هنگام کلیک، با استفاده از navigate(path) کاربر را به صفحه‌ی جدید هدایت کنید.

4. نکته مهم

شما نباید ساختار، استایل یا layout را تغییر دهید. فقط منطق مربوط به:

  • ایجاد آرایه آیتم‌ها

  • تشخیص مسیر فعال

  • جابه‌جایی بین صفحات را پیاده‌سازی کنید.

فایل Modal

فایل Table

در این بخش باید یک کامپوننت عمومی جدول (Generic Table) پیاده‌سازی کنید که بتواند هر نوع داده‌ای را بر اساس ستون‌های داده‌شده نمایش دهد.

هدف:

ساخت کامپوننتی که با دریافت آرایه‌ای از ستون‌ها و داده‌ها، جدول را به‌صورت داینامیک نمایش دهد. این کامپوننت باید بتواند برای هر نوع داده‌ای (Users، Products، Records و...) قابل استفاده باشد.

مشخصات Props

1. columns: Column[]

هر ستون شامل دو مقدار است:

{
  key: string;   // کلید مربوط به فیلد داده (مثلاً "name" یا "price")
  label: string; // عنوان نمایشی در هدر جدول
}

2. data: T[]

آرایه‌ای از آبجکت‌ها که هرکدام یک ردیف جدول هستند. هر شیء می‌تواند شامل هر کلید دلخواهی باشد (بنابراین باید از Generic Type استفاده کنید).

وظیفه شما

درون جدول، باید موارد زیر را پیاده‌سازی کنید:

بخش سرستون (TableHead)

  • با استفاده از columns.map تمام ستون‌ها را رندر کنید.

  • مقدار col.label باید در هر سلول نمایش داده شود.

  • جهت متن در سلول‌ها باید راست‌چین باشد (align="right").

بخش بدنه (TableBody)

  • با استفاده از data.map، تمام سطرهای جدول را ایجاد کنید.

  • برای هر ردیف (row) از columns.map استفاده کنید تا ترتیب ستون‌ها مطابق columns باشد.

  • مقدار هر سلول باید از row[col.key] خوانده شود.

نکات مهم

  1. جدول باید کاملاً داینامیک باشد — هیچ ستونی نباید به‌صورت دستی نوشته شود.

  2. برای کلیدهای TableRow و TableCell از مقادیر منحصربه‌فرد (مثل index یا key) استفاده کنید.

  3. نوع داده‌ی ورودی را به‌صورت Generic تعریف کنید تا برای هر مدل داده قابل استفاده باشد:

const GenericTable = <T extends Record<string, any>>({...}: GenericTableProps<T>)

نتیجه مورد انتظار

هنگام استفاده از این کامپوننت به شکل زیر:

<GenericTable
  columns={[
    { key: "name", label: "نام" },
    { key: "price", label: "قیمت" },
  ]}
  data={[
    { name: "آب معدنی", price: 2000 },
    { name: "چیپس", price: 3000 },
  ]}
/>

دایرکتوری features

در این بخش سه ویژگی (Feature) با نام‌های زیر وجود دارد:

  1. users

  2. products

  3. dashboard

در ادامه، هر سه دایرکتوری را به‌صورت جداگانه بررسی می‌کنیم و تمام مواردی را که باید پیاده‌سازی کنید، توضیح خواهیم داد.

هوک‌ها

در هر فایل باید یکسری هوک نوشته شوند که آن‌ها را شرح می‌دهیم

فایل useUsers.ts

در این فایل، دو هوک مرتبط با مدیریت کاربران باید پیاده‌سازی شوند:

  1. useUsers

  2. useCreateUser

هر دو هوک از کتابخانه‌ی React Query (TanStack Query) برای مدیریت داده‌های سمت سرور و به‌روزرسانی خودکار UI استفاده می‌کنند.

پیاده‌سازی هوک useUsers

در این بخش، باید از هوک useQuery مربوط به React Query استفاده کنید.

مراحل کلی به این صورت است:

  1. ابتدا یک کلید ثابت برای کش داده‌ها با نام مثلاً "users" تعریف کنید.

  2. سپس از useQuery برای فراخوانی تابعی مانند getUsers استفاده کنید که داده‌ها را از API برمی‌گرداند.

  3. اگر در ورودی هوک یک مقدار جستجو (q) دریافت می‌کنید، آن را هم در queryKey لحاظ کنید تا کش برای هر مقدار جستجو منحصربه‌فرد باشد.

  4. در نهایت، هوک باید داده‌ها، وضعیت بارگذاری و خطا را برگرداند.

نکته: تابع getUsers را از مسیر ../../api/users.api ایمپورت کنید و نوع داده‌ی خروجی را از User تعریف‌شده در ../../types بگیرید.

پیاده‌سازی هوک useCreateUser

در این بخش باید از هوک useMutation استفاده کنید تا امکان افزودن کاربر جدید فراهم شود.

مراحل پیشنهادی:

  1. از useQueryClient برای دسترسی به کش سراسری React Query استفاده کنید.

  2. از useMutation برای فراخوانی تابعی مانند createUser بهره بگیرید. این تابع باید اطلاعات کاربر جدید را به API ارسال کند.

  3. در بخش onSuccess، با استفاده از invalidateQueries کش مربوط به کلید "users" را پاک کنید تا پس از افزودن کاربر جدید، لیست کاربران به‌روز شود.

نکته: برای ورودی تابع ایجاد کاربر، از نوع Omit<User, "id"> استفاده کنید تا شناسه (id) که توسط سرور ایجاد می‌شود، ارسال نگردد.

فایل useProducts.ts

در این فایل باید دو هوک برای مدیریت داده‌های مربوط به محصولات پیاده‌سازی شوند:

  1. useProducts برای دریافت فهرست محصولات از سرور

  2. useCreateProduct برای ایجاد محصول جدید و به‌روزرسانی داده‌ها پس از آن

پیاده‌سازی هوک useProducts

در این قسمت، باید با استفاده از useQuery داده‌های مربوط به محصولات را از API دریافت کنید.

مراحل کلی:

  1. ابتدا یک کلید کش برای داده‌های محصولات با نام "products" تعریف کنید تا React Query داده‌ها را بر اساس آن ذخیره و مدیریت کند.

  2. سپس از useQuery برای فراخوانی تابع getProducts استفاده کنید. این تابع داده‌ها را از سرور برمی‌گرداند.

  3. هوک باید داده‌ها، وضعیت بارگذاری (isLoading)، خطا (error) و سایر وضعیت‌های مربوط به Query را برگرداند تا در کامپوننت‌های دیگر قابل استفاده باشد.

نکته: تابع getProducts باید از مسیر ../../api/products.api ایمپورت شود و نوع داده‌ی خروجی آن Product[] باشد که از مسیر ../../types در دسترس است.

پیاده‌سازی هوک useCreateProduct

در این بخش باید هوکی برای ایجاد محصول جدید پیاده‌سازی کنید. برای این کار از useMutation استفاده می‌شود.

مراحل پیشنهادی:

  1. از useQueryClient برای دسترسی به کش سراسری React Query استفاده کنید.

  2. از useMutation برای ارسال داده‌های محصول جدید با تابع createProduct بهره بگیرید.

  3. در قسمت onSuccess، با استفاده از invalidateQueries کش مربوط به "products" را پاک کنید تا پس از ایجاد محصول، لیست محصولات به‌روزرسانی شود.

نکته: در این بخش باید نوع ورودی تابع ایجاد محصول را به‌صورت Omit<Product, "id"> تعریف کنید، چون شناسه محصول توسط سرور ساخته می‌شود.

فایل useAttendance.ts

در این فایل چهار هوک مهم برای مدیریت حضور و غیاب کاربران وجود دارد:

  1. usePresentUsers → دریافت لیست کاربران حاضر

  2. useEnterUser → ثبت ورود کاربر

  3. useExitUser → ثبت خروج کاربر

  4. useResetAttendance → ریست کردن داده‌های حضور و غیاب

پیاده‌سازی هوک usePresentUsers

  • این هوک برای دریافت تمام رکوردهای حاضر کاربران استفاده می‌شود.

  • باید از useQuery استفاده کنید و یک کلید کش سراسری تعریف کنید، مثلاً "attendance".

  • تابع queryFn باید getPresentUsers باشد تا داده‌ها را از سرور بگیرد.

  • هوک باید وضعیت بارگذاری (isLoading)، داده‌ها (data) و خطا (error) را برگرداند تا در کامپوننت‌ها قابل استفاده باشد.

پیاده‌سازی هوک useEnterUser

  • این هوک برای ثبت ورود یک کاربر جدید است.

  • از useMutation استفاده کنید و تابع mutationFn آن enterUser باشد.

  • بعد از موفقیت، باید کش مربوط به "attendance" را با invalidateQueries تازه کنید تا لیست کاربران حاضر بروز شود.

  • نوع ورودی داده‌ها (payload) باید مطابق EnterPayload باشد.

پیاده‌سازی هوک useExitUser

  • مشابه useEnterUser است ولی برای ثبت خروج کاربران.

  • mutationFn اینجا تابع exitUser است و بعد از موفقیت کش "attendance" باید تازه شود.

  • نوع داده‌ی ورودی ExitPayload و خروجی ExitResponse است.

پیاده‌سازی هوک useResetAttendance

  • این هوک برای ریست کردن کل داده‌های حضور و غیاب استفاده می‌شود.

  • از useMutation استفاده کنید و تابع mutationFn آن resetAttendance باشد.

  • پس از موفقیت، کش "attendance" را با invalidateQueries تازه کنید.

  • این هوک هیچ داده‌ای به عنوان ورودی نمی‌گیرد و خروجی آن هم void است.

ارسال پاسخ برای این سؤال
فایلی انتخاب نشده است.