Compare commits

..

242 Commits
master ... dev

Author SHA1 Message Date
Yoga Pangestu
637cc4a3d0 chore: remove pnpm workspace configuration file 2026-09-04 15:41:01 +07:00
Yoga Pangestu
e66cea1d12 feat: implement Google OAuth authentication; add GoogleAuthController, update user model, and configure services 2026-09-04 15:31:13 +07:00
Yoga Pangestu
b99876fb4c feat: enhance assignment and material management; add academic term filtering and improve pagination handling 2026-09-04 15:20:16 +07:00
Yoga Pangestu
892d14c13a feat: enhance AssignmentIndex; implement infinite scroll and simplify pagination handling 2026-09-04 14:18:09 +07:00
Yoga Pangestu
8fb596fb32 feat: enhance MaterialController and MaterialService; add academic term handling in pagination and index methods 2026-09-04 14:17:04 +07:00
Yoga Pangestu
56d607a131 feat: enhance assignment management; implement new card and column components, improve pagination and filtering, and add lecturer information 2026-09-04 14:13:09 +07:00
Yoga Pangestu
35a64c2045 feat: implement infinite scroll for various admin pages and update pagination handling
- Refactored AdministratorController, LecturerController, and StudentController to utilize Inertia's scroll feature for paginated data.
- Enhanced DataCards and DataTable components to support infinite scrolling, allowing for a smoother user experience when navigating large datasets.
- Updated multiple admin pages (e.g., MaterialIndex, LogIndex, FeedbackIndex) to integrate infinite scroll functionality, improving data loading efficiency.
- Adjusted server table hooks to reset keys appropriately for infinite scroll scenarios.
2026-09-04 13:59:26 +07:00
Yoga Pangestu
33f354ad94 feat: enhance MaterialService and MaterialCard; add lecturer information and improve material display with created date 2026-09-04 13:42:33 +07:00
Yoga Pangestu
139875ce31 feat: enhance AssignmentService and MaterialService; add notification functionality for new assignments and materials 2026-09-04 13:26:41 +07:00
Yoga Pangestu
facf7ab177 feat: enhance AttachmentPreviewDialog integration; add icon variant and improve file display in material cards 2026-09-04 11:51:27 +07:00
Yoga Pangestu
19397d3b5b feat: implement DataCards component and ViewToggle; enhance material display options and pagination handling 2026-09-04 10:58:15 +07:00
Yoga Pangestu
acee293b36 feat: enhance CourseClassService; improve search functionality and refine class retrieval based on user roles 2026-09-04 10:34:28 +07:00
Yoga Pangestu
6ec257a1cc feat: update ViewDetailDialog; conditionally display target roles based on user permissions 2026-09-04 10:16:12 +07:00
Yoga Pangestu
ceeb1e325d feat: update StudentsExport and StudentService; inject User dependency and refine data retrieval methods 2026-09-04 10:10:57 +07:00
Yoga Pangestu
7ea4fb067d feat: refactor attendance management; remove 'mine' method and related components, update permissions and types 2026-09-04 09:56:04 +07:00
Yoga Pangestu
d411085400 feat: update BEST_PRACTICE.md; clarify Eloquent select behavior with joins and aggregate methods 2026-09-04 09:45:18 +07:00
Yoga Pangestu
66bacaf0e1 feat: update CourseClass and CourseRegistration services; refine select queries to improve data retrieval efficiency 2026-09-04 09:33:42 +07:00
Yoga Pangestu
609828a2e2 feat: update Schedule management; refactor role checks to use enums and simplify update method, remove unused fields from Schedule type 2026-09-04 09:33:26 +07:00
Yoga Pangestu
573d834905 feat: implement Assignment and Submission management; add controllers, requests, and services for assignment functionality 2026-09-04 09:17:42 +07:00
Yoga Pangestu
024dc4dd5a feat: enhance Material management; implement authorization checks for material actions and refactor user role handling 2026-09-04 09:00:30 +07:00
Yoga Pangestu
88319dac15 feat: refactor CourseRegistration management; update controller methods, add new request classes, and adjust routes for department leader role 2026-09-04 08:29:03 +07:00
Yoga Pangestu
80435b6cfa feat: implement CourseClass and ClassEnrollment management; add controllers, services, and requests for course enrollment functionality 2026-09-03 19:59:58 +07:00
Yoga Pangestu
f6da193473 refactor: reorganize FeedbackService methods; move delete method and restore pendingCount method for improved clarity 2026-09-03 16:27:33 +07:00
Yoga Pangestu
32c04b91dd feat: implement reply functionality for feedback; add ReplyFeedbackRequest and update FeedbackService; enhance UI for reply actions and display 2026-09-03 16:24:04 +07:00
Yoga Pangestu
900c27041a feat: update Announcement management to support target roles; refactor Announcement model, service, and controller; enhance UI for role-based visibility 2026-09-03 15:45:38 +07:00
Yoga Pangestu
37ce3c15ea feat: enhance AcademicAdvisingLogService to filter logs based on student role; update RolePermissionSeeder to include view-academic-advising-logs permission; rename 'Pembimbing Akademik' to 'Dosen' in UI 2026-09-03 15:05:28 +07:00
Yoga Pangestu
51e943572a feat: update AcademicAdvisingLogController and AcademicAdvisingLogService to accept User parameter for improved access control; enhance query scoping based on user roles 2026-09-03 14:44:05 +07:00
Yoga Pangestu
80997b97b4 feat: enhance LetterRequestService and UI to support lecturer information; update pagination and column display for improved clarity 2026-09-03 14:26:43 +07:00
Yoga Pangestu
db56ec94ea feat: add upload file functionality to letter requests; integrate AttachmentPreviewDialog and update column actions 2026-09-03 14:07:37 +07:00
Yoga Pangestu
aa50487bfc feat: update LetterRequestController and LetterRequestService to accept User parameter for improved access control; add letter request permissions in RolePermissionSeeder 2026-09-03 13:58:04 +07:00
Yoga Pangestu
1c901e9103 feat: update CourseService and CourseController to accept User parameter for improved scoping 2026-09-03 13:45:27 +07:00
Yoga Pangestu
2b458e26fa feat: add leadership history dialog and integrate into DepartmentIndex 2026-09-03 13:25:02 +07:00
Yoga Pangestu
f00fab9520 feat: implement DegreeLevel enum and integrate it into DepartmentRequest and DepartmentService 2026-09-03 13:07:35 +07:00
Yoga Pangestu
8055d4ec5d refactor: optimize AcademicTermService methods and improve query handling 2026-09-03 12:52:32 +07:00
Yoga Pangestu
f69ed0bc15 refactor: reorganize StudentService methods and enhance query efficiency with enums 2026-09-03 11:35:27 +07:00
Yoga Pangestu
aa47ec494e feat: enhance Combobox component with multi-select filtering and grouped item handling 2026-09-03 11:33:13 +07:00
Yoga Pangestu
8f9eb5f77a refactor: streamline LecturerService methods and enhance role assignment with enum 2026-09-03 11:25:23 +07:00
Yoga Pangestu
ad5fe33ec6 feat: implement user role management with enum and refactor role handling in administrator services 2026-09-03 11:17:41 +07:00
Yoga Pangestu
3bec716dab feat: add best practices documentation for Laravel projects 2026-09-03 11:02:27 +07:00
Yoga Pangestu
9f717a3441 refactor: update model relationships and request validation rules for consistency 2026-09-03 10:53:48 +07:00
Yoga Pangestu
88286fe596 feat: implement attendance management features including user-specific views and permissions 2026-09-03 10:12:58 +07:00
Yoga Pangestu
4980ad95b3 Refactor assignment management system
- Updated the PermissionCatalog to remove unnecessary permissions for assignment submissions.
- Modified RolePermissionSeeder to align with the updated permissions.
- Enhanced useServerTable hook to support resetKeys for Inertia's reset visit option.
- Removed obsolete columns.tsx file related to assignment columns.
- Revamped assignment index page to utilize InfiniteScroll and improved UI components.
- Introduced new assignment status management with enums and updated database schema.
- Created GradeSubmissionRequest for validation of submission grading.
- Implemented score editing functionality in submission index with real-time updates.
- Added accordion component for better UI organization in assignment descriptions.
2026-09-03 09:51:27 +07:00
Yoga Pangestu
a809307272 feat: implement assignment submission functionality with validation and user permissions 2026-09-02 23:40:06 +07:00
Yoga Pangestu
be0897ad6e feat: enhance assignment management with user role-based filtering and attachment preview 2026-09-02 23:04:55 +07:00
Yoga Pangestu
2b7b4e2ab0 feat: enhance material management with user-specific filtering and attachment preview 2026-09-02 22:51:09 +07:00
Yoga Pangestu
753e7735a7 feat: implement schedule management with day of week enumeration and enhanced filtering options 2026-09-02 22:17:44 +07:00
Yoga Pangestu
fa4c4f37f3 feat: initialize search state from URL parameters in useServerTable hook 2026-09-02 21:11:12 +07:00
Yoga Pangestu
53afdb1fec feat: enhance student and lecturer data retrieval with improved joins and sorting 2026-09-02 19:00:39 +07:00
Yoga Pangestu
8528a3258d feat: enhance sorting in CourseService for improved course selection 2026-09-02 18:24:08 +07:00
Yoga Pangestu
a94ebfb77d feat: add Skripsi II course to CourseSeeder for semester 8 2026-09-02 18:23:28 +07:00
Yoga Pangestu
378ddebac9 feat: refactor DatabaseSeeder and UserSeeder for improved structure and local environment handling 2026-09-02 18:15:21 +07:00
Yoga Pangestu
cdd81caf22 feat: update CreateForm and EditForm to filter courses and lecturers based on selected lecturer, enhancing user experience 2026-09-02 18:04:14 +07:00
Yoga Pangestu
1b68cf6125 feat: enhance CourseClass pagination and UI by adding academic term and semester number details 2026-09-02 18:01:19 +07:00
Yoga Pangestu
c57787eb20 feat: add semester parity validation for course duplication and enhance UI for target term selection 2026-09-02 17:53:40 +07:00
Yoga Pangestu
5613078c00 feat: add course term parity validation for course selection in CreateForm and EditForm 2026-09-02 17:41:35 +07:00
Yoga Pangestu
c1cee7cd45 feat: add duplicate course classes functionality with validation and UI integration 2026-09-02 17:39:58 +07:00
Yoga Pangestu
0d27a9e706 feat: remove ClassEnrollmentRequest and related enrollment logic, simplifying class enrollment management 2026-09-02 16:45:59 +07:00
Yoga Pangestu
65618bf961 feat: enhance leadership management by adding role assignment and revocation for lecturers 2026-09-02 15:42:19 +07:00
Yoga Pangestu
03c3fc61af feat: implement bulk creation of course classes and enhance validation for lecturer assignment 2026-09-02 15:34:55 +07:00
Yoga Pangestu
584056e379 feat: enhance course class management by adding course class assignments and improving course selection UI 2026-09-02 15:07:41 +07:00
Yoga Pangestu
28a25fabfd feat: add student current semester handling in course registration logic 2026-09-02 14:38:08 +07:00
Yoga Pangestu
9892f60e5e feat: refactor course registration logic to use department-specific courses and improve submission payload structure 2026-09-02 13:59:02 +07:00
Yoga Pangestu
86863ff2f8 feat: enhance course pagination and grouping by department and semester 2026-09-02 13:38:40 +07:00
Yoga Pangestu
9823b879d8 refactor: remove redundant date validation logic from AcademicTermRequest 2026-09-02 13:26:55 +07:00
Yoga Pangestu
ded7217dbc feat: rename 'name' to 'academic_year' in AcademicTerm and related components for consistency 2026-09-02 13:20:18 +07:00
Yoga Pangestu
6c55fb5f36 fix: adjust padding in FormDialog component for better layout 2026-09-02 12:43:09 +07:00
Yoga Pangestu
0327b07669 feat: set default maxDate in DatePicker component 2026-09-02 11:56:00 +07:00
Yoga Pangestu
692b84cc1b feat: add open semesters badge to academic terms and remove course registration pages
- Added a new column to display open semesters in the academic terms table with badges.
- Removed course registration related pages and components including index, create, and show.
- Updated course registration types to reflect changes in the data structure.
- Refactored admin routes for course registrations to streamline submission handling.
2026-09-01 21:39:46 +07:00
Yoga Pangestu
6621757713 feat: add department selection and advisor filtering in student create and edit forms 2026-09-01 20:59:56 +07:00
Yoga Pangestu
a7aec7245b feat: rename "Dosen Wali" to "Pembimbing Akademik" in various components for consistency 2026-09-01 20:51:50 +07:00
Yoga Pangestu
2bb892ef50 feat: add lecturer management to Department module, including leadership fields and display in UI 2026-09-01 20:49:04 +07:00
Yoga Pangestu
10c3e42228 feat: add open_semesters field to AcademicTerm model and request, update related services and migration 2026-09-01 14:27:02 +07:00
Yoga Pangestu
93ddee8fd6 feat: implement many-to-many relationship for academic advising logs and students 2026-09-01 00:46:21 +07:00
Yoga Pangestu
a6c6966cf1 feat: update alert rendering for feedback and student status changes based on permissions 2026-09-01 00:08:45 +07:00
Yoga Pangestu
93c5514c22 Refactor Letter Request Management
- Created LetterRequestRequest and LetterRequestStatusRequest for handling form requests.
- Removed the old LetterRequestRequest class and updated the authorization logic.
- Changed the student relationship to user in the LetterRequest model.
- Updated LetterRequestService to reflect changes in the model and request handling.
- Modified permission catalog to include new permissions for letter request status updates.
- Updated database migrations and seeders to use user_id instead of student_id.
- Removed student-specific letter request pages and components, consolidating functionality under admin routes.
- Adjusted frontend components to accommodate new data structure and permissions.
- Enhanced the letter request columns to support status updates directly from the table.
2026-09-01 00:03:13 +07:00
Yoga Pangestu
7353b46975 feat: enhance paginated method to filter courses based on user roles 2026-08-31 22:46:53 +07:00
Yoga Pangestu
56c41db373 feat: refactor column definitions to use dynamic rendering based on permissions 2026-08-31 22:33:20 +07:00
Yoga Pangestu
4350f7d9ea feat: refactor navigation structure in AppSidebar to use permissions for dynamic item rendering 2026-08-31 16:34:31 +07:00
Yoga Pangestu
6834d47e2b feat: remove SiteHeader component from dashboard page 2026-08-31 16:31:20 +07:00
Yoga Pangestu
c1ba03d314 feat: remove 'developer' role from user seeder entries for mahasiswa 2026-08-31 16:26:39 +07:00
Yoga Pangestu
04f8f770bb feat: update LetterRequestRequest validation and enhance student selection with Combobox in forms 2026-08-31 16:25:36 +07:00
Yoga Pangestu
8be0d57e16 feat: add create and edit feedback functionality with corresponding UI components 2026-08-31 16:09:54 +07:00
Yoga Pangestu
b4ffdcb9dd feat: implement media upload functionality with controller, request, service, and migration 2026-08-31 16:00:40 +07:00
Yoga Pangestu
daeeb3eb52 feat: refactor feedback service methods and enhance feedback user details in UI 2026-08-31 15:37:18 +07:00
Yoga Pangestu
fdfa0bf6c7 feat: simplify feedback subject column rendering by removing unnecessary markup 2026-08-31 15:27:01 +07:00
Yoga Pangestu
89454c3cc4 feat: enhance FormDialog component with improved layout and utility class integration 2026-08-31 15:26:02 +07:00
Yoga Pangestu
ddbbf1cc7f feat: implement feedback management with status updates and permissions 2026-08-31 15:19:56 +07:00
Yoga Pangestu
ef3988f734 feat: reorganize imports and enhance schedule index layout with additional instructions 2026-08-31 14:59:21 +07:00
Yoga Pangestu
a702b506fd feat: enhance schedule layout for better responsiveness and styling 2026-08-31 14:54:18 +07:00
Yoga Pangestu
941de83949 feat: add pending feedback count to user notifications and sidebar 2026-08-31 14:48:02 +07:00
Yoga Pangestu
86ea75ec91 feat: implement authorization checks for various requests in academic and user management 2026-08-31 14:41:25 +07:00
Yoga Pangestu
23961969f4 feat: implement role and permission management with CRUD functionality 2026-08-30 23:47:58 +07:00
Yoga Pangestu
acda85b4e5 feat: implement log management system with viewing and downloading capabilities 2026-08-30 23:39:07 +07:00
Yoga Pangestu
5994f38f01 feat: implement permission checks for academic terms, courses, departments, and user management
- Added permission checks for creating, updating, and deleting academic terms, courses, and departments.
- Updated the routes to enforce permissions for various actions in the admin panel.
- Enhanced user management by adding permissions for administrators, lecturers, and students.
- Refactored components to conditionally render actions based on user permissions.
- Updated the auth type to include optional permissions array for user roles.
2026-08-30 23:27:31 +07:00
Yoga Pangestu
e9acf29c20 feat: implement academic advising and letter request management for lecturers and students 2026-08-30 22:13:26 +07:00
Yoga Pangestu
8d56ed16ac feat: enhance notification system with creator information and notification sending functionality 2026-08-30 21:47:34 +07:00
Yoga Pangestu
5cc33cce2c feat: implement announcement management with CRUD functionality 2026-08-30 21:11:30 +07:00
26a6771448 Merge pull request 'feat: enhance table styling with background color and text color adjustments' (#74) from feat/enhance-tabel-styling into dev
Reviewed-on: itm/siakad-itm#74
2026-08-30 22:02:58 +08:00
Yoga Pangestu
9cc8395848 feat: enhance table styling with background color and text color adjustments
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 21:02:36 +07:00
eaba8edf96 Merge pull request 'feat: update search placeholders and remove empty text in data tables' (#73) from feat/update-search-and-empty-datatable into dev
Reviewed-on: itm/siakad-itm#73
2026-08-30 21:58:34 +08:00
Yoga Pangestu
6b36d65eb9 feat: update search placeholders and remove empty text in data tables
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 20:58:19 +07:00
6f8e5fabb0 Merge pull request 'feat: remove unused settings routes from app sidebar' (#72) from feat/remove-unused-settings-route into dev
Reviewed-on: itm/siakad-itm#72
2026-08-30 21:52:32 +08:00
Yoga Pangestu
4761d611c8 feat: remove unused settings routes from app sidebar
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 20:52:16 +07:00
e50d6f873f Merge pull request 'feat: add course registration management for admin and student' (#71) from feat/add-course-registration into dev
Reviewed-on: itm/siakad-itm#71
2026-08-30 21:50:56 +08:00
Yoga Pangestu
8336504032 feat: add course registration management for admin and student
Some checks failed
tests / ci (pull_request) Has been cancelled
- Implemented CourseRegistrationShow component for admin to view registration details.
- Added course registration creation and listing for students.
- Enhanced student registration edit and create forms with improved error handling.
- Introduced new columns for course registration submissions in student view.
- Updated routes for course registration management in admin and student contexts.
- Added logging and status handling for course registration submissions.
- Improved type definitions for course registration and user roles.
2026-08-30 20:50:27 +07:00
62da33936b Merge pull request 'feat: add current semester field to student management and update related components' (#70) from feat/add-current-semester-field-to-studens into dev
Reviewed-on: itm/siakad-itm#70
2026-08-30 18:28:47 +08:00
Yoga Pangestu
0fbd3cc247 feat: add current semester field to student management and update related components
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 17:28:12 +07:00
1e30a74072 Merge pull request 'feat: simplify academic term selection logic and clean up imports in TuitionInvoiceIndex' (#69) from feat/default-academic-term-for-cummary-card-tuition into dev
Reviewed-on: itm/siakad-itm#69
2026-08-30 18:23:47 +08:00
Yoga Pangestu
40a6a8b49a feat: simplify academic term selection logic and clean up imports in TuitionInvoiceIndex
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 17:23:16 +07:00
63a9203aab Merge pull request 'feat: enhance Tuition Invoice management with summary statistics and improved filtering' (#68) from feat/add-summary-card-tuition-invoice into dev
Reviewed-on: itm/siakad-itm#68
2026-08-30 17:26:27 +08:00
Yoga Pangestu
cad1e109fe feat: enhance Tuition Invoice management with summary statistics and improved filtering
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 16:26:09 +07:00
ef137ca364 Merge pull request 'feat: implement PaymentMethod enum and integrate payment method handling in tuition payment processes' (#67) from feat/implement-filter into dev
Reviewed-on: itm/siakad-itm#67
2026-08-30 17:06:27 +08:00
Yoga Pangestu
2212498136 feat: implement PaymentMethod enum and integrate payment method handling in tuition payment processes
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 16:06:05 +07:00
d0793b739a Merge pull request 'feat: enhance Tuition Invoice management with academic term formatting and validation' (#66) from feat/enhance-tuition-invoice into dev
Reviewed-on: itm/siakad-itm#66
2026-08-30 16:23:00 +08:00
Yoga Pangestu
c4dbf54e4c feat: enhance Tuition Invoice management with academic term formatting and validation
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 15:22:32 +07:00
4183ec1e36 Merge pull request 'feat: enhance Tuition Invoice management with multi-student selection and validation' (#65) from feat/enhance-tuition-with-multiple-student into dev
Reviewed-on: itm/siakad-itm#65
2026-08-30 15:26:30 +08:00
Yoga Pangestu
1213d828b5 feat: enhance Tuition Invoice management with multi-student selection and validation
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 14:26:10 +07:00
783a0f6dda Merge pull request 'feat: implement Course Registration management with CRUD functionality' (#64) from refactor/move-Course-Registration-from-academic-class-to-manage into dev
Reviewed-on: itm/siakad-itm#64
2026-08-30 15:11:46 +08:00
Yoga Pangestu
a53bfa0037 feat: implement Course Registration management with CRUD functionality
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 14:11:30 +07:00
3c68462964 Merge pull request 'refactor: standardize import statements and replace anchor tags with Link component in multiple files' (#63) from refcator/replace-anchor-tags-with-Link into dev
Reviewed-on: itm/siakad-itm#63
2026-08-30 14:59:51 +08:00
Yoga Pangestu
adddd2fc2e refactor: standardize import statements and replace anchor tags with Link component in multiple files
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 13:59:33 +07:00
ac449e1353 Merge pull request 'refactor: update CourseClassRequest and CourseClassSeeder for lecturer validation and dynamic seeding' (#62) from refactor/update-CourseClassRequest-and-CourseClassSeeder into dev
Reviewed-on: itm/siakad-itm#62
2026-08-30 14:42:50 +08:00
Yoga Pangestu
cbed63f43c refactor: update CourseClassRequest and CourseClassSeeder for lecturer validation and dynamic seeding
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 13:42:30 +07:00
7af236a379 Merge pull request 'feat: update CourseSeeder with new course data and add reference images' (#61) from feat/update-CourseSeeder into dev
Reviewed-on: itm/siakad-itm#61
2026-08-30 14:34:42 +08:00
Yoga Pangestu
0e3bab43b2 feat: update CourseSeeder with new course data and add reference images
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 13:34:15 +07:00
40c55e9bd2 Merge pull request 'refactor: remove class_name from CourseClass and related components' (#60) from refactor/remove-class_name-and-fix-department-relation into dev
Reviewed-on: itm/siakad-itm#60
2026-08-30 14:15:08 +08:00
Yoga Pangestu
5710a57992 refactor: remove class_name from CourseClass and related components
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 13:14:53 +07:00
6c28782484 Merge pull request 'feat: add Course management functionality with CRUD operations and UI integration' (#59) from refactor/move-course-form-manage-to-master into dev
Reviewed-on: itm/siakad-itm#59
2026-08-30 13:47:11 +08:00
Yoga Pangestu
a6d1fa2cf7 feat: add Course management functionality with CRUD operations and UI integration
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 12:46:43 +07:00
1fe2b30156 Merge pull request 'feat: enhance department handling by adding degree level and formatting labels' (#58) from feat/adding-degre-level-and-formatting-label into dev
Reviewed-on: itm/siakad-itm#58
2026-08-30 13:35:39 +08:00
Yoga Pangestu
8e2f2da5fc feat: enhance department handling by adding degree level and formatting labels
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 12:35:22 +07:00
71439aab4d Merge pull request 'feat: enhance student creation and editing forms with combobox for academic advisor selection' (#57) from feat/enhance-Dosen-Wali-input-combobox into dev
Reviewed-on: itm/siakad-itm#57
2026-08-30 13:24:33 +08:00
Yoga Pangestu
f32c844e65 feat: enhance student creation and editing forms with combobox for academic advisor selection
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 12:24:09 +07:00
1faa6ae588 Merge pull request 'feat: add enrollment year filter and export functionality for students' (#56) from feat/add-enrollment-year-filter into dev
Reviewed-on: itm/siakad-itm#56
2026-08-30 13:21:32 +08:00
Yoga Pangestu
c0858e02ec feat: add enrollment year filter and export functionality for students
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 12:21:03 +07:00
82d7b9bb1e Merge pull request 'Refactor lecturer management to support multiple departments' (#55) from refactor/lecturer-management-to-support-multiple-departments into dev
Reviewed-on: itm/siakad-itm#55
2026-08-30 13:18:37 +08:00
Yoga Pangestu
07a00a92a2 Refactor lecturer management to support multiple departments
Some checks failed
tests / ci (pull_request) Has been cancelled
- Updated LecturerController to load multiple departments for lecturers.
- Modified LecturerRequest to accept an array of department IDs instead of a single department ID.
- Changed Department model to establish a many-to-many relationship with Lecturer.
- Adjusted Lecturer model to reflect the new many-to-many relationship with Department.
- Updated LecturerService to handle multiple departments during creation and updates.
- Created LecturerFactory and StudentFactory for generating test data.
- Added a migration for the new lecturer_department pivot table.
- Refactored seeder classes to accommodate the new structure for lecturers and departments.
- Enhanced frontend components for creating and editing lecturers to support multiple department selection.
2026-08-30 12:18:13 +07:00
bf6f4b19b7 Merge pull request 'feat: optimize user queries in AdministratorService, LecturerService, and StudentService for better performance' (#54) from feat/optimize-user-queries into dev
Reviewed-on: itm/siakad-itm#54
2026-08-30 12:35:21 +08:00
Yoga Pangestu
1b5367c78a feat: optimize user queries in AdministratorService, LecturerService, and StudentService for better performance
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 11:35:10 +07:00
7bdfd25adb Merge pull request 'feat: simplify pagination methods by removing sort and direction parameters' (#53) from feat/removing-sort-and-direction-parameters into dev
Reviewed-on: itm/siakad-itm#53
2026-08-30 12:23:26 +08:00
Yoga Pangestu
62ac0a4c1d feat: simplify pagination methods by removing sort and direction parameters
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 11:23:02 +07:00
b76d82c2f0 Merge pull request 'feat: add active status management for academic terms, users, and students with new UpdateStatusRequest' (#52) from feat/add-status-management-academic-terms into dev
Reviewed-on: itm/siakad-itm#52
2026-08-30 12:18:37 +08:00
Yoga Pangestu
982051ee2e feat: add active status management for academic terms, users, and students with new UpdateStatusRequest
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 11:18:20 +07:00
62f4d09026 Merge pull request 'feat: implement user status management with active/inactive toggle' (#51) from feat-implement-user-status into dev
Reviewed-on: itm/siakad-itm#51
2026-08-30 12:08:30 +08:00
Yoga Pangestu
9c03876a71 feat: implement user status management with active/inactive toggle
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 11:08:08 +07:00
249a3c86b4 Merge pull request 'feat: refactor student request classes to improve organization and structure' (#50) from refactor/student-request-classes into dev
Reviewed-on: itm/siakad-itm#50
2026-08-30 11:58:23 +08:00
Yoga Pangestu
c620713651 feat: refactor student request classes to improve organization and structure
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 10:58:05 +07:00
67252c3f51 Merge pull request 'feat: add export functionality for lecturers and students, including styled heading rows in exports' (#49) from feat/add-export-functionality-for-lecturers-and-students into dev
Reviewed-on: itm/siakad-itm#49
2026-08-30 11:57:13 +08:00
Yoga Pangestu
c77f6ae603 feat: add export functionality for lecturers and students, including styled heading rows in exports
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-30 10:56:23 +07:00
c321c99ac1 Merge pull request 'feat: implement student status update functionality and validation' (#48) from feat/implement-student-status into dev
Reviewed-on: itm/siakad-itm#48
2026-08-26 02:13:33 +08:00
Yoga Pangestu
d59e19ed3e feat: implement student status update functionality and validation
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-26 01:13:18 +07:00
3418b3c44e Merge pull request 'feat: update terminology from 'password' to 'kata sandi' across various components and pages' (#47) from feat/update-terminology-from-password-to-kata-sandi into dev
Reviewed-on: itm/siakad-itm#47
2026-08-26 01:58:19 +08:00
Yoga Pangestu
79261bd0e0 feat: update terminology from 'password' to 'kata sandi' across various components and pages
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-26 00:57:49 +07:00
c323fcccb5 Merge pull request 'feat: add GenderBadge and StudentStatusBadge components; update user columns to utilize new badges' (#46) from feat/add-gender-and-status-badge-for-user-manu into dev
Reviewed-on: itm/siakad-itm#46
2026-08-26 01:51:44 +08:00
Yoga Pangestu
1012127196 feat: add GenderBadge and StudentStatusBadge components; update user columns to utilize new badges
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-26 00:51:22 +07:00
5c0cadcebf Merge pull request 'feat: add filter functionality to various admin pages' (#45) from feat/add-filter-funcionality into dev
Reviewed-on: itm/siakad-itm#45
2026-08-26 01:32:53 +08:00
Yoga Pangestu
a76ee85c24 feat: add filter functionality to various admin pages
Some checks failed
tests / ci (pull_request) Has been cancelled
- Implemented filter dialogs in the following pages:
  - Academic Classes Assignments
  - Course Registrations
  - Materials
  - Announcements
  - Feedback
  - Tuition Invoices
  - Course Classes
  - Courses
  - Academic Terms
  - Academic Advising Logs
  - Letter Requests
  - Administrators
  - Lecturers
  - Students

- Updated the useServerTable hook to support filter parameters.
- Enhanced the UI with filter options for better data management and retrieval.
2026-08-26 00:32:16 +07:00
a8f864aa28 Merge pull request 'feat: enhance UI components with icons and improve navigation links' (#44) from feat/enhance-UI-components-with-icon into dev
Reviewed-on: itm/siakad-itm#44
2026-08-26 00:42:40 +08:00
Yoga Pangestu
9626181d46 feat: enhance UI components with icons and improve navigation links
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 23:42:14 +07:00
02745ba207 Merge pull request 'refactor: simplify layout structure in SettingsLayout component' (#43) from refactor/simplify-SettingsLayout into dev
Reviewed-on: itm/siakad-itm#43
2026-08-26 00:31:02 +08:00
Yoga Pangestu
c7bb237f8a refactor: simplify layout structure in SettingsLayout component
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 23:30:32 +07:00
76e12b9dba Merge pull request 'feat: add tooltip to sidebar menu buttons in NavMain and NavSecondary components' (#42) from feat/add-tooltip-to-sidebar into dev
Reviewed-on: itm/siakad-itm#42
2026-08-26 00:26:14 +08:00
Yoga Pangestu
658a161907 feat: add tooltip to sidebar menu buttons in NavMain and NavSecondary components
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 23:25:33 +07:00
0a7d1e793d Merge pull request 'feat: add academic advising logs and letter requests management' (#41) from refactor/organized-folder-structure into dev
Reviewed-on: itm/siakad-itm#41
2026-08-26 00:05:50 +08:00
Yoga Pangestu
c74a7c7792 feat: add academic advising logs and letter requests management
Some checks failed
tests / ci (pull_request) Has been cancelled
- Implemented academic advising logs functionality with create, edit, and delete capabilities.
- Created UI components for displaying academic advising logs in a data table format.
- Added letter requests management with similar CRUD operations and UI components.
- Refactored routes to organize academic classes, announcements, finances, and services under appropriate namespaces.
2026-08-25 23:05:19 +07:00
6fc1e2772e Merge pull request 'feat: standardize pagination method naming and signatures across services' (#40) from feat/standardize-pagination-method into dev
Reviewed-on: itm/siakad-itm#40
2026-08-25 22:30:36 +08:00
Yoga Pangestu
03b1fcd7e7 feat: standardize pagination method naming and signatures across services
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 21:30:16 +07:00
6698211396 Merge pull request 'feat: Refactor Admin Manage structure into sub-namespaces for better organization' (#39) from feat/reafctor-admin-manage-structure-into-sub-namespaces into dev
Reviewed-on: itm/siakad-itm#39
2026-08-25 22:18:09 +08:00
Yoga Pangestu
353829e053 feat: Refactor Admin Manage structure into sub-namespaces for better organization
Some checks failed
tests / ci (pull_request) Has been cancelled
- Created sub-namespaces under Admin\Manage for Course, Academic, Announcement, Finance, and Service.
- Added new FormRequest classes for handling validation in each sub-namespace.
- Implemented Service classes for managing business logic related to each resource.
- Updated routes to reflect the new sub-namespace structure.
- Enhanced code organization and maintainability by grouping related functionalities.
2026-08-25 21:17:48 +07:00
9abd50f322 Merge pull request 'feat: refactor controllers and update department service usage for consistency' (#38) from feat/refactor-controllers into dev
Reviewed-on: itm/siakad-itm#38
2026-08-25 20:52:12 +08:00
Yoga Pangestu
e6f272bb59 feat: refactor controllers and update department service usage for consistency
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 19:51:27 +07:00
5901f8e649 Merge pull request 'feat: update sidebar navigation labels and structure for better clarity' (#37) from feat/order-sidebar-menu into dev
Reviewed-on: itm/siakad-itm#37
2026-08-25 20:39:59 +08:00
Yoga Pangestu
f7343f4892 feat: update sidebar navigation labels and structure for better clarity
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 19:39:37 +07:00
5ae30e5804 Merge pull request 'feat: implement feedback management system with controller, service, and frontend components' (#36) from feat/implement-feedback-management into dev
Reviewed-on: itm/siakad-itm#36
2026-08-25 20:37:56 +08:00
Yoga Pangestu
e7664de711 feat: implement feedback management system with controller, service, and frontend components
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 19:37:00 +07:00
ce90904b84 Merge pull request 'feat: implement HasValues trait for enums and update related code for consistency' (#35) from feat/implement-HasValues-trait-for-enum into dev
Reviewed-on: itm/siakad-itm#35
2026-08-25 20:17:02 +08:00
Yoga Pangestu
b59804342b feat: implement HasValues trait for enums and update related code for consistency
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 19:15:40 +07:00
13d5ce5853 Merge pull request 'fix: standardize closure syntax and ensure newline at end of file in multiple files' (#34) from fix/standardize-closure-syntax into dev
Reviewed-on: itm/siakad-itm#34
2026-08-25 19:17:16 +08:00
Yoga Pangestu
9c2bb62666 fix: standardize closure syntax and ensure newline at end of file in multiple files
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 18:16:54 +07:00
d49790ca7e Merge pull request 'Refactor admin settings structure and implement profile and security management' (#33) from refactor/admin-setting into dev
Reviewed-on: itm/siakad-itm#33
2026-08-25 18:58:16 +08:00
Yoga Pangestu
70d37741ef Refactor admin settings structure and implement profile and security management
Some checks failed
tests / ci (pull_request) Has been cancelled
- Moved profile and security settings from the general settings route to a dedicated admin settings route.
- Created new components and pages for managing user profile and security settings.
- Updated user menu to link to the new admin settings pages.
- Removed old settings pages and routes that are no longer in use.
- Added tests for profile and security settings functionality.
2026-08-25 17:57:47 +07:00
13ccb4d19c Merge pull request 'feat: implement feedback system with CRUD functionality' (#32) from feat/implement-feedback-system into dev
Reviewed-on: itm/siakad-itm#32
2026-08-25 15:26:33 +08:00
Yoga Pangestu
62f8c167e8 feat: implement feedback system with CRUD functionality
Some checks failed
tests / ci (pull_request) Has been cancelled
- Add FeedbackController to handle feedback submissions and management.
- Create Feedback model and migration for feedbacks table.
- Introduce FeedbackStatus and FeedbackType enums for better type handling.
- Implement FeedbackService for business logic related to feedback.
- Create FeedbackRequest for validation of feedback data.
- Add Tiptap rich text editor for feedback message input.
- Develop frontend components for displaying and managing feedback.
- Add routes for feedback management in web.php.
- Create utility types for feedback in TypeScript.
- Update app-sidebar to include feedback navigation.
2026-08-25 14:25:50 +07:00
f698e5f567 Merge pull request 'feat: add academic advising log management with CRUD operations and UI integration' (#31) from feat/add-academic-advising-log into dev
Reviewed-on: itm/siakad-itm#31
2026-08-25 14:30:30 +08:00
Yoga Pangestu
daff18996f feat: add academic advising log management with CRUD operations and UI integration
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 13:30:02 +07:00
4577d45e62 Merge pull request 'feat: implement notification system with CRUD operations and UI integration' (#30) from feat/implement-notification-system into dev
Reviewed-on: itm/siakad-itm#30
2026-08-25 14:28:59 +08:00
Yoga Pangestu
0397959f60 feat: implement notification system with CRUD operations and UI integration
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 13:21:11 +07:00
e615547317 Merge pull request 'feat: enhance scrollbar styling for better visibility and aesthetics' (#29) from feat/enhance-scrollbacr-styling into dev
Reviewed-on: itm/siakad-itm#29
2026-08-25 02:20:18 +08:00
Yoga Pangestu
095c51cbfc feat: enhance scrollbar styling for better visibility and aesthetics
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 01:19:50 +07:00
6ceb297a52 Merge pull request 'feat: implement letter request management with CRUD operations' (#28) from feat/implement-letter-request into dev
Reviewed-on: itm/siakad-itm#28
2026-08-25 02:14:33 +08:00
Yoga Pangestu
e3633caea5 feat: implement letter request management with CRUD operations
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 01:14:10 +07:00
db5560e261 Merge pull request 'feat: add announcement management functionality with CRUD operations' (#27) from feat/add-announcement-management into dev
Reviewed-on: itm/siakad-itm#27
2026-08-25 02:03:37 +08:00
Yoga Pangestu
e268153971 feat: add announcement management functionality with CRUD operations
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 01:03:20 +07:00
24cda20051 Merge pull request 'feat: add course registration management functionality with CRUD operations' (#26) from feat/add-course-registration-management into dev
Reviewed-on: itm/siakad-itm#26
2026-08-25 01:53:00 +08:00
Yoga Pangestu
2f23747781 feat: add course registration management functionality with CRUD operations
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-25 00:51:50 +07:00
4ff18c23c5 Merge pull request 'feat: add tuition invoice and payment management' (#25) from feat/add-tuition-invoice into dev
Reviewed-on: itm/siakad-itm#25
2026-08-25 01:35:11 +08:00
Yoga Pangestu
8e4d0ba530 feat: add tuition invoice and payment management
Some checks failed
tests / ci (pull_request) Has been cancelled
- Implemented StudentService to retrieve all students for selection.
- Created migration for tuition_invoices and tuition_payments tables.
- Added seeders for TuitionInvoice and TuitionPayment with sample data.
- Updated DatabaseSeeder to include new seeders.
- Developed UI components for managing tuition invoices and payments, including forms and data tables.
- Introduced RupiahInput component for formatted currency input.
- Added routes for tuition invoices and payments management.
- Defined TypeScript types for tuition invoices and payments.
2026-08-25 00:34:32 +07:00
605f451a8e Merge pull request 'feat: add attendance management functionality with CRUD operations' (#24) from feat/add-attendance-management into dev
Reviewed-on: itm/siakad-itm#24
2026-08-24 19:47:27 +08:00
Yoga Pangestu
25c7c8b13f feat: add attendance management functionality with CRUD operations
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-24 18:46:31 +07:00
78de5a4131 Merge pull request 'feat: add schedule management functionality with CRUD operations' (#23) from feat/add-schedule-management into dev
Reviewed-on: itm/siakad-itm#23
2026-08-24 19:33:25 +08:00
Yoga Pangestu
73f41729d1 feat: add schedule management functionality with CRUD operations
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-24 18:32:44 +07:00
424a2cd6e8 Merge pull request 'feat: add assignment and submission management features' (#22) from feat/add-assinment-and-submission into dev
Reviewed-on: itm/siakad-itm#22
2026-08-24 19:16:25 +08:00
Yoga Pangestu
b0e16a6cd5 feat: add assignment and submission management features
- Created SubmissionService to handle submission logic for assignments.
- Added migrations for assignments and submissions tables.
- Implemented AssignmentSeeder and SubmissionSeeder for initial data.
- Updated DatabaseSeeder to include new seeders.
- Enhanced app sidebar to include assignments navigation.
- Developed datetime field component for better date and time input.
- Created assignment management pages with data tables for assignments and submissions.
- Implemented forms for creating and editing assignments and submissions.
- Added routes for assignment and submission management in admin panel.
- Defined types for assignments and submissions to improve type safety.
2026-08-24 18:15:44 +07:00
2b3c3cae03 Merge pull request 'fix: add full width class to Attachment component in FileUploadField' (#21) from fix/add-width-class-to-attachment into dev
Reviewed-on: itm/siakad-itm#21
2026-08-24 17:51:25 +08:00
Yoga Pangestu
04f9f885ed fix: add full width class to Attachment component in FileUploadField 2026-08-24 16:50:37 +07:00
2103b4ae11 Merge pull request 'feat: implement material management functionality with CRUD operations' (#20) from feat/implement-material-crud into dev
Reviewed-on: itm/siakad-itm#20
2026-08-21 21:28:28 +08:00
Yoga Pangestu
7c50a493c3 feat: implement material management functionality with CRUD operations 2026-08-21 20:28:03 +07:00
ff4173bb36 Merge pull request 'feat: add course and class enrollment management' (#19) from feat/add-course-and-class-enrollment-management into dev
Reviewed-on: itm/siakad-itm#19
2026-08-21 20:06:36 +08:00
Yoga Pangestu
015cbc229b feat: add course and class enrollment management
- Implemented ClassEnrollmentIndex component for managing class enrollments, including adding and removing students.
- Created CourseClassIndex component for managing course classes with CRUD functionality.
- Developed columns for course management in createCourseColumns function.
- Added CourseIndex component for managing courses with CRUD operations.
- Introduced types for class enrollment and course class to enhance type safety.
- Updated routes to include endpoints for managing courses, course classes, and enrollments.
2026-08-21 19:06:05 +07:00
8c81e990b8 Merge pull request 'refactor: streamline success message handling in destroy methods of AcademicTermController and DepartmentController' (#18) from refactor/return-back-inertia-handling into dev
Reviewed-on: itm/siakad-itm#18
2026-08-21 15:34:25 +08:00
Yoga Pangestu
d489f6117f refactor: streamline success message handling in destroy methods of AcademicTermController and DepartmentController 2026-08-21 14:33:47 +07:00
b052a85d24 Merge pull request 'fix: correct relationship key for academic advisor in StudentController' (#17) from fix/correct-relationship-student into dev
Reviewed-on: itm/siakad-itm#17
2026-08-21 15:31:16 +08:00
Yoga Pangestu
5bfbf0d388 fix: correct relationship key for academic advisor in StudentController 2026-08-21 14:30:29 +07:00
3f2fe8e935 Merge pull request 'feat: add lecturer and student management pages with CRUD functionality' (#16) from feat/add-user-and-department-crud into dev
Reviewed-on: itm/siakad-itm#16
2026-08-21 15:15:32 +08:00
Yoga Pangestu
2885de2c20 feat: add lecturer and student management pages with CRUD functionality
- Implemented LecturerEdit and LecturerIndex components for managing lecturers.
- Created StudentCreate and StudentEdit components for adding and editing students.
- Developed StudentIndex component for listing students with search and pagination.
- Added department and user types for better type safety.
- Updated routes for lecturers and students, including reset password functionality.
- Removed AppSidebar from dashboard for a cleaner layout.
2026-08-21 14:14:01 +07:00
32f700ea84 Merge pull request 'feat/add-shadcn-component' (#15) from feat/add-shadcn-component into dev
Reviewed-on: itm/siakad-itm#15
2026-08-18 15:59:56 +08:00
Yoga Pangestu
3b35f0eef9 feat: add PhoneInput and Textarea components; update package dependencies 2026-08-18 14:58:08 +07:00
Yoga Pangestu
579f2346f7 fix: update destroy method to return back instead of redirecting to index 2026-08-18 13:49:43 +07:00
bead96499f Merge pull request 'feat: add media library and permission management' (#14) from feat/install-spatie-media into dev
Reviewed-on: itm/siakad-itm#14
2026-08-05 16:59:21 +08:00
Yoga Pangestu
bffc5014ed feat: add media library and permission management
- Created media-library configuration file for managing media uploads and conversions.
- Added permission configuration file to define roles and permissions.
- Implemented migration for media table to store media items with necessary attributes.
- Created migration for permission tables to manage roles and permissions relationships.
- Updated DatabaseSeeder to include RolePermissionSeeder for seeding roles and permissions.
- Added RolePermissionSeeder to define specific permissions and roles for the application.
- Modified UserSeeder to assign roles to users during seeding.
2026-08-05 15:57:51 +07:00
b80a52460a Merge pull request 'feat: add active state to SidebarMenuButton based on current URL' (#13) from feat/active-state-sidebar into dev
Reviewed-on: itm/siakad-itm#13
2026-08-05 14:57:20 +08:00
Yoga Pangestu
5dd6145d59 feat: add active state to SidebarMenuButton based on current URL
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-05 13:56:03 +07:00
663d88c80d Merge pull request 'feat: integrate useFlashToast hook into AppLayout component' (#12) from feat/integrate-toast into dev
Reviewed-on: itm/siakad-itm#12
2026-08-05 14:52:38 +08:00
Yoga Pangestu
4929c51d24 feat: integrate useFlashToast hook into AppLayout component
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-05 13:51:28 +07:00
fa76c64730 Merge pull request 'feat: implement academic term management with CRUD functionality' (#11) from feat/academic-term-management into dev
Reviewed-on: itm/siakad-itm#11
2026-08-05 14:49:14 +08:00
Yoga Pangestu
400e8d8c8a feat: implement academic term management with CRUD functionality
Some checks failed
tests / ci (pull_request) Has been cancelled
- Added Semester enum with labels for odd and even semesters.
- Created AcademicTerm model, controller, service, and request for handling academic terms.
- Implemented paginated listing, creation, updating, and deletion of academic terms.
- Added routes for academic terms management under admin/master.
- Created frontend components for displaying and managing academic terms, including forms and data tables.
- Integrated confirmation dialog for delete actions and form dialogs for creating and editing academic terms.
- Added seeder for initial academic term data.
2026-08-05 13:48:36 +07:00
75178c12a8 Merge pull request 'feat: add reusable components for dialogs and headers' (#10) from feat/reusable-dialogs-headers into dev
Reviewed-on: itm/siakad-itm#10
2026-08-05 14:47:06 +08:00
Yoga Pangestu
1b5510b4e5 feat: add reusable components for dialogs and headers
Some checks failed
tests / ci (pull_request) Has been cancelled
- Implemented DeleteConfirmDialog for confirming deletions.
- Created FormDialog for handling forms within dialogs.
- Added PageHeader component for consistent page headers.
- Introduced RowActions component for action buttons with tooltips.
- Added useServerTable hook for managing server-side table interactions.
- Updated DatePicker component with default month and dropdown caption layout.
- Removed outdated data.json file.
2026-08-05 13:46:38 +07:00
14618f1e3e Merge pull request 'feat: Add Calendar, Popover, and RadioGroup components with styling and functionality' (#9) from feat/add-popover-calendar-input-group-component into dev
Reviewed-on: itm/siakad-itm#9
2026-08-05 14:23:26 +08:00
Yoga Pangestu
b10991ea6c feat: Add Calendar, Popover, and RadioGroup components with styling and functionality
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-05 13:22:37 +07:00
8dde2f8e26 Merge pull request 'refactor: enhance component structure and styling for improved layout and usability' (#8) from refactor/fixed-header into dev
Reviewed-on: itm/siakad-itm#8
2026-08-03 19:36:46 +08:00
Yoga Pangestu
3905c2ce2e refactor: enhance component structure and styling for improved layout and usability
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-03 18:36:04 +07:00
d405bfb022 Merge pull request 'Refactor code structure for improved readability and maintainability' (#7) from refactor/change-laravel-logo into dev
Reviewed-on: itm/siakad-itm#7
2026-08-03 19:19:24 +08:00
Yoga Pangestu
3e44029431 Refactor code structure for improved readability and maintainability
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-08-03 18:18:12 +07:00
13d6038cb9 Merge pull request 'feat: Refactor UI components and add new Drawer, Table, and Tabs components' (#6) from feat/dashboard-template into dev
Reviewed-on: itm/siakad-itm#6
2026-08-03 16:58:08 +08:00
Yoga Pangestu
fbf29ad4e9 feat: Refactor UI components and add new Drawer, Table, and Tabs components
Some checks failed
tests / ci (pull_request) Has been cancelled
- Removed unnecessary "use client" directive from checkbox and select components.
- Added new Drawer component with associated subcomponents (DrawerTrigger, DrawerClose, etc.) for better UI management.
- Introduced Table component with TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, and TableCaption for structured data display.
- Added Tabs component with TabsList, TabsTrigger, and TabsContent for tabbed navigation.
- Updated user-info component to use full_name instead of name for better clarity.
- Modified user-menu-content to include translations for settings and logout options.
- Added data.json file for mock data in the dashboard.
- Updated dashboard page to utilize new components and improved layout.
- Refactored User type to replace name with full_name for consistency across the application.
2026-08-03 15:57:38 +07:00
0b739527d8 Merge pull request 'refactor: update component imports and improve styling' (#5) from refactor/install-shadcn-and-update-component into dev
Reviewed-on: itm/siakad-itm#5
2026-08-03 16:20:15 +08:00
Yoga Pangestu
8f692c44c7 refactor: update component imports and improve styling
Some checks failed
tests / ci (pull_request) Has been cancelled
- Refactored imports in Select, Separator, Sheet, Sidebar, Skeleton, Sonner, Toggle, and Tooltip components to use new radix-ui package.
- Enhanced styling for various components, including SelectTrigger, SelectContent, and TooltipContent for better visual consistency.
- Introduced a new hook `useIsMobile` to manage mobile state detection.
- Adjusted class names and styles for better responsiveness and accessibility across components.
- Added support for custom spacing in ToggleGroup component.
2026-08-03 15:19:17 +07:00
b8e807d30b Merge pull request 'feat: update user registration to use username instead of name, add user profile model and migration, and implement gender enum' (#4) from feat/user-profile-and-username into dev
Reviewed-on: itm/siakad-itm#4
2026-08-03 14:49:32 +08:00
Yoga Pangestu
5cc8ba26b2 feat: update user registration to use username instead of name, add user profile model and migration, and implement gender enum
Some checks failed
tests / ci (pull_request) Has been cancelled
- Changed user creation to use 'username' instead of 'name'.
- Added UserProfile model with relationships to User and gender enum.
- Updated validation rules for username and email.
- Modified migrations to create user_profiles table and adjust users table.
- Updated seeder to create users with profiles.
- Translated various UI texts to Indonesian and improved layout components.
2026-08-03 13:43:30 +07:00
4ff5a23279 Merge pull request 'buat model mahasiswa' (#2) from feat/mahasiswa into dev
Reviewed-on: itm/siakad-itm#2
2026-07-29 19:46:50 +08:00
Yoga Pangestu
4e7675068b buat model mahasiswa
Some checks failed
tests / ci (pull_request) Has been cancelled
2026-07-29 18:41:48 +07:00
433 changed files with 38452 additions and 10425 deletions

View File

@ -0,0 +1,80 @@
---
name: socialite-development
description: "Manages OAuth social authentication with Laravel Socialite. Activate when adding social login providers; configuring OAuth redirect/callback flows; retrieving authenticated user details; customizing scopes or parameters; setting up community providers; testing with Socialite fakes; or when the user mentions social login, OAuth, Socialite, or third-party authentication."
license: MIT
metadata:
author: laravel
---
# Socialite Authentication
## Documentation
Use `search-docs` for detailed Socialite patterns and documentation (installation, configuration, routing, callbacks, testing, scopes, stateless auth).
## Available Providers
Built-in: `facebook`, `twitter`, `twitter-oauth-2`, `linkedin`, `linkedin-openid`, `google`, `github`, `gitlab`, `bitbucket`, `slack`, `slack-openid`, `twitch`
Community: 150+ additional providers at [socialiteproviders.com](https://socialiteproviders.com). For provider-specific setup, use `WebFetch` on `https://socialiteproviders.com/{provider-name}`.
Configuration key in `config/services.php` must match the driver name exactly — note the hyphenated keys: `twitter-oauth-2`, `linkedin-openid`, `slack-openid`.
Twitter/X: Use `twitter-oauth-2` (OAuth 2.0) for new projects. The legacy `twitter` driver is OAuth 1.0. Driver names remain unchanged despite the platform rebrand.
Community providers differ from built-in providers in the following ways:
- Installed via `composer require socialiteproviders/{name}`
- Must register via event listener — NOT auto-discovered like built-in providers
- Use `search-docs` for the registration pattern
## Adding a Provider
### 1. Configure the provider
Add the provider's `client_id`, `client_secret`, and `redirect` to `config/services.php`. The config key must match the driver name exactly.
### 2. Create redirect and callback routes
Two routes are needed: one that calls `Socialite::driver('provider')->redirect()` to send the user to the OAuth provider, and one that calls `Socialite::driver('provider')->user()` to receive the callback and retrieve user details.
### 3. Authenticate and store the user
In the callback, use `updateOrCreate` to find or create a user record from the provider's response (`id`, `name`, `email`, `token`, `refreshToken`), then call `Auth::login()`.
### 4. Customize the redirect (optional)
- `scopes()` — merge additional scopes with the provider's defaults
- `setScopes()` — replace all scopes entirely
- `with()` — pass optional parameters (e.g., `['hd' => 'example.com']` for Google)
- `asBotUser()` — Slack only; generates a bot token (`xoxb-`) instead of a user token (`xoxp-`). Must be called before both `redirect()` and `user()`. Only the `token` property will be hydrated on the user object.
- `stateless()` — for API/SPA contexts where session state is not maintained
### 5. Verify
1. Config key matches driver name exactly (check the list above for hyphenated names)
2. `client_id`, `client_secret`, and `redirect` are all present
3. Redirect URL matches what is registered in the provider's OAuth dashboard
4. Callback route handles denied grants (when user declines authorization)
Use `search-docs` for complete code examples of each step.
## Additional Features
Use `search-docs` for usage details on: `enablePKCE()`, `userFromToken($token)`, `userFromTokenAndSecret($token, $secret)` (OAuth 1.0), retrieving user details.
User object: `getId()`, `getName()`, `getEmail()`, `getAvatar()`, `getNickname()`, `token`, `refreshToken`, `expiresIn`, `approvedScopes`
## Testing
Socialite provides `Socialite::fake()` for testing redirects and callbacks. Use `search-docs` for faking redirects, callback user data, custom token properties, and assertion methods.
## Common Pitfalls
- Config key must match driver name exactly — hyphenated drivers need hyphenated keys (`linkedin-openid`, `slack-openid`, `twitter-oauth-2`). Mismatch silently fails.
- Every provider needs `client_id`, `client_secret`, and `redirect` in `config/services.php`. Missing any one causes cryptic errors.
- `scopes()` merges with defaults; `setScopes()` replaces all scopes entirely.
- Missing `stateless()` in API/SPA contexts causes `InvalidStateException`.
- Redirect URL in `config/services.php` must exactly match the provider's OAuth dashboard (including trailing slashes and protocol).
- Do not pass `state`, `response_type`, `client_id`, `redirect_uri`, or `scope` via `with()` — these are reserved.
- Community providers require event listener registration via `SocialiteWasCalled`.
- `user()` throws when the user declines authorization. Always handle denied grants.

View File

@ -8,6 +8,10 @@ APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI="${APP_URL}/auth/google/callback"
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database

View File

@ -26,10 +26,8 @@ jobs:
tools: composer:v2
coverage: none
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- name: Setup Application
run: composer setup

3
.gitignore vendored
View File

@ -19,8 +19,7 @@
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
bun-debug.log
/auth.json
/.fleet
/.idea

1
.npmrc
View File

@ -1 +0,0 @@
ignore-scripts=true

View File

@ -14,6 +14,7 @@ ## Foundational Context
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v13
- laravel/prompts (PROMPTS) - v0
- laravel/socialite (SOCIALITE) - v5
- laravel/wayfinder (WAYFINDER) - v0
- larastan/larastan (LARASTAN) - v3
- laravel/boost (BOOST) - v2
@ -51,7 +52,7 @@ ## Application Structure & Architecture
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `bun run build`, `bun run dev`, or `composer run dev`. Ask them.
## Documentation Files
@ -174,7 +175,7 @@ ## Testing
## Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `bun run build` or ask the user to run `bun run dev` or `composer run dev`.
=== wayfinder/core rules ===

354
BEST_PRACTICE.md Normal file
View File

@ -0,0 +1,354 @@
# Best Practice Laravel
Panduan konvensi kode untuk **semua project** dengan tech stack: **Laravel 13 (PHP 8.3)**, **Inertia + React + TypeScript**, **Laravel Wayfinder**, **Spatie Permission**, **Pest**, **Larastan**, **Laravel Pint**.
Dokumen ini bersifat reusable antar-project — jangan isi dengan nama domain/fitur spesifik satu project saja. Kalau sebuah project punya pengecualian dari aturan di sini, catat pengecualiannya di `CLAUDE.md`/README project tersebut, bukan mengubah dokumen ini.
Semua kode baru (controller, service, request, model, enum, komponen React) **wajib** mengikuti dokumen ini. Saat mengubah kode lama yang menyimpang, samakan dengan konvensi di sini sekalian (boy scout rule), kecuali perubahan di luar scope task yang sedang dikerjakan.
---
## 1. Konsistensi Struktur & Penamaan
- **Nama method harus konsisten** untuk fungsi yang setara. Jangan sampai controller A punya `store()` tapi controller B untuk hal yang sama pakai `save()`.
- **Urutan method harus konsisten** di semua Controller, Service, Model, dan class lain. Urutan baku untuk resource controller:
1. `index()`
2. `create()` (jika ada, hanya untuk non-Inertia modal-less form)
3. `store()`
4. `show()` (jika ada)
5. `edit()` (jika ada)
6. `update()`
7. `destroy()`
8. Method kustom lain (`updateStatus()`, `resetPassword()`, dll) **selalu ditaruh setelah** ketujuh method di atas, bukan disisipkan di tengah.
Contoh salah — method kustom nyempil di antara `update()` dan `destroy()`:
```php
index(), create(), store(), edit(), update(), resetPassword(), destroy()
```
Contoh benar:
```php
index(), create(), store(), edit(), update(), destroy(), resetPassword()
```
- **Organisasi Controller/Request/Service by domain.** Kelompokkan berdasarkan domain bisnis project yang bersangkutan, konsisten di ketiga layer:
```
app/Http/Controllers/{Domain}/{Name}Controller.php
app/Http/Requests/{Domain}/{Name}Request.php
app/Services/{Domain}/{Name}Service.php
```
Domain ditentukan oleh kebutuhan project (mis. `Manage`, `Master`, `Finances`, `Users`), bukan nama fitur individual. Class baru yang berhubungan masuk ke domain yang sudah ada; jangan bikin domain baru untuk satu fitur kecil.
- Ikuti konvensi penamaan Laravel community (PSR-12 + konvensi umum):
| Yang di-nama-i | Gaya | Contoh benar | Contoh salah |
|---|---|---|---|
| Controller | singular | `ArticleController` | `ArticlesController` |
| Route (URI) | plural | `articles/1` | `article/1` |
| Route name | snake_case + dot | `users.show_active` | `users.show-active` |
| Model | singular | `User` | `Users` |
| Relasi hasOne/belongsTo | singular | `articleComment` | `articleComments` |
| Relasi lain (hasMany, dll) | plural | `articleComments` | `articleComment` |
| Tabel | plural, snake_case | `article_comments` | `articleComments` |
| Tabel pivot | singular, alfabetis | `article_user` | `user_article` |
| Kolom tabel | snake_case, tanpa nama model | `meta_title` | `article_meta_title` |
| Foreign key | singular model + `_id` | `article_id` | `id_article` |
| Primary key | - | `id` | `custom_id` |
| Migration | deskriptif | `2017_01_01_000000_create_articles_table` | `2017_01_01_000000_articles` |
| Method | camelCase | `getAll()` | `get_all()` |
| Method resource controller | verba standar | `store()` | `saveArticle()` |
| Method test (Pest) | deskriptif, `it(...)`/`test(...)` | `it('rejects guest from viewing article')` | — |
| Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` |
| Collection | deskriptif, plural | `$activeUsers` | `$data` |
| Object tunggal | deskriptif, singular | `$activeUser` | `$obj` |
| File config/lang index | snake_case | `articles_enabled` | `ArticlesEnabled` |
| File view Blade | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` |
| Komponen React (Inertia page/props) | PascalCase file, camelCase prop | `EditForm.tsx`, `isOpen` | — |
| Contract (interface) | adjective/noun, tanpa prefix `I` | `AuthenticationInterface` | `IAuthentication` |
| Trait | adjective | `Notifiable` | `NotificationTrait` |
| Enum | singular | `UserType` | `UserTypeEnum` |
| FormRequest | singular + `Request` | `UpdateUserRequest` | `UserFormRequest` |
| Seeder | singular | `UserSeeder` | `UsersSeeder` |
---
## 2. Enum
- Semua data opsi yang sudah pasti/tetap valuenya **wajib** pakai native PHP Enum (`enum ... : string`), jangan pakai konstanta class atau string mentah.
- Setiap Enum yang punya representasi UI **wajib** menyediakan method `label(): string` berbahasa Indonesia:
```php
enum OrderStatus: string
{
case Pending = 'pending';
case Completed = 'completed';
public function label(): string
{
return match ($this) {
self::Pending => 'Menunggu',
self::Completed => 'Selesai',
};
}
}
```
- Kalau project punya banyak Enum, buat trait bantu (mis. `HasValues`) untuk method umum seperti `values()`/`options()` supaya tidak duplikat logika di tiap enum.
- Validasi enum di FormRequest pakai `Rule::enum(XxxEnum::class)`, bukan `Rule::in([...])` manual — termasuk kalau ditulis sebagai `Rule::in(XxxEnum::values())`, itu tetap salah karena tidak mendapat pesan error bawaan enum dan gampang lolos review karena polanya mirip valid:
```php
// Bad
'status' => ['required', 'string', Rule::in(OrderStatus::values())],
// Good
'status' => ['required', 'string', Rule::enum(OrderStatus::class)],
```
`Rule::in([...])` tetap sah dipakai untuk daftar nilai tetap yang **belum** dijadikan Enum (mis. sedang dipertimbangkan atau memang bukan kandidat Enum) — tapi begitu ada Enum untuk field itu, wajib pindah ke `Rule::enum()`.
- Kirim enum ke frontend sebagai `value` + `label`, jangan kirim instance PHP mentah ke Inertia props — gunakan `->value` dan `->label()` secara eksplisit atau resource/mapper kecil.
---
## 3. Model & Eloquent
- Gunakan `#[Guarded(['id'])]` (attribute), **bukan** `protected $guarded` atau `#[Fillable]`. Tambahkan kolom lain ke `Guarded` hanya jika memang tidak boleh diisi lewat mass assignment (mis. `last_login_at`).
- **Hapus model/class yang sudah tidak dipakai**, jangan dibiarkan menumpuk. Cek dengan grep nama class-nya (bukan cuma teks labelnya) sebelum menyimpulkan tidak terpakai — model kosong tanpa `#[Guarded]` dan tanpa referensi pemakaian adalah tanda kuat dead code.
- **Selalu buat relasi dua arah.** Kalau `Order belongsTo Customer`, maka `Customer` juga harus punya relasi baliknya (`hasOne`/`hasMany` sesuai kardinalitas). Jangan biarkan relasi hanya berjalan satu arah.
- **Jangan asumsikan foreign key default Eloquent tanpa mengecek migration.** Sebelum menulis `hasMany`/`hasOne`/`belongsTo` tanpa parameter FK eksplisit, pastikan kolom hasil konvensi (`{model}_id`) memang ada di tabel terkait. Kalau kolomnya berbeda (mis. relasi menyeberang lewat model perantara), gunakan FK eksplisit atau `hasManyThrough`, bukan dibiarkan salah diam-diam.
- **Urutkan method relasi dalam satu model per kelompok tipe, lalu alfabetis di dalam tiap kelompok**, dengan urutan kelompok: `belongsTo``hasOne``hasMany``belongsToMany``hasOneThrough`/`hasManyThrough` → relasi morph (`morphTo`/`morphMany`/`morphToMany`). Method non-relasi (`casts()`, accessor, method bisnis custom) tetap di posisi semula relatif terhadap blok relasi.
```php
// Good — dikelompokkan per tipe, lalu alfabetis
public function department(): BelongsTo { ... } // belongsTo
public function user(): BelongsTo { ... }
public function profile(): HasOne { ... } // hasOne
public function attendances(): HasMany { ... } // hasMany
public function submissions(): HasMany { ... }
public function departments(): BelongsToMany { ... } // belongsToMany
```
- Eloquent-first, hindari raw query/`DB::` kecuali untuk kasus performa spesifik yang benar-benar butuh (agregasi berat, bulk update) — dan beri komentar alasannya.
- **Cegah N+1**: selalu eager-load relasi yang dipakai di view/Inertia props dengan `with()`/`load()`. Saat memakai partial eager load (`with('term:id,start_date,end_date')`), pastikan kolom yang dipakai untuk cast/accessor ikut disertakan, atau serialisasi akan error.
- Mass assignment lewat relasi, bukan set atribut manual satu-satu:
```php
// Bad
$article = new Article;
$article->title = $request->title;
$article->category_id = $category->id;
$article->save();
// Good
$category->articles()->create($request->validated());
```
- Untuk data besar (export, batch update, notifikasi massal), gunakan `chunk()`/`chunkById()`/`cursor()`, jangan `get()` lalu `foreach` penuh di memori:
```php
// Bad
foreach (User::all() as $user) { ... }
// Good
User::chunkById(500, function ($users) {
foreach ($users as $user) { ... }
});
```
- Query builder singkat & ekspresif:
| Panjang | Singkat |
|---|---|
| `->where('column', '=', 1)` | `->where('column', 1)` |
| `->orderBy('created_at', 'desc')` | `->latest()` |
| `->orderBy('created_at', 'asc')` | `->oldest()` |
| `->select('id', 'name')->get()` | `->get(['id', 'name'])` |
| `->first()->name` | `->value('name')` |
Pola `->get([...])`/`->paginate($perPage, [...])` ini berlaku juga walau ada `with()`/`when()`/`join()`/dll di antara — Eloquent tidak peduli di posisi mana `select()` dipanggil relatif ke klausa lain, cuma peduli klausa itu ada sebelum eksekusi. **Kecuali** kalau query yang sama pakai `withCount()`/`withSum()`/`withAvg()`/`withMax()`/`withMin()`: method-method itu diam-diam menyuntik `select(table.*)` kalau belum ada `select()` eksplisit sebelumnya, dan Eloquent hanya menerapkan kolom dari `get($columns)`/`paginate($perPage, $columns)` kalau belum ada `select()` — begitu `withCount()` lebih dulu mengisi kolom, argumen kolom di method terminal **diabaikan diam-diam tanpa error** (balik jadi `select(*)`, bocor semua kolom). Jadi kalau ada `withCount()`/`withSum()`/dst di query, `select([...])` eksplisit **wajib** tetap dipertahankan sebelum pemanggilan `withCount()`/dst, jangan dipindah ke method terminal.
---
## 4. Controller, Request, dan Service
- **Controller** hanya mengatur alur request → response (validasi input dipanggil, service dipanggil, redirect/Inertia render dikembalikan). **Tidak boleh** ada query Eloquent kompleks atau business logic langsung di controller.
- **Selalu gunakan FormRequest**, sekecil apapun validasinya — jangan validasi inline di controller dengan `$request->validate()`.
- Business logic (kalkulasi, orkestrasi antar model, side effect seperti notifikasi/log) **disimpan di Service**, bukan di controller atau model.
- **Kalau method Service butuh user yang sedang login untuk scoping/filtering** (mis. dosen cuma lihat kelasnya sendiri, mahasiswa cuma lihat jurusannya sendiri), terima sebagai parameter eksplisit `User $user` (non-nullable, tanpa default) di **posisi pertama** — jangan panggil `auth()->user()` langsung di dalam Service. Controller yang menyuplainya lewat `$request->user()`. Ini soal testability (Service tidak bergantung diam-diam ke global state) dan konsistensi lintas Service.
```php
// Bad
public function paginated(int $perPage = 25): LengthAwarePaginator
{
$user = auth()->user();
// ...
}
// Good
public function paginated(User $user, int $perPage = 25): LengthAwarePaginator
{
// ...
}
```
- Otorisasi berbasis permission (Spatie) dicek di dua tempat:
- Route-level: middleware `permission:create-xxx` dipasang per-route/per-group.
- Object-level (mis. user hanya boleh mengubah record miliknya sendiri): di `authorize()` milik FormRequest, kombinasikan `$this->user()->can('permission-name')` dengan pengecekan kepemilikan record. Untuk route tanpa FormRequest (mis. `destroy()` yang tidak butuh validasi input), pengecekan yang sama dilakukan inline dengan `abort_if()`/`abort_unless()` di Controller.
- **Predikat kepemilikan itu sendiri ditaruh sebagai method di Model (`User`, atau model pemilik lain yang relevan), bukan didefinisikan ulang di tiap Controller/FormRequest yang butuh.** Satu aturan bisnis harus punya satu sumber kebenaran — supaya konsisten dan gampang di-test. Penamaan: `is<Peran>Of($target)` untuk peran yang punya nama (mis. `isAdvisorOf`), atau `canManage<Model>($target)` untuk gate umum "boleh mengubah/menghapus record ini".
```php
// Bad — predikat yang sama diulang di FormRequest DAN Controller
// (FormRequest)
public function authorize(): bool
{
$assignment = $this->route('assignment');
return $this->user()->can('update-assignments')
&& (! $this->user()->hasRole('dosen') || $assignment->courseClass->lecturer_id === $this->user()->lecturer?->id);
}
// (Controller::destroy(), butuh predikat yang sama karena tidak ada FormRequest)
private function abortUnlessLecturerOwnsAssignment(Assignment $assignment): void
{
$user = request()->user();
abort_if($user->hasRole('dosen') && $assignment->courseClass?->lecturer_id !== $user->lecturer?->id, 403);
}
// Good — predikat di Model, dipakai dari FormRequest maupun Controller
// (User model)
public function canManageAssignment(Assignment $assignment): bool
{
if ($this->hasRole(UserRole::Dosen->value)) {
return $assignment->courseClass->lecturer_id === $this->lecturer?->id;
}
return true;
}
// (FormRequest)
public function authorize(): bool
{
return $this->user()->can('update-assignments')
&& $this->user()->canManageAssignment($this->route('assignment'));
}
// (Controller::destroy())
abort_unless(request()->user()->canManageAssignment($assignment), 403);
```
- Constructor injection untuk dependency (Service, Model), jangan `new Xxx` langsung di dalam method:
```php
// Bad
$user = new User;
$user->create($request->validated());
// Good
public function __construct(protected UserService $userService) {}
$this->userService->create($request->validated());
```
- **Single Responsibility** — satu method cuma ngerjain satu hal:
```php
// Bad
public function update(Request $request): string
{
$validated = $request->validate([...]);
foreach ($request->events as $event) {
$date = $this->carbon->parse($event['date'])->toString();
$this->logger->log('Update event ' . $date);
}
$this->event->updateGeneralEvent($request->validated());
return back();
}
// Good
public function update(UpdateEventRequest $request): RedirectResponse
{
$this->logService->logEvents($request->events);
$this->eventService->updateGeneralEvent($request->validated());
return back();
}
```
- Pecah method yang melakukan banyak hal jadi beberapa method kecil dengan nama deskriptif:
```php
// Bad
public function getFullNameAttribute(): string
{
if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) {
return 'Mr. ' . $this->first_name . ' ' . $this->last_name;
}
return $this->first_name[0] . '. ' . $this->last_name;
}
// Good
public function getFullNameAttribute(): string
{
return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}
public function isVerifiedClient(): bool { ... }
public function getFullNameLong(): string { ... }
public function getFullNameShort(): string { ... }
```
- Return type selalu dideklarasikan secara eksplisit (`: Response`, `: RedirectResponse`, `: Collection`, dll).
- Untuk operasi yang menyentuh >1 tabel sekaligus (mis. buat record + update counter terkait), bungkus dengan `DB::transaction()` di dalam Service, supaya atomik.
---
## 5. Kode Ringkas & Idiomatis Laravel
Gunakan helper singkat yang sudah tersedia, jangan syntax panjang manual:
| Syntax panjang | Syntax ringkas |
|---|---|
| `Session::get('cart')` | `session('cart')` |
| `$request->session()->get('cart')` | `session('cart')` |
| `Session::put('cart', $data)` | `session(['cart' => $data])` |
| `$request->input('name')` | `$request->name` / `request('name')` |
| `return Redirect::back()` | `return back()` |
| `is_null($obj->relation) ? null : $obj->relation->id` | `$obj->relation?->id` |
| `return view('index')->with('title', $t)->with('client', $c)` | `return view('index', compact('title', 'client'))` |
| `$request->has('value') ? $request->value : 'default'` | `$request->get('value', 'default')` |
| `Carbon::now()`, `Carbon::today()` | `now()`, `today()` |
| `App::make('Class')` | `app('Class')` |
- **Jangan panggil `env()` di luar file config.** Simpan dulu ke `config/*.php`, lalu akses lewat `config('nama.key')`. Ini berlaku juga untuk kredensial/flag baru yang ditambahkan.
- **DocBlock boleh dipakai kalau memang dibutuhkan dan penting** — bukan default yang dipasang di semua method. Untuk method standar (CRUD biasa, atau apa pun yang sudah jelas maksudnya dari nama method + return/param type PHP), **jangan** tambah DocBlock — itu pemborosan. DocBlock baru layak ditulis untuk kasus yang memang penting, misalnya:
1. **Generic type** yang tidak bisa diekspresikan native PHP, mis. `@return Collection<int, Student>`, `@param array<int, string>`.
2. **Perilaku non-obvious** yang bisa bikin pembaca salah paham kalau tidak dijelaskan: aturan bisnis tersembunyi, constraint yang tidak kelihatan dari nama method, workaround keterbatasan interface/library.
```php
// Bad — DocBlock generik yang cuma mengulang nama method, tidak nambah informasi
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array { ... }
// Good — tanpa DocBlock sama sekali, karena nama + return type sudah cukup jelas
public function rules(): array { ... }
// Good — DocBlock dipertahankan karena memang menjelaskan hal yang tidak
// kelihatan dari signature method (fallback logic yang bisa mengejutkan)
/**
* Falls back to the one with the latest start date if none is explicitly active.
*/
public function getActive(): ?AcademicTerm { ... }
```
- Komentar kode hanya untuk menjelaskan **kenapa**, bukan **apa** — kalau nama variabel/method sudah jelas, jangan tambah komentar.
---
## 6. Frontend (Inertia + React + TypeScript)
- **Selalu jalankan `php artisan wayfinder:generate --with-form`** setelah menambah/mengubah route, controller, atau FormRequest — flag `--with-form` wajib, tanpa itu halaman auth/settings gagal `tsc`.
- Panggil route lewat Wayfinder helper yang di-generate (`import { store } from '@/routes/...'`), **jangan** hardcode string URL di komponen React.
- Props yang dikirim dari controller ke halaman Inertia harus sudah dalam bentuk final untuk UI (enum sudah di-`value`/`label`, tanggal sudah diformat/di-ISO-kan) — jangan lempar model Eloquent mentah tanpa transformasi eksplisit.
- Komponen React: file `PascalCase.tsx`, satu komponen per file untuk komponen yang di-export dan dipakai di tempat lain; komponen kecil khusus halaman boleh co-located di file yang sama.
- Tipe untuk data dari backend didefinisikan eksplisit (interface/type), jangan `any`.
---
## 7. Testing
- Test ditulis dengan **Pest**, bukan gaya PHPUnit class-based, kecuali menyentuh kode lama yang belum dimigrasikan.
- Nama test deskriptif dan menyatakan perilaku, bukan nama method: `it('rejects guest from updating another user's profile')`.
- Untuk endpoint yang dilindungi permission, test **minimal** mencakup: pengguna dengan permission (berhasil), pengguna tanpa permission (403), dan — kalau ada object-level authorization — kasus user lain yang mencoba mengakses record bukan miliknya (ditolak).
- Gunakan factory (`Model::factory()`), jangan insert manual ke DB di test.
- Jalankan `php artisan test` (atau `vendor/bin/pest`) sebelum menganggap task selesai jika ada perubahan pada Service/Controller/Request.
---
## 8. Kualitas Kode & Tooling
- Jalankan **Laravel Pint** (`vendor/bin/pint --parallel`) sebelum commit — style harus konsisten, jangan format manual.
- Jalankan **Larastan** (`vendor/bin/phpstan analyse`) untuk perubahan yang menyentuh tipe/return value; perbaiki temuannya, jangan suppress kecuali benar-benar false positive dan beri alasan.
- Jangan gunakan `@phpstan-ignore` / `@ts-ignore` sebagai jalan pintas — perbaiki akar masalah tipe-nya.
---
## 9. Keamanan
- Validasi selalu di FormRequest (lihat §4), termasuk validasi kepemilikan record lewat `Rule::exists()` yang di-scope, bukan hanya cek ID ada di tabel mana pun.
- Jangan expose data sensitif (password hash, token) lewat Inertia props atau API resource — pastikan `Guarded`/`hidden` pada model sudah benar dan props controller hanya kirim field yang dibutuhkan.
- Upload file lewat satu service upload terpusat per project, jangan tulis logic upload baru per fitur — validasi mime/size selalu di FormRequest terkait.
- Permission baru yang ditambahkan harus terdaftar di seeder permission (Spatie) dan dipasang di route lewat `permission:` middleware.

View File

@ -0,0 +1,32 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Fortify;
class AuthenticateUser
{
/**
* Resolve the user attempting to log in, rejecting inactive accounts.
*/
public function __invoke(Request $request): ?User
{
$user = User::where(Fortify::username(), $request->input(Fortify::username()))->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
return null;
}
if (! $user->is_active) {
throw ValidationException::withMessages([
Fortify::username() => 'Akun Anda tidak aktif. Silakan hubungi administrator.',
]);
}
return $user;
}
}

View File

@ -12,11 +12,6 @@ class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules, ProfileValidationRules;
/**
* Validate and create a newly registered user.
*
* @param array<string, string> $input
*/
public function create(array $input): User
{
Validator::make($input, [
@ -25,7 +20,7 @@ public function create(array $input): User
])->validate();
return User::create([
'name' => $input['name'],
'username' => $input['username'],
'email' => $input['email'],
'password' => $input['password'],
]);

View File

@ -2,40 +2,40 @@
namespace App\Concerns;
use App\Enums\Gender;
use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Validation\Rule;
trait ProfileValidationRules
{
/**
* Get the validation rules used to validate user profiles.
*
* @return array<string, array<int, ValidationRule|array<mixed>|string>>
*/
protected function profileRules(?int $userId = null): array
{
return [
'name' => $this->nameRules(),
'username' => $this->usernameRules($userId),
'email' => $this->emailRules($userId),
'full_name' => ['required', 'string', 'max:150'],
'phone_number' => ['required', 'string', 'max:20'],
'address' => ['required', 'string'],
'gender' => ['required', 'string', Rule::in(Gender::values())],
'birth_date' => ['required', 'date'],
'birth_place' => ['required', 'string', 'max:100'],
];
}
/**
* Get the validation rules used to validate user names.
*
* @return array<int, ValidationRule|array<mixed>|string>
*/
protected function nameRules(): array
protected function usernameRules(?int $userId = null): array
{
return ['required', 'string', 'max:255'];
return [
'required',
'string',
'max:20',
'min:3',
'alpha_dash',
$userId === null
? Rule::unique(User::class)
: Rule::unique(User::class)->ignore($userId),
];
}
/**
* Get the validation rules used to validate user emails.
*
* @return array<int, ValidationRule|array<mixed>|string>
*/
protected function emailRules(?int $userId = null): array
{
return [

View File

@ -0,0 +1,21 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum AssignmentStatus: string
{
use HasValues;
case Open = 'open';
case Closed = 'closed';
public function label(): string
{
return match ($this) {
self::Open => 'Dibuka',
self::Closed => 'Ditutup',
};
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum AttendanceStatus: string
{
use HasValues;
case Present = 'present';
case Absent = 'absent';
public function label(): string
{
return match ($this) {
self::Present => 'Hadir',
self::Absent => 'Tidak Hadir',
};
}
}

23
app/Enums/ClassMethod.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum ClassMethod: string
{
use HasValues;
case Online = 'online';
case Offline = 'offline';
case Hybrid = 'hybrid';
public function label(): string
{
return match ($this) {
self::Online => 'Daring',
self::Offline => 'Luring',
self::Hybrid => 'Hybrid',
};
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Enums\Concerns;
trait HasValues
{
/**
* @return array<int, string>
*/
public static function values(): array
{
return array_column(self::cases(), 'value');
}
/**
* @return array<int, array{value: string, label: string}>
*/
public static function options(): array
{
return array_map(fn (self $case) => [
'value' => $case->value,
'label' => $case->label(),
], self::cases());
}
}

31
app/Enums/DayOfWeek.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum DayOfWeek: string
{
use HasValues;
case Monday = 'Monday';
case Tuesday = 'Tuesday';
case Wednesday = 'Wednesday';
case Thursday = 'Thursday';
case Friday = 'Friday';
case Saturday = 'Saturday';
case Sunday = 'Sunday';
public function label(): string
{
return match ($this) {
self::Monday => 'Senin',
self::Tuesday => 'Selasa',
self::Wednesday => 'Rabu',
self::Thursday => 'Kamis',
self::Friday => 'Jumat',
self::Saturday => 'Sabtu',
self::Sunday => 'Minggu',
};
}
}

27
app/Enums/DegreeLevel.php Normal file
View File

@ -0,0 +1,27 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum DegreeLevel: string
{
use HasValues;
case D3 = 'D3';
case D4 = 'D4';
case S1 = 'S1';
case S2 = 'S2';
case S3 = 'S3';
public function label(): string
{
return match ($this) {
self::D3 => 'D3 (Diploma Tiga)',
self::D4 => 'D4 (Diploma Empat)',
self::S1 => 'S1 (Sarjana)',
self::S2 => 'S2 (Magister)',
self::S3 => 'S3 (Doktor)',
};
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum FeedbackStatus: string
{
use HasValues;
case Submitted = 'submitted';
case InReview = 'in_review';
case Resolved = 'resolved';
public function label(): string
{
return match ($this) {
self::Submitted => 'Terkirim',
self::InReview => 'Ditinjau',
self::Resolved => 'Selesai',
};
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum FeedbackType: string
{
use HasValues;
case Kritik = 'kritik';
case Saran = 'saran';
case Aduan = 'aduan';
public function label(): string
{
return match ($this) {
self::Kritik => 'Kritik',
self::Saran => 'Saran',
self::Aduan => 'Aduan',
};
}
}

21
app/Enums/Gender.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum Gender: string
{
use HasValues;
case Male = 'male';
case Female = 'female';
public function label(): string
{
return match ($this) {
self::Male => 'Laki-laki',
self::Female => 'Perempuan',
};
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum LetterStatus: string
{
use HasValues;
case Submitted = 'submitted';
case InProcess = 'in_process';
case Completed = 'completed';
case Rejected = 'rejected';
public function label(): string
{
return match ($this) {
self::Submitted => 'Diajukan',
self::InProcess => 'Diproses',
self::Completed => 'Selesai',
self::Rejected => 'Ditolak',
};
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum PaymentMethod: string
{
use HasValues;
case Transfer = 'transfer';
case Cash = 'cash';
public function label(): string
{
return match ($this) {
self::Transfer => 'Transfer',
self::Cash => 'Tunai',
};
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum PaymentStatus: string
{
use HasValues;
case Unpaid = 'unpaid';
case Partial = 'partial';
case Paid = 'paid';
public function label(): string
{
return match ($this) {
self::Unpaid => 'Belum Bayar',
self::Partial => 'Sebagian',
self::Paid => 'Lunas',
};
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum RegistrationStatus: string
{
use HasValues;
case Submitted = 'submitted';
case Approved = 'approved';
case Rejected = 'rejected';
public function label(): string
{
return match ($this) {
self::Submitted => 'Diajukan',
self::Approved => 'Disetujui',
self::Rejected => 'Ditolak',
};
}
}

21
app/Enums/Semester.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum Semester: string
{
use HasValues;
case odd = 'odd';
case even = 'even';
public function label(): string
{
return match ($this) {
self::odd => 'Ganjil',
self::even => 'Genap',
};
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum StudentStatus: string
{
use HasValues;
case Active = 'active';
case OnLeave = 'on_leave';
case Graduated = 'graduated';
case DroppedOut = 'dropped_out';
public function label(): string
{
return match ($this) {
self::Active => 'Aktif',
self::OnLeave => 'Cuti',
self::Graduated => 'Lulus',
self::DroppedOut => 'Drop Out',
};
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum SubmissionStatus: string
{
use HasValues;
case NotSubmitted = 'not_submitted';
case Submitted = 'submitted';
public function label(): string
{
return match ($this) {
self::NotSubmitted => 'Belum Mengumpulkan',
self::Submitted => 'Sudah Mengumpulkan',
};
}
}

29
app/Enums/UserRole.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\HasValues;
enum UserRole: string
{
use HasValues;
case Mahasiswa = 'mahasiswa';
case Dosen = 'dosen';
case Kaprodi = 'kaprodi';
case StaffAdmin = 'staff-admin';
case StaffKeuangan = 'staff-keuangan';
case Developer = 'developer';
public function label(): string
{
return match ($this) {
self::Mahasiswa => 'Mahasiswa',
self::Dosen => 'Dosen',
self::Kaprodi => 'Ketua Program Studi',
self::StaffAdmin => 'Staff Admin',
self::StaffKeuangan => 'Staff Keuangan',
self::Developer => 'Developer',
};
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Exports\Concerns;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
trait StyledHeadingRow
{
abstract public function title(): string;
public function styles(Worksheet $sheet): array
{
return [
1 => [
'font' => ['bold' => true, 'color' => ['rgb' => '733E0A']],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['rgb' => 'FDC700'],
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_LEFT,
'vertical' => Alignment::VERTICAL_CENTER,
],
],
];
}
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$lastColumn = $sheet->getHighestColumn();
$sheet->insertNewRowBefore(1, 1);
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->setCellValue('A1', $this->title().' '.config('app.name'));
$sheet->getRowDimension(1)->setRowHeight(24);
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
$sheet->getStyle('A1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
$sheet->getStyle('A1')->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
},
];
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Exports;
use App\Exports\Concerns\StyledHeadingRow;
use App\Models\User;
use App\Services\Admin\Users\LecturerService;
use Illuminate\Support\Enumerable;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithStyles;
class LecturersExport implements FromCollection, ShouldAutoSize, WithEvents, WithHeadings, WithMapping, WithStyles
{
use StyledHeadingRow;
public function __construct(
private readonly string $search = '',
private readonly ?string $gender = null,
private readonly ?int $departmentId = null,
) {}
public function title(): string
{
return 'Data Dosen';
}
public function collection(): Enumerable
{
return app(LecturerService::class)->forExport($this->search, $this->gender, $this->departmentId);
}
public function headings(): array
{
return [
'NIDN',
'Nama Lengkap',
'Username',
'Email',
'Nomor Telepon',
'Jurusan',
'Jenis Kelamin',
'Tempat Lahir',
'Tanggal Lahir',
'Alamat',
];
}
public function map($user): array
{
/** @var User $user */
return [
$user->lecturer?->lecturer_number ?? '-',
$user->profile?->full_name ?? '-',
$user->username,
$user->email,
$user->profile?->phone_number ?? '-',
$user->lecturer?->departments->pluck('name')->join(', ') ?: '-',
$user->profile?->gender?->label() ?? '-',
$user->profile?->birth_place ?? '-',
$user->profile?->birth_date?->format('d M Y') ?? '-',
$user->profile?->address ?? '-',
];
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace App\Exports;
use App\Exports\Concerns\StyledHeadingRow;
use App\Models\User;
use App\Services\Admin\Users\StudentService;
use Illuminate\Support\Enumerable;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithStyles;
class StudentsExport implements FromCollection, ShouldAutoSize, WithEvents, WithHeadings, WithMapping, WithStyles
{
use StyledHeadingRow;
public function __construct(
private readonly User $user,
private readonly string $search = '',
private readonly ?string $gender = null,
private readonly ?int $departmentId = null,
private readonly ?string $status = null,
private readonly ?int $enrollmentYear = null,
) {}
public function title(): string
{
return 'Data Mahasiswa';
}
public function collection(): Enumerable
{
return app(StudentService::class)->forExport($this->user, $this->search, $this->gender, $this->departmentId, $this->status, $this->enrollmentYear);
}
public function headings(): array
{
return [
'NIM',
'Nama Lengkap',
'Username',
'Email',
'Nomor Telepon',
'Jurusan',
'Angkatan',
'Status',
'Jenis Kelamin',
'Tempat Lahir',
'Tanggal Lahir',
'Alamat',
];
}
public function map($user): array
{
/** @var User $user */
return [
$user->student?->student_number ?? '-',
$user->profile?->full_name ?? '-',
$user->username,
$user->email,
$user->profile?->phone_number ?? '-',
$user->student?->department?->name ?? '-',
$user->student?->enrollment_year ?? '-',
$user->student?->status?->label() ?? '-',
$user->profile?->gender?->label() ?? '-',
$user->profile?->birth_place ?? '-',
$user->profile?->birth_date?->format('d M Y') ?? '-',
$user->profile?->address ?? '-',
];
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers\Admin\AcademicClasses\Assignment;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AcademicClasses\Assignment\AssignmentRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\Assignment;
use App\Services\Admin\AcademicClasses\Assignment\AssignmentService;
use App\Services\Admin\Manage\CourseClass\CourseClassService;
use App\Services\Admin\Master\AcademicTermService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class AssignmentController extends Controller
{
public function __construct(
private readonly AssignmentService $service,
private readonly CourseClassService $courseClassService,
private readonly AcademicTermService $academicTermService,
) {}
public function index(PaginatedRequest $request): Response
{
$academicTermSelected = $request->has('academic_term_id');
$academicTermId = $academicTermSelected
? ((int) $request->validated('academic_term_id') ?: null)
: $this->academicTermService->getActive()?->id;
return Inertia::render('admin/academic-classes/assignments/index', [
'assignments' => Inertia::scroll(fn () => $this->service->paginated(
$request->user(),
...$request->validatedWithDefaults(),
courseClassId: $request->validated('course_class_id'),
academicTermId: $academicTermId,
)),
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
'filterCourseClasses' => Inertia::always($this->courseClassService->getAllForSelectByAcademicTerm($request->user(), $academicTermId)),
'academicTerms' => $this->academicTermService->getAllForSelect(),
'filters' => Inertia::always([
'course_class_id' => $request->validated('course_class_id'),
'academic_term_id' => $academicTermSelected
? (string) ($academicTermId ?? 0)
: ($academicTermId ? (string) $academicTermId : null),
]),
]);
}
public function store(AssignmentRequest $request): RedirectResponse
{
$this->service->create($request->validated(), $request->file('attachment'));
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil ditambahkan.']);
return to_route('admin.academic-classes.assignments.index');
}
public function update(AssignmentRequest $request, Assignment $assignment): RedirectResponse
{
$this->service->update($assignment, $request->validated(), $request->file('attachment'));
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil diperbarui.']);
return to_route('admin.academic-classes.assignments.index');
}
public function destroy(Assignment $assignment): RedirectResponse
{
abort_unless(request()->user()->canManageAssignment($assignment), 403);
$this->service->delete($assignment);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil dihapus.'])->back();
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\Admin\AcademicClasses\Assignment;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AcademicClasses\Assignment\SubmitAssignmentRequest;
use App\Http\Requests\Admin\AcademicClasses\Assignment\GradeSubmissionRequest;
use App\Models\Assignment;
use App\Models\Submission;
use App\Services\Admin\AcademicClasses\Assignment\SubmissionService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class SubmissionController extends Controller
{
public function __construct(
private readonly SubmissionService $service,
) {}
public function index(Assignment $assignment): Response
{
abort_unless(request()->user()->canManageAssignment($assignment), 403);
$assignment->load(['courseClass.course', 'courseClass.lecturer.user.profile', 'courseClass.academicTerm']);
return Inertia::render('admin/academic-classes/assignments/submissions', [
'assignment' => $assignment,
'submissions' => $this->service->forAssignment($assignment),
]);
}
public function grade(GradeSubmissionRequest $request, Assignment $assignment, Submission $submission): RedirectResponse
{
$this->service->grade($submission, $request->validated('score'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Nilai berhasil disimpan.'])->back();
}
public function submit(SubmitAssignmentRequest $request, Assignment $assignment): RedirectResponse
{
$this->service->submitForStudent($assignment, $request->user()->student, $request->validated(), $request->file('file'));
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil dikumpulkan.']);
return to_route('admin.academic-classes.assignments.index');
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers\Admin\AcademicClasses;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AcademicClasses\AttendanceStoreRequest;
use App\Models\CourseClass;
use App\Services\Admin\AcademicClasses\AttendanceService;
use App\Services\Admin\Master\AcademicTermService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class AttendanceController extends Controller
{
public function __construct(
private readonly AttendanceService $service,
private readonly AcademicTermService $academicTermService,
) {}
public function index(Request $request): Response
{
$academicTermId = $request->has('academic_term_id')
? $request->integer('academic_term_id')
: $this->academicTermService->getActive()?->id;
return Inertia::render('admin/academic-classes/attendances/index', [
'courseClasses' => $this->service->courseClassesForUser($request->user(), $academicTermId),
'academicTerms' => $this->academicTermService->getAllForSelect(),
'filters' => [
'academic_term_id' => $academicTermId ? (string) $academicTermId : null,
],
]);
}
public function show(CourseClass $courseClass): Response
{
$this->abortUnlessLecturerOwnsClass($courseClass);
$courseClass->load(['course', 'academicTerm']);
return Inertia::render('admin/academic-classes/attendances/course-class', [
'courseClass' => $courseClass,
'sessions' => $this->service->sessionsForClass($courseClass),
'nextMeetingNumber' => $this->service->nextMeetingNumber($courseClass),
]);
}
public function session(CourseClass $courseClass, int $meetingNumber): Response
{
$this->abortUnlessLecturerOwnsClass($courseClass);
$courseClass->load('course');
$data = $this->service->session($courseClass, $meetingNumber);
return Inertia::render('admin/academic-classes/attendances/session', [
'courseClass' => $courseClass,
'meetingNumber' => $meetingNumber,
'roster' => $data['roster'],
'date' => $data['date'],
]);
}
public function store(AttendanceStoreRequest $request, CourseClass $courseClass, int $meetingNumber): RedirectResponse
{
$this->service->saveSession($courseClass->id, $meetingNumber, $request->validated('date'), $request->validated('records'));
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kehadiran berhasil disimpan.']);
return to_route('admin.academic-classes.attendances.session', [$courseClass->id, $meetingNumber]);
}
public function destroy(CourseClass $courseClass, int $meetingNumber): RedirectResponse
{
$this->abortUnlessLecturerOwnsClass($courseClass);
$this->service->deleteSession($courseClass->id, $meetingNumber);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Sesi kehadiran berhasil dihapus.']);
return to_route('admin.academic-classes.attendances.show', [$courseClass->id]);
}
/**
* A dosen may only take or manage attendance for classes they lecture.
*/
private function abortUnlessLecturerOwnsClass(CourseClass $courseClass): void
{
$user = request()->user();
abort_if(
$user->hasRole('dosen') && $courseClass->lecturer_id !== $user->lecturer?->id,
403,
);
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers\Admin\AcademicClasses;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AcademicClasses\MaterialRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\Material;
use App\Services\Admin\AcademicClasses\MaterialService;
use App\Services\Admin\Manage\CourseClass\CourseClassService;
use App\Services\Admin\Master\AcademicTermService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class MaterialController extends Controller
{
public function __construct(
private readonly MaterialService $service,
private readonly CourseClassService $courseClassService,
private readonly AcademicTermService $academicTermService,
) {}
public function index(PaginatedRequest $request): Response
{
$academicTermSelected = $request->has('academic_term_id');
$academicTermId = $academicTermSelected
? ((int) $request->validated('academic_term_id') ?: null)
: $this->academicTermService->getActive()?->id;
return Inertia::render('admin/academic-classes/materials/index', [
'materials' => Inertia::scroll(fn () => $this->service->paginated(
$request->user(),
...$request->validatedWithDefaults(),
courseClassId: $request->validated('course_class_id'),
academicTermId: $academicTermId,
)),
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
'filterCourseClasses' => Inertia::always($this->courseClassService->getAllForSelectByAcademicTerm($request->user(), $academicTermId)),
'academicTerms' => $this->academicTermService->getAllForSelect(),
'filters' => Inertia::always([
'course_class_id' => $request->validated('course_class_id'),
'academic_term_id' => $academicTermSelected
? (string) ($academicTermId ?? 0)
: ($academicTermId ? (string) $academicTermId : null),
]),
]);
}
public function store(MaterialRequest $request): RedirectResponse
{
$this->service->create($request->validated(), $request->file('file'));
Inertia::flash('toast', ['type' => 'success', 'message' => 'Materi berhasil ditambahkan.']);
return to_route('admin.academic-classes.materials.index');
}
public function update(MaterialRequest $request, Material $material): RedirectResponse
{
$this->service->update($material, $request->validated(), $request->file('file'));
Inertia::flash('toast', ['type' => 'success', 'message' => 'Materi berhasil diperbarui.']);
return to_route('admin.academic-classes.materials.index');
}
public function destroy(Material $material): RedirectResponse
{
abort_unless(request()->user()->canManageMaterial($material), 403);
$this->service->delete($material);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Materi berhasil dihapus.'])->back();
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers\Admin\AcademicClasses;
use App\Enums\UserRole;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AcademicClasses\ScheduleRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\Schedule;
use App\Services\Admin\AcademicClasses\ScheduleService;
use App\Services\Admin\Master\AcademicTermService;
use App\Services\Admin\Master\CourseService;
use App\Services\Admin\Master\DepartmentService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class ScheduleController extends Controller
{
public function __construct(
private readonly ScheduleService $service,
private readonly AcademicTermService $academicTermService,
private readonly DepartmentService $departmentService,
private readonly CourseService $courseService,
) {}
public function index(PaginatedRequest $request): Response
{
$user = $request->user();
$isPersonalView = $user->hasRole(UserRole::Mahasiswa->value) || $user->hasRole(UserRole::Dosen->value);
$academicTermId = $request->has('academic_term_id')
? $request->validated('academic_term_id')
: $this->academicTermService->getActive()?->id;
return Inertia::render('admin/academic-classes/schedules/index', [
'schedules' => $this->service->all(
$user,
academicTermId: $academicTermId,
departmentId: $request->validated('department_id'),
semesterNumber: $request->validated('semester_number'),
),
'courseClasses' => $this->service->courseClassOptions(),
'academicTerms' => $this->academicTermService->getAllForSelect(),
'departments' => $this->departmentService->getAllForSelect(),
'semesterNumbers' => $this->courseService->getSemesterNumbers(),
'isPersonalView' => $isPersonalView,
'filters' => [
'academic_term_id' => $academicTermId ? (string) $academicTermId : null,
'department_id' => $request->validated('department_id'),
'semester_number' => $request->validated('semester_number'),
],
]);
}
public function store(ScheduleRequest $request): RedirectResponse
{
$this->service->create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jadwal berhasil ditambahkan.']);
return to_route('admin.academic-classes.schedules.index');
}
public function update(ScheduleRequest $request, Schedule $schedule): RedirectResponse
{
$this->service->update($schedule, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jadwal berhasil diperbarui.']);
return to_route('admin.academic-classes.schedules.index');
}
public function destroy(Schedule $schedule): RedirectResponse
{
$this->service->delete($schedule);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Jadwal berhasil dihapus.'])->back();
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Admin\Developer;
use App\Http\Controllers\Controller;
use App\Http\Requests\PaginatedRequest;
use App\Services\Admin\Developer\LogViewerService;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class LogController extends Controller
{
public function __construct(
private readonly LogViewerService $service,
) {}
public function index(PaginatedRequest $request): Response
{
$files = $this->service->availableFiles();
$file = $request->validated('file') ?: $files->first()['name'] ?? null;
return Inertia::render('admin/developer/logs/index', [
'entries' => $file
? Inertia::scroll(fn () => $this->service->paginated(
$file,
...$request->validatedWithDefaults(),
level: $request->validated('level'),
))
: null,
'files' => $files,
'selectedFile' => $file,
'filters' => $request->only(['file', 'level']),
]);
}
public function download(PaginatedRequest $request): BinaryFileResponse
{
$file = $request->validated('file');
abort_unless($file, 404);
$files = $this->service->availableFiles();
abort_unless($files->pluck('name')->contains($file), 404);
return response()->download(storage_path('logs/'.$file));
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\Admin\Developer;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Developer\UpdateRolePermissionsRequest;
use App\Services\Admin\Developer\RolePermissionService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\Permission\Models\Role;
class RolePermissionController extends Controller
{
public function __construct(
private readonly RolePermissionService $service,
) {}
public function index(): Response
{
return Inertia::render('admin/developer/roles/index', [
'roles' => $this->service->roles(),
'permissionGroups' => $this->service->groupedPermissions(),
]);
}
public function update(UpdateRolePermissionsRequest $request, Role $role): RedirectResponse
{
$this->service->updatePermissions($role, $request->validated('permissions'));
Inertia::flash('toast', ['type' => 'success', 'message' => 'Permission role berhasil diperbarui.']);
return to_route('admin.developer.roles.index');
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Enums\FeedbackStatus;
use App\Enums\FeedbackType;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Feedback\FeedbackRequest;
use App\Http\Requests\Admin\Feedback\ReplyFeedbackRequest;
use App\Http\Requests\Admin\Feedback\UpdateFeedbackStatusRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\Feedback;
use App\Services\Admin\FeedbackService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class FeedbackController extends Controller
{
public function __construct(
private readonly FeedbackService $service,
) {}
public function index(PaginatedRequest $request): Response
{
return Inertia::render('admin/feedback/index', [
'feedbacks' => Inertia::scroll(fn () => $this->service->paginated(
$request->user(),
...$request->validatedWithDefaults(),
type: $request->validated('type'),
status: $request->validated('status'),
)),
'types' => FeedbackType::options(),
'statuses' => FeedbackStatus::options(),
'filters' => $request->only(['type', 'status']),
]);
}
public function create(): Response
{
return Inertia::render('admin/feedback/create', [
'types' => FeedbackType::options(),
]);
}
public function store(FeedbackRequest $request): RedirectResponse
{
$this->service->create($request->user(), $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Masukan berhasil dikirim.']);
return to_route('admin.feedback.index');
}
public function edit(Request $request, Feedback $feedback): Response
{
abort_unless($feedback->user_id === $request->user()->id, 403);
abort_unless($feedback->status === FeedbackStatus::Submitted, 403);
return Inertia::render('admin/feedback/edit', [
'feedback' => $feedback,
'types' => FeedbackType::options(),
]);
}
public function update(FeedbackRequest $request, Feedback $feedback): RedirectResponse
{
$this->service->update($feedback, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Masukan berhasil diperbarui.']);
return to_route('admin.feedback.index');
}
public function destroy(Request $request, Feedback $feedback): RedirectResponse
{
abort_unless($feedback->user_id === $request->user()->id, 403);
abort_unless($feedback->status === FeedbackStatus::Submitted, 403);
$this->service->delete($feedback);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Masukan berhasil dihapus.'])->back();
}
public function updateStatus(UpdateFeedbackStatusRequest $request, Feedback $feedback): RedirectResponse
{
$this->service->updateStatus($feedback, $request->validated('status'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status masukan berhasil diperbarui.'])->back();
}
public function reply(ReplyFeedbackRequest $request, Feedback $feedback): RedirectResponse
{
$this->service->reply($feedback, $request->validated('reply'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Balasan berhasil dikirim.'])->back();
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers\Admin\Finances;
use App\Enums\StudentStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Finances\TuitionInvoiceRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\TuitionInvoice;
use App\Services\Admin\Finances\TuitionInvoiceService;
use App\Services\Admin\Master\AcademicTermService;
use App\Services\Admin\Users\StudentService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class TuitionInvoiceController extends Controller
{
public function __construct(
private readonly TuitionInvoiceService $service,
private readonly StudentService $studentService,
private readonly AcademicTermService $academicTermService,
) {}
public function index(PaginatedRequest $request): Response
{
$students = $this->studentService->getAllForSelect(status: StudentStatus::Active->value);
$invoicedTermIdsByStudent = $this->service->getInvoicedTermIdsByStudent($students->pluck('id')->all());
$students->each(function ($student) use ($invoicedTermIdsByStudent) {
$student->invoiced_term_ids = $invoicedTermIdsByStudent->get($student->id, []);
});
$academicTerms = $this->academicTermService->getAllForSelect();
$requestedTermId = $request->validated('academic_term_id');
$summaryTermId = $requestedTermId ?? $this->academicTermService->getActive()?->id;
return Inertia::render('admin/finances/tuition-invoices/index', [
'invoices' => Inertia::scroll(fn () => $this->service->paginated(
...$request->validatedWithDefaults(),
academicTermId: $requestedTermId,
status: $request->validated('status'),
paymentMethod: $request->validated('payment_method'),
)),
'summary' => $this->service->summary(
search: $request->validated('search') ?? '',
academicTermId: $summaryTermId,
),
'summaryAcademicTerm' => $academicTerms->firstWhere('id', $summaryTermId),
'students' => $students,
'academicTerms' => $academicTerms,
'filters' => $request->only(['academic_term_id', 'status', 'payment_method']),
]);
}
public function store(TuitionInvoiceRequest $request): RedirectResponse
{
$this->service->createMany($request->validated('student_ids'), $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tagihan berhasil ditambahkan.']);
return to_route('admin.finances.tuition-invoices.index');
}
public function update(TuitionInvoiceRequest $request, TuitionInvoice $tuitionInvoice): RedirectResponse
{
$this->service->update($tuitionInvoice, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tagihan berhasil diperbarui.']);
return to_route('admin.finances.tuition-invoices.index');
}
public function destroy(TuitionInvoice $tuitionInvoice): RedirectResponse
{
$this->service->delete($tuitionInvoice);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Tagihan berhasil dihapus.'])->back();
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Admin\Finances;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Finances\TuitionPaymentRequest;
use App\Models\TuitionInvoice;
use App\Models\TuitionPayment;
use App\Services\Admin\Finances\TuitionPaymentService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class TuitionPaymentController extends Controller
{
public function __construct(
private readonly TuitionPaymentService $service,
) {}
public function index(TuitionInvoice $tuitionInvoice): Response
{
$tuitionInvoice->load(['student.user.profile', 'student.department', 'academicTerm']);
return Inertia::render('admin/finances/tuition-invoices/payments', [
'invoice' => $tuitionInvoice,
'payments' => $this->service->forInvoice($tuitionInvoice),
]);
}
public function store(TuitionPaymentRequest $request, TuitionInvoice $tuitionInvoice): RedirectResponse
{
$this->service->create($tuitionInvoice, $request->validated(), $request->file('proof'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pembayaran berhasil ditambahkan.'])->back();
}
public function update(TuitionPaymentRequest $request, TuitionInvoice $tuitionInvoice, TuitionPayment $payment): RedirectResponse
{
$this->service->update($payment, $request->validated(), $request->file('proof'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pembayaran berhasil diperbarui.'])->back();
}
public function destroy(TuitionInvoice $tuitionInvoice, TuitionPayment $payment): RedirectResponse
{
$this->service->delete($payment);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pembayaran berhasil dihapus.'])->back();
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Admin\Manage;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\AnnouncementRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\Announcement;
use App\Services\Admin\Manage\AnnouncementService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class AnnouncementController extends Controller
{
public function __construct(
private readonly AnnouncementService $service,
) {}
public function index(PaginatedRequest $request): Response
{
return Inertia::render('admin/manage/announcements/index', [
'announcements' => Inertia::scroll(fn () => $this->service->paginated(
$request->user(),
...$request->validatedWithDefaults(),
)),
]);
}
public function store(AnnouncementRequest $request): RedirectResponse
{
$this->service->create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumuman berhasil ditambahkan.']);
return to_route('admin.manage.announcements.index');
}
public function update(AnnouncementRequest $request, Announcement $announcement): RedirectResponse
{
$this->service->update($announcement, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumuman berhasil diperbarui.']);
return to_route('admin.manage.announcements.index');
}
public function destroy(Announcement $announcement): RedirectResponse
{
$this->service->delete($announcement);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumuman berhasil dihapus.'])->back();
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\Admin\Manage\CourseClass;
use App\Http\Controllers\Controller;
use App\Models\ClassEnrollment;
use App\Models\CourseClass;
use App\Services\Admin\Manage\CourseClass\ClassEnrollmentService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class ClassEnrollmentController extends Controller
{
public function __construct(
private readonly ClassEnrollmentService $service,
) {}
public function index(CourseClass $courseClass): Response
{
$courseClass->load(['course', 'lecturer.user.profile', 'academicTerm']);
return Inertia::render('admin/manage/course-classes/enrollments', [
'courseClass' => $courseClass,
'enrollments' => $this->service->forClass($courseClass),
]);
}
public function destroy(CourseClass $courseClass, ClassEnrollment $enrollment): RedirectResponse
{
$this->service->unenroll($enrollment);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Mahasiswa berhasil dikeluarkan dari kelas.'])->back();
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\Admin\Manage\CourseClass;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\CourseClass\CourseClassRequest;
use App\Http\Requests\Admin\Manage\CourseClass\DuplicateCourseClassesRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\CourseClass;
use App\Services\Admin\Manage\CourseClass\CourseClassService;
use App\Services\Admin\Master\AcademicTermService;
use App\Services\Admin\Master\CourseService;
use App\Services\Admin\Users\LecturerService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class CourseClassController extends Controller
{
public function __construct(
private readonly CourseClassService $service,
private readonly CourseService $courseService,
private readonly LecturerService $lecturerService,
private readonly AcademicTermService $academicTermService,
) {}
public function index(PaginatedRequest $request): Response
{
return Inertia::render('admin/manage/course-classes/index', [
'courseClasses' => Inertia::scroll(fn () => $this->service->paginated(
...$request->validatedWithDefaults(),
academicTermId: $request->validated('academic_term_id'),
method: $request->validated('method'),
)),
'courses' => $this->courseService->getAllForSelect(),
'lecturers' => $this->lecturerService->getAllForSelect(),
'academicTerms' => $this->academicTermService->getAllForSelect(),
'courseClassAssignments' => $this->service->getAllForSelect($request->user()),
'filters' => $request->only(['academic_term_id', 'method']),
]);
}
public function store(CourseClassRequest $request): RedirectResponse
{
$created = $this->service->createMany($request->validated());
Inertia::flash('toast', [
'type' => 'success',
'message' => $created->count() > 1
? "{$created->count()} kelas mata kuliah berhasil ditambahkan."
: 'Kelas mata kuliah berhasil ditambahkan.',
]);
return to_route('admin.manage.course-classes.index');
}
public function update(CourseClassRequest $request, CourseClass $courseClass): RedirectResponse
{
$this->service->update($courseClass, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kelas mata kuliah berhasil diperbarui.']);
return to_route('admin.manage.course-classes.index');
}
public function destroy(CourseClass $courseClass): RedirectResponse
{
$this->service->delete($courseClass);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kelas mata kuliah berhasil dihapus.'])->back();
}
public function duplicate(DuplicateCourseClassesRequest $request): RedirectResponse
{
$result = $this->service->duplicateFromTerm(
$request->validated('source_academic_term_id'),
$request->validated('target_academic_term_id'),
);
$message = $result['created'] > 0
? "{$result['created']} kelas mata kuliah berhasil diduplikasi."
: 'Tidak ada kelas mata kuliah baru yang diduplikasi.';
if ($result['skipped'] > 0) {
$message .= " {$result['skipped']} dilewati karena sudah ada di periode tujuan.";
}
Inertia::flash('toast', ['type' => 'success', 'message' => $message]);
return to_route('admin.manage.course-classes.index');
}
}

View File

@ -0,0 +1,181 @@
<?php
namespace App\Http\Controllers\Admin\Manage;
use App\Enums\UserRole;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\CourseRegistration\RejectCourseRegistrationRequest;
use App\Http\Requests\Admin\Manage\CourseRegistration\SignAsAdvisorCourseRegistrationRequest;
use App\Http\Requests\Admin\Manage\CourseRegistration\SignAsDepartmentLeaderCourseRegistrationRequest;
use App\Http\Requests\Admin\Manage\CourseRegistration\UpdateCourseRegistrationRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\Course;
use App\Models\CourseRegistrationSubmission;
use App\Models\Student;
use App\Services\Admin\Manage\CourseRegistrationService;
use App\Services\Admin\Master\AcademicTermService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class CourseRegistrationController extends Controller
{
public function __construct(
private readonly CourseRegistrationService $service,
private readonly AcademicTermService $academicTermService,
) {}
public function index(PaginatedRequest $request): Response|RedirectResponse
{
$user = $request->user();
if ($user->hasRole(UserRole::Mahasiswa->value)) {
return to_route('admin.manage.course-registrations.show', $user->student->id);
}
return Inertia::render('admin/manage/course-registrations/index', [
'departments' => $this->service->departmentSummary(),
'registrations' => Inertia::scroll(fn () => $this->service->paginated(
$user,
...$request->validatedWithDefaults(),
departmentId: $request->validated('department_id'),
)),
'filters' => $request->only(['department_id']),
]);
}
public function show(Student $student): Response
{
$user = request()->user();
$isStudent = $user->hasRole(UserRole::Mahasiswa->value);
abort_if($isStudent && $user->student->id !== $student->id, 403);
abort_if(! $isStudent && ! $user->canAccessCourseRegistrationOf($student), 403);
$backHref = $isStudent
? null
: route('admin.manage.course-registrations.index');
$canReview = ! $isStudent
&& $user->can('approve-course-registrations')
&& $user->canReviewCourseRegistrationOf($student);
$canSignAsDepartmentLeader = $user->isDepartmentLeaderOf($student);
$canSignAsAdvisor = $user->isAdvisorOf($student);
$activeTerm = $this->academicTermService->getActive();
$allCourseClasses = $activeTerm
? $this->service->availableCourseClasses($student->department_id, $activeTerm->id)
: collect();
$departmentCourses = $this->service->departmentCourses($student->department_id);
$semesters = $departmentCourses
->map(fn (Course $course) => $course->semester_number)
->unique()
->sort()
->values();
$submissions = $activeTerm
? $semesters->mapWithKeys(fn (?int $semesterNumber) => [
CourseRegistrationSubmission::semesterKey($semesterNumber) => $this->service->buildSubmissionPayload(
$student,
$activeTerm,
$semesterNumber,
$departmentCourses,
$allCourseClasses,
),
])
: collect();
return Inertia::render('admin/manage/course-registrations/show', [
'submissions' => $submissions,
'semesters' => $semesters->values(),
'openSemesters' => $activeTerm?->open_semesters ?? [],
'studentCurrentSemester' => $student->current_semester,
'hasActiveTerm' => $activeTerm !== null,
'backHref' => $backHref,
'canReview' => $canReview,
'canSignAsDepartmentLeader' => $canSignAsDepartmentLeader,
'canSignAsAdvisor' => $canSignAsAdvisor,
]);
}
public function update(Student $student, string $semester, UpdateCourseRegistrationRequest $request): RedirectResponse
{
$semesterNumber = CourseRegistrationSubmission::parseSemesterKey($semester);
$activeTerm = $this->academicTermService->getActive();
abort_if($activeTerm === null, 422);
$selectableIds = $this->service->selectableCourseClassIds(
$student->department_id,
$activeTerm->id,
$semesterNumber,
$activeTerm->open_semesters ?? [],
$student->current_semester,
);
$selectedIds = array_values(array_intersect(
$request->validated('course_class_ids') ?? [],
$selectableIds,
));
$submission = $this->service->findOrCreateForStudent($student, $activeTerm, $semesterNumber);
$this->service->saveRegistrations(
$submission,
$selectedIds,
$activeTerm->id,
$student->id,
$request->file('signature'),
);
Inertia::flash('toast', ['type' => 'success', 'message' => 'KRS berhasil disimpan.']);
return to_route('admin.manage.course-registrations.show', $student->id);
}
public function signAsDepartmentLeader(Student $student, string $semester, SignAsDepartmentLeaderCourseRegistrationRequest $request): RedirectResponse
{
$submission = $this->service->findOrCreateForSemester($student, $semester);
$this->service->signAsDepartmentLeader($submission, $request->file('signature'));
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tanda tangan berhasil disimpan.']);
return to_route('admin.manage.course-registrations.show', $student->id);
}
public function signAsAdvisor(Student $student, string $semester, SignAsAdvisorCourseRegistrationRequest $request): RedirectResponse
{
$submission = $this->service->findOrCreateForSemester($student, $semester);
$this->service->signAsAdvisor($submission, $request->file('signature'));
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tanda tangan berhasil disimpan.']);
return to_route('admin.manage.course-registrations.show', $student->id);
}
public function approve(CourseRegistrationSubmission $submission): RedirectResponse
{
abort_unless(
request()->user()->can('approve-course-registrations')
&& request()->user()->canReviewCourseRegistrationOf($submission->student),
403
);
$this->service->approve($submission);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'KRS berhasil disetujui.'])->back();
}
public function reject(RejectCourseRegistrationRequest $request, CourseRegistrationSubmission $submission): RedirectResponse
{
$this->service->reject($submission, $request->validated('reason'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'KRS berhasil ditolak.'])->back();
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Admin\Master;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Master\AcademicTermRequest;
use App\Http\Requests\Admin\UpdateStatusRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\AcademicTerm;
use App\Services\Admin\Master\AcademicTermService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class AcademicTermController extends Controller
{
public function __construct(
private readonly AcademicTermService $service,
) {}
public function index(PaginatedRequest $request): Response
{
return Inertia::render('admin/master/academic-terms/index', [
'academicTerms' => Inertia::scroll(fn () => $this->service->paginated(
...$request->validatedWithDefaults(),
semester: $request->validated('semester'),
isActive: $request->filled('is_active') ? $request->boolean('is_active') : null,
)),
'filters' => $request->only(['semester', 'is_active']),
]);
}
public function store(AcademicTermRequest $request): RedirectResponse
{
$this->service->create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Periode akademik berhasil ditambahkan.']);
return to_route('admin.master.academic-terms.index');
}
public function update(AcademicTermRequest $request, AcademicTerm $academicTerm): RedirectResponse
{
$this->service->update($academicTerm, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Periode akademik berhasil diperbarui.']);
return to_route('admin.master.academic-terms.index');
}
public function destroy(AcademicTerm $academicTerm): RedirectResponse
{
$this->service->delete($academicTerm);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Periode akademik berhasil dihapus.'])->back();
}
public function updateStatus(UpdateStatusRequest $request, AcademicTerm $academicTerm): RedirectResponse
{
$this->service->updateActiveStatus($academicTerm, $request->boolean('is_active'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status periode akademik berhasil diperbarui.'])->back();
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers\Admin\Master;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Master\CourseRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\Course;
use App\Services\Admin\Master\CourseService;
use App\Services\Admin\Master\DepartmentService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class CourseController extends Controller
{
public function __construct(
private readonly CourseService $service,
private readonly DepartmentService $departmentService,
) {}
public function index(PaginatedRequest $request): Response
{
return Inertia::render('admin/master/courses/index', [
'courses' => Inertia::scroll(fn () => $this->service->paginated(
$request->user(),
...$request->validatedWithDefaults(),
departmentId: $request->validated('department_id'),
semesterNumber: $request->validated('semester_number'),
)),
'departments' => $this->departmentService->getAllForSelect(),
'semesterNumbers' => $this->service->getSemesterNumbers(),
'filters' => $request->only(['department_id', 'semester_number']),
]);
}
public function store(CourseRequest $request): RedirectResponse
{
$this->service->create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Mata kuliah berhasil ditambahkan.']);
return to_route('admin.master.courses.index');
}
public function update(CourseRequest $request, Course $course): RedirectResponse
{
$this->service->update($course, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Mata kuliah berhasil diperbarui.']);
return to_route('admin.master.courses.index');
}
public function destroy(Course $course): RedirectResponse
{
$this->service->delete($course);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Mata kuliah berhasil dihapus.'])->back();
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Admin\Master;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Master\DepartmentRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\Department;
use App\Services\Admin\Master\DepartmentService;
use App\Services\Admin\Users\LecturerService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class DepartmentController extends Controller
{
public function __construct(
private readonly DepartmentService $service,
private readonly LecturerService $lecturerService,
) {}
public function index(PaginatedRequest $request): Response
{
return Inertia::render('admin/master/departments/index', [
'departments' => Inertia::scroll(fn () => $this->service->paginated(...$request->validatedWithDefaults())),
'lecturers' => $this->lecturerService->getAllForSelect(),
]);
}
public function store(DepartmentRequest $request): RedirectResponse
{
$this->service->create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jurusan berhasil ditambahkan.']);
return to_route('admin.master.departments.index');
}
public function update(DepartmentRequest $request, Department $department): RedirectResponse
{
$this->service->update($department, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jurusan berhasil diperbarui.']);
return to_route('admin.master.departments.index');
}
public function destroy(Department $department): RedirectResponse
{
$this->service->delete($department);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Jurusan berhasil dihapus.'])->back();
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Admin\Services;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Services\AcademicAdvisingLogRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\AcademicAdvisingLog;
use App\Services\Admin\Services\AcademicAdvisingLogService;
use App\Services\Admin\Users\LecturerService;
use App\Services\Admin\Users\StudentService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class AcademicAdvisingLogController extends Controller
{
public function __construct(
private readonly AcademicAdvisingLogService $service,
private readonly StudentService $studentService,
private readonly LecturerService $lecturerService,
) {}
public function index(PaginatedRequest $request): Response
{
return Inertia::render('admin/services/academic-advising-logs/index', [
'logs' => Inertia::scroll(fn () => $this->service->paginated(
$request->user(),
...$request->validatedWithDefaults(),
lecturerId: $request->validated('lecturer_id'),
)),
'students' => $this->studentService->getAllForSelect(),
'lecturers' => $this->lecturerService->getAllForSelect(),
'filters' => $request->only(['lecturer_id']),
]);
}
public function store(AcademicAdvisingLogRequest $request): RedirectResponse
{
$this->service->create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil ditambahkan.']);
return to_route('admin.services.academic-advising-logs.index');
}
public function update(AcademicAdvisingLogRequest $request, AcademicAdvisingLog $academicAdvisingLog): RedirectResponse
{
$this->service->update($academicAdvisingLog, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil diperbarui.']);
return to_route('admin.services.academic-advising-logs.index');
}
public function destroy(AcademicAdvisingLog $academicAdvisingLog): RedirectResponse
{
$this->service->delete($academicAdvisingLog);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil dihapus.'])->back();
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Admin\Services;
use App\Enums\LetterStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Services\LetterRequest\LetterRequestRequest;
use App\Http\Requests\Admin\Services\LetterRequest\LetterRequestStatusRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\LetterRequest;
use App\Services\Admin\Services\LetterRequestService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class LetterRequestController extends Controller
{
public function __construct(
private readonly LetterRequestService $service,
) {}
public function index(PaginatedRequest $request): Response
{
return Inertia::render('admin/services/letter-requests/index', [
'letterRequests' => Inertia::scroll(fn () => $this->service->paginated(
$request->user(),
...$request->validatedWithDefaults(),
status: $request->validated('status'),
)),
'filters' => $request->only(['status']),
]);
}
public function store(LetterRequestRequest $request): RedirectResponse
{
$this->service->create([
'user_id' => auth()->id(),
...$request->validated(),
]);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil diajukan.']);
return to_route('admin.services.letter-requests.index');
}
public function update(LetterRequestRequest $request, LetterRequest $letterRequest): RedirectResponse
{
$this->service->update($letterRequest, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil diperbarui.']);
return to_route('admin.services.letter-requests.index');
}
public function destroy(LetterRequest $letterRequest): RedirectResponse
{
abort_if($letterRequest->status !== LetterStatus::Submitted, 403);
$this->service->delete($letterRequest);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil dihapus.'])->back();
}
public function updateStatus(LetterRequestStatusRequest $request, LetterRequest $letterRequest): RedirectResponse
{
$this->service->updateStatus($letterRequest, $request->validated('status'), $request->file('result'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status surat permohonan berhasil diperbarui.'])->back();
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace App\Http\Controllers\Admin\Settings;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Settings\ProfileDeleteRequest;
use App\Http\Requests\Admin\Settings\ProfileUpdateRequest;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Inertia\Response;
class ProfileController extends Controller
{
public function edit(Request $request): Response
{
$user = $request->user();
[$role, $roleData] = match (true) {
$user->hasRole('mahasiswa') => $this->studentRoleData($user),
$user->hasRole('dosen') => $this->lecturerRoleData($user),
default => $this->administratorRoleData($user),
};
return Inertia::render('admin/settings/profile', [
'mustVerifyEmail' => $user instanceof MustVerifyEmail,
'status' => $request->session()->get('status'),
'profile' => $user->profile,
'role' => $role,
'roleData' => $roleData,
]);
}
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$user = $request->user();
$user->fill($request->safe()->only(['username', 'email']));
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
$user->save();
$user->profile()->updateOrCreate([], $request->safe()->only([
'full_name', 'phone_number', 'address', 'gender', 'birth_date', 'birth_place',
]));
Inertia::flash('toast', ['type' => 'success', 'message' => 'Profil berhasil diperbarui.']);
return to_route('admin.settings.profile.edit');
}
public function destroy(ProfileDeleteRequest $request): RedirectResponse
{
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
private function studentRoleData(User $user): array
{
$user->load(['profile', 'student.department', 'student.academicAdvisor.user.profile']);
return ['mahasiswa', $user->student ? [
'student_number' => $user->student->student_number,
'department' => $user->student->department?->name,
'enrollment_year' => $user->student->enrollment_year,
'academic_advisor' => $user->student->academicAdvisor?->user?->profile?->full_name,
] : null];
}
private function lecturerRoleData(User $user): array
{
$user->load(['profile', 'lecturer.departments']);
return ['dosen', $user->lecturer ? [
'lecturer_number' => $user->lecturer->lecturer_number,
'department' => $user->lecturer->departments->pluck('name')->join(', ') ?: null,
] : null];
}
private function administratorRoleData(User $user): array
{
$user->load(['profile', 'roles']);
return ['admin', [
'roles' => $user->roles->pluck('name')->all(),
]];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Admin\Settings;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Settings\PasswordUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Validation\Rules\Password;
use Inertia\Inertia;
use Inertia\Response;
class SecurityController extends Controller
{
public function edit(): Response
{
return Inertia::render('admin/settings/security', [
'passwordRules' => Password::defaults()->toPasswordRulesString(),
]);
}
public function update(PasswordUpdateRequest $request): RedirectResponse
{
$request->user()->update([
'password' => $request->password,
]);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil diperbarui.'])->back();
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers\Admin\Users;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UpdateStatusRequest;
use App\Http\Requests\Admin\Users\AdministratorRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\User;
use App\Services\Admin\Users\AdministratorService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class AdministratorController extends Controller
{
public function __construct(
private readonly AdministratorService $service,
) {}
public function index(PaginatedRequest $request): Response
{
return Inertia::render('admin/users/administrators/index', [
'administrators' => Inertia::scroll(fn () => $this->service->paginated(
...$request->validatedWithDefaults(),
gender: $request->validated('gender'),
)),
'filters' => $request->only(['gender']),
]);
}
public function create(): Response
{
return Inertia::render('admin/users/administrators/create', [
'roles' => $this->service->availableRoles(),
]);
}
public function store(AdministratorRequest $request): RedirectResponse
{
$this->service->create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Administrator berhasil ditambahkan.']);
return to_route('admin.users.administrators.index');
}
public function edit(User $user): Response
{
$user->load(['profile', 'roles']);
return Inertia::render('admin/users/administrators/edit', [
'user' => $user,
'roles' => $this->service->availableRoles(),
]);
}
public function update(AdministratorRequest $request, User $user): RedirectResponse
{
$this->service->update($user, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Administrator berhasil diperbarui.']);
return to_route('admin.users.administrators.index');
}
public function destroy(User $user): RedirectResponse
{
$this->service->delete($user);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Administrator berhasil dihapus.'])->back();
}
public function resetPassword(User $user): RedirectResponse
{
$this->service->resetPassword($user);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil direset.'])->back();
}
public function updateUserStatus(UpdateStatusRequest $request, User $user): RedirectResponse
{
$this->service->updateUserStatus($user, $request->boolean('is_active'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status akun berhasil diperbarui.'])->back();
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers\Admin\Users;
use App\Exports\LecturersExport;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UpdateStatusRequest;
use App\Http\Requests\Admin\Users\LecturerRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\User;
use App\Services\Admin\Master\DepartmentService;
use App\Services\Admin\Users\LecturerService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
use Maatwebsite\Excel\Facades\Excel;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class LecturerController extends Controller
{
public function __construct(
private readonly LecturerService $service,
private readonly DepartmentService $departmentService,
) {}
public function index(PaginatedRequest $request): Response
{
return Inertia::render('admin/users/lecturers/index', [
'lecturers' => Inertia::scroll(fn () => $this->service->paginated(
...$request->validatedWithDefaults(),
gender: $request->validated('gender'),
departmentId: $request->validated('department_id'),
)),
'departments' => $this->departmentService->getAllForSelect(),
'filters' => $request->only(['gender', 'department_id']),
]);
}
public function create(): Response
{
return Inertia::render('admin/users/lecturers/create', [
'departments' => $this->departmentService->getAllForSelect(),
]);
}
public function store(LecturerRequest $request): RedirectResponse
{
$this->service->create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Dosen berhasil ditambahkan.']);
return to_route('admin.users.lecturers.index');
}
public function edit(User $user): Response
{
$user->load(['profile', 'lecturer.departments']);
return Inertia::render('admin/users/lecturers/edit', [
'user' => $user,
'departments' => $this->departmentService->getAllForSelect(),
]);
}
public function update(LecturerRequest $request, User $user): RedirectResponse
{
$this->service->update($user, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Dosen berhasil diperbarui.']);
return to_route('admin.users.lecturers.index');
}
public function destroy(User $user): RedirectResponse
{
$this->service->delete($user);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Dosen berhasil dihapus.'])->back();
}
public function resetPassword(User $user): RedirectResponse
{
$this->service->resetPassword($user);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil direset.'])->back();
}
public function updateUserStatus(UpdateStatusRequest $request, User $user): RedirectResponse
{
$this->service->updateUserStatus($user, $request->boolean('is_active'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status akun berhasil diperbarui.'])->back();
}
public function export(PaginatedRequest $request): BinaryFileResponse
{
return Excel::download(new LecturersExport(
search: $request->validated('search') ?? '',
gender: $request->validated('gender'),
departmentId: $request->validated('department_id'),
), 'dosen.xlsx');
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace App\Http\Controllers\Admin\Users;
use App\Enums\StudentStatus;
use App\Exports\StudentsExport;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UpdateStatusRequest;
use App\Http\Requests\Admin\Users\Student\StudentRequest;
use App\Http\Requests\Admin\Users\Student\StudentStatusRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\User;
use App\Services\Admin\Master\DepartmentService;
use App\Services\Admin\Users\LecturerService;
use App\Services\Admin\Users\StudentService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
use Maatwebsite\Excel\Facades\Excel;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class StudentController extends Controller
{
public function __construct(
private readonly StudentService $service,
private readonly DepartmentService $departmentService,
private readonly LecturerService $lecturerService,
) {}
public function index(PaginatedRequest $request): Response
{
return Inertia::render('admin/users/students/index', [
'students' => Inertia::scroll(fn () => $this->service->paginated(
$request->user(),
...$request->validatedWithDefaults(),
gender: $request->validated('gender'),
departmentId: $request->validated('department_id'),
status: $request->validated('status'),
enrollmentYear: $request->validated('enrollment_year'),
)),
'departments' => $this->departmentService->getAllForSelect(),
'statuses' => StudentStatus::options(),
'enrollmentYears' => $this->service->getEnrollmentYears(),
'filters' => $request->only(['gender', 'department_id', 'status', 'enrollment_year']),
]);
}
public function create(): Response
{
return Inertia::render('admin/users/students/create', [
'departments' => $this->departmentService->getAllForSelect(),
'lecturers' => $this->lecturerService->getAllForSelect(),
]);
}
public function store(StudentRequest $request): RedirectResponse
{
$this->service->create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Mahasiswa berhasil ditambahkan.']);
return to_route('admin.users.students.index');
}
public function edit(User $user): Response
{
$user->load(['profile', 'student.department', 'student.academicAdvisor.user.profile']);
return Inertia::render('admin/users/students/edit', [
'user' => $user,
'departments' => $this->departmentService->getAllForSelect(),
'lecturers' => $this->lecturerService->getAllForSelect(),
]);
}
public function update(StudentRequest $request, User $user): RedirectResponse
{
$this->service->update($user, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Mahasiswa berhasil diperbarui.']);
return to_route('admin.users.students.index');
}
public function destroy(User $user): RedirectResponse
{
$this->service->delete($user);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Mahasiswa berhasil dihapus.'])->back();
}
public function resetPassword(User $user): RedirectResponse
{
$this->service->resetPassword($user);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil direset.'])->back();
}
public function updateStatus(StudentStatusRequest $request, User $user): RedirectResponse
{
$this->service->updateStatus($user, $request->validated('status'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status mahasiswa berhasil diperbarui.'])->back();
}
public function updateUserStatus(UpdateStatusRequest $request, User $user): RedirectResponse
{
$this->service->updateUserStatus($user, $request->boolean('is_active'));
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status akun berhasil diperbarui.'])->back();
}
public function export(PaginatedRequest $request): BinaryFileResponse
{
return Excel::download(new StudentsExport(
user: $request->user(),
search: $request->validated('search') ?? '',
gender: $request->validated('gender'),
departmentId: $request->validated('department_id'),
status: $request->validated('status'),
enrollmentYear: $request->validated('enrollment_year'),
), 'mahasiswa.xlsx');
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;
use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
class GoogleAuthController extends Controller
{
public function redirect(): SymfonyRedirectResponse
{
return Socialite::driver('google')->redirect();
}
public function callback(Request $request): RedirectResponse
{
$googleUser = Socialite::driver('google')->user();
$user = User::where('email', $googleUser->getEmail())->first();
if (! $user) {
Inertia::flash('toast', [
'type' => 'error',
'message' => 'Akun dengan email Google tersebut tidak ditemukan. Silakan hubungi administrator.',
]);
return Redirect::route('login');
}
if (! $user->is_active) {
Inertia::flash('toast', [
'type' => 'error',
'message' => 'Akun Anda tidak aktif. Silakan hubungi administrator.',
]);
return Redirect::route('login');
}
if ($user->google_id !== $googleUser->getId()) {
$user->update(['google_id' => $googleUser->getId()]);
}
Auth::login($user, remember: true);
$request->session()->regenerate();
return Redirect::intended(route('dashboard', absolute: false));
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\MediaUploadRequest;
use App\Services\MediaUploadService;
use Illuminate\Http\JsonResponse;
class MediaUploadController extends Controller
{
public function __construct(
private readonly MediaUploadService $service,
) {}
public function store(MediaUploadRequest $request): JsonResponse
{
$media = $this->service->store(
$request->user(),
$request->file('file'),
$request->validated('collection') ?? 'content',
);
return response()->json([
'id' => $media->id,
'name' => $media->name,
'file_name' => $media->file_name,
'mime_type' => $media->mime_type,
'size' => $media->size,
'url' => $media->getUrl(),
]);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers;
use App\Models\Notification;
use App\Services\NotificationService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class NotificationController extends Controller
{
public function __construct(
private readonly NotificationService $service,
) {}
public function markAllRead(Request $request): RedirectResponse
{
$this->service->markAllAsRead($request->user());
return back();
}
public function destroyAll(Request $request): RedirectResponse
{
$this->service->deleteAll($request->user());
return back();
}
public function markRead(Request $request, Notification $notification): RedirectResponse
{
abort_unless($notification->user_id === $request->user()->id, 403);
$this->service->markAsRead($notification);
return back();
}
public function destroy(Request $request, Notification $notification): RedirectResponse
{
abort_unless($notification->user_id === $request->user()->id, 403);
$this->service->delete($notification);
return back();
}
}

View File

@ -1,62 +0,0 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Http\Requests\Settings\ProfileDeleteRequest;
use App\Http\Requests\Settings\ProfileUpdateRequest;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Inertia\Response;
class ProfileController extends Controller
{
/**
* Show the user's profile settings page.
*/
public function edit(Request $request): Response
{
return Inertia::render('settings/profile', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => $request->session()->get('status'),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
Inertia::flash('toast', ['type' => 'success', 'message' => __('Profile updated.')]);
return to_route('profile.edit');
}
/**
* Delete the user's profile.
*/
public function destroy(ProfileDeleteRequest $request): RedirectResponse
{
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@ -1,66 +0,0 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Http\Requests\Settings\PasswordUpdateRequest;
use App\Http\Requests\Settings\TwoFactorAuthenticationRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Validation\Rules\Password;
use Inertia\Inertia;
use Inertia\Response;
use Laravel\Fortify\Features;
class SecurityController extends Controller
{
/**
* Show the user's security settings page.
*/
public function edit(TwoFactorAuthenticationRequest $request): Response
{
$props = [
'canManageTwoFactor' => Features::canManageTwoFactorAuthentication(),
'canManagePasskeys' => Features::canManagePasskeys(),
'passkeys' => Features::canManagePasskeys()
? $request->user()
->passkeys()
->select(['id', 'name', 'credential', 'created_at', 'last_used_at'])
->latest()
->get()
->map(fn ($passkey) => [
'id' => $passkey->id,
'name' => $passkey->name,
'authenticator' => $passkey->authenticator,
'created_at_diff' => $passkey->created_at->diffForHumans(),
'last_used_at_diff' => $passkey->last_used_at?->diffForHumans(),
])
->values()
->all()
: [],
'passwordRules' => Password::defaults()->toPasswordRulesString(),
];
if (Features::canManageTwoFactorAuthentication()) {
$request->ensureStateIsValid();
$props['twoFactorEnabled'] = $request->user()->hasEnabledTwoFactorAuthentication();
$props['requiresConfirmation'] = Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm');
}
return Inertia::render('settings/security', $props);
}
/**
* Update the user's password.
*/
public function update(PasswordUpdateRequest $request): RedirectResponse
{
$request->user()->update([
'password' => $request->password,
]);
Inertia::flash('toast', ['type' => 'success', 'message' => __('Password updated.')]);
return back();
}
}

View File

@ -2,7 +2,10 @@
namespace App\Http\Middleware;
use App\Services\Admin\FeedbackService;
use App\Services\NotificationService;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
@ -39,9 +42,19 @@ public function share(Request $request): array
...parent::share($request),
'name' => config('app.name'),
'auth' => [
'user' => $request->user(),
'user' => $request->user()?->load('roles:id,name'),
'permissions' => $request->user()?->getAllPermissions()->pluck('name')->values() ?? [],
],
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
'unreadNotificationsCount' => fn () => $request->user()
? app(NotificationService::class)->unreadCount($request->user())
: 0,
'pendingFeedbackCount' => fn () => $request->user()
? app(FeedbackService::class)->pendingCount($request->user())
: 0,
'notifications' => Inertia::merge(fn () => $request->user()
? app(NotificationService::class)->paginated($request->user())
: null)->append('data', 'id'),
];
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Requests\Admin\AcademicClasses\Assignment;
use App\Enums\AssignmentStatus;
use App\Enums\UserRole;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class AssignmentRequest extends FormRequest
{
public function authorize(): bool
{
if ($this->isMethod('post')) {
return $this->user()->can('create-assignments');
}
return $this->user()->can('update-assignments')
&& $this->user()->canManageAssignment($this->route('assignment'));
}
public function rules(): array
{
$user = $this->user();
$courseClassRule = Rule::exists('course_classes', 'id');
if ($user->hasRole(UserRole::Dosen->value)) {
$courseClassRule->where('lecturer_id', $user->lecturer?->id);
}
return [
'course_class_id' => ['required', 'integer', $courseClassRule],
'title' => ['required', 'string', 'max:150'],
'description' => ['nullable', 'string'],
'deadline' => ['required', 'date'],
'status' => ['nullable', Rule::enum(AssignmentStatus::class)],
'attachment' => ['nullable', 'file', 'max:10240', 'mimes:pdf,doc,docx,ppt,pptx,xls,xlsx,jpg,jpeg,png,mp4,zip'],
];
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Admin\AcademicClasses\Assignment;
use Illuminate\Foundation\Http\FormRequest;
class GradeSubmissionRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('update-assignment-submissions')
&& $this->user()->canManageAssignment($this->route('assignment'));
}
public function rules(): array
{
return [
'score' => ['nullable', 'numeric', 'min:0', 'max:100'],
];
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Requests\Admin\AcademicClasses\Assignment;
use App\Enums\AssignmentStatus;
use App\Enums\RegistrationStatus;
use App\Models\Submission;
use Illuminate\Foundation\Http\FormRequest;
class SubmitAssignmentRequest extends FormRequest
{
public function authorize(): bool
{
if (! $this->user()->can('submit-assignments')) {
return false;
}
$student = $this->user()->student;
$assignment = $this->route('assignment');
if (! $student || ! $assignment) {
return false;
}
// Whether the assignment still accepts submissions is governed by its
// status, not the deadline — a lecturer closes it explicitly.
if ($assignment->status !== AssignmentStatus::Open) {
return false;
}
return $assignment->courseClass()
->whereHas('registrations', function ($q) use ($student) {
$q->where('student_id', $student->id)
->whereHas('submission', fn ($q) => $q->where('status', RegistrationStatus::Approved));
})
->exists();
}
public function rules(): array
{
$assignment = $this->route('assignment');
$student = $this->user()->student;
$hasExistingFile = Submission::query()
->where('assignment_id', $assignment->id)
->where('student_id', $student?->id)
->whereHas('media')
->exists();
return [
'notes' => ['nullable', 'string'],
'file' => [
$hasExistingFile ? 'nullable' : 'required',
'file',
'max:10240',
'mimes:pdf,doc,docx,ppt,pptx,xls,xlsx,jpg,jpeg,png,mp4,zip',
],
];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests\Admin\AcademicClasses;
use App\Enums\AttendanceStatus;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class AttendanceStoreRequest extends FormRequest
{
public function authorize(): bool
{
if (! $this->user()->can('create-attendances')) {
return false;
}
$user = $this->user();
$courseClass = $this->route('course_class');
if ($user->hasRole('dosen') && $courseClass?->lecturer_id !== $user->lecturer?->id) {
return false;
}
return true;
}
public function rules(): array
{
return [
'date' => ['required', 'date'],
'records' => ['required', 'array', 'min:1'],
'records.*.student_id' => ['required', 'integer', Rule::exists('students', 'id')],
'records.*.status' => ['required', Rule::enum(AttendanceStatus::class)],
];
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests\Admin\AcademicClasses;
use App\Enums\UserRole;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class MaterialRequest extends FormRequest
{
public function authorize(): bool
{
if ($this->isMethod('post')) {
return $this->user()->can('create-materials');
}
return $this->user()->can('update-materials')
&& $this->user()->canManageMaterial($this->route('material'));
}
public function rules(): array
{
$user = $this->user();
$courseClassRule = Rule::exists('course_classes', 'id');
if ($user->hasRole(UserRole::Dosen->value)) {
$courseClassRule->where('lecturer_id', $user->lecturer?->id);
}
return [
'course_class_id' => ['required', 'integer', $courseClassRule],
'title' => ['required', 'string', 'max:150'],
'description' => ['nullable', 'string'],
'meeting_number' => ['nullable', 'integer', 'min:1'],
'file' => [
'nullable',
'file',
'max:10240',
'mimes:pdf,doc,docx,ppt,pptx,xls,xlsx,jpg,jpeg,png,mp4,zip',
],
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\Admin\AcademicClasses;
use App\Enums\DayOfWeek;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ScheduleRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-schedules' : 'update-schedules');
}
public function rules(): array
{
return [
'course_class_id' => ['required', 'integer', Rule::exists('course_classes', 'id')],
'day_of_week' => ['required', 'string', Rule::enum(DayOfWeek::class)],
'start_time' => ['required', 'date_format:H:i'],
'end_time' => ['required', 'date_format:H:i', 'after:start_time'],
'room' => ['nullable', 'string', 'max:20'],
'online_link' => ['nullable', 'string', 'max:255', 'url'],
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Admin\Developer;
use App\Support\PermissionCatalog;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateRolePermissionsRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('update-roles');
}
public function rules(): array
{
return [
'permissions' => ['present', 'array'],
'permissions.*' => ['string', Rule::in(PermissionCatalog::all())],
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Admin\Feedback;
use App\Enums\FeedbackStatus;
use App\Enums\FeedbackType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class FeedbackRequest extends FormRequest
{
public function authorize(): bool
{
if ($this->isMethod('post')) {
return $this->user()->can('create-feedback');
}
$feedback = $this->route('feedback');
return $this->user()->can('update-feedback')
&& $feedback->user_id === $this->user()->id
&& $feedback->status === FeedbackStatus::Submitted;
}
public function rules(): array
{
return [
'type' => ['required', 'string', Rule::enum(FeedbackType::class)],
'subject' => ['required', 'string', 'max:150'],
'message' => ['required', 'string'],
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\Admin\Feedback;
use Illuminate\Foundation\Http\FormRequest;
class ReplyFeedbackRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('reply-feedback');
}
public function rules(): array
{
return [
'reply' => ['required', 'string'],
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\Admin\Feedback;
use App\Enums\FeedbackStatus;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateFeedbackStatusRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('update-feedback-status');
}
public function rules(): array
{
return [
'status' => ['required', Rule::enum(FeedbackStatus::class)],
];
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Requests\Admin\Finances;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class TuitionInvoiceRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-tuition-invoices' : 'update-tuition-invoices');
}
public function rules(): array
{
$rules = [
'academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
'amount_due' => ['required', 'numeric', 'min:0'],
'due_date' => ['required', 'date'],
];
if ($this->isMethod('post')) {
$rules['student_ids'] = ['required', 'array', 'min:1'];
$rules['student_ids.*'] = [
'integer',
Rule::exists('students', 'id'),
Rule::unique('tuition_invoices', 'student_id')
->where('academic_term_id', $this->input('academic_term_id')),
];
} else {
$invoice = $this->route('tuition_invoice');
$rules['student_id'] = [
'required',
'integer',
Rule::exists('students', 'id'),
Rule::unique('tuition_invoices', 'student_id')
->where('academic_term_id', $this->input('academic_term_id'))
->ignore($invoice?->id),
];
}
return $rules;
}
public function messages(): array
{
return [
'student_ids.*.unique' => 'Salah satu mahasiswa yang dipilih sudah memiliki tagihan untuk periode akademik tersebut.',
'student_id.unique' => 'Mahasiswa ini sudah memiliki tagihan untuk periode akademik tersebut.',
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests\Admin\Finances;
use App\Enums\PaymentMethod;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class TuitionPaymentRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-tuition-payments' : 'update-tuition-payments');
}
public function rules(): array
{
$invoice = $this->route('tuition_invoice');
$payment = $this->route('payment');
$otherPaidTotal = $invoice->payments()
->when($payment, fn ($query) => $query->where('id', '!=', $payment->id))
->sum('amount_paid');
$remaining = max(0, round($invoice->amount_due - $otherPaidTotal, 2));
return [
'amount_paid' => ['required', 'numeric', 'min:0.01', "max:{$remaining}"],
'paid_at' => ['required', 'date', 'before_or_equal:now'],
'payment_method' => ['required', Rule::enum(PaymentMethod::class)],
'notes' => ['nullable', 'string'],
'proof' => ['nullable', 'file', 'max:10240', 'mimes:pdf,jpg,jpeg,png'],
];
}
public function messages(): array
{
return [
'amount_paid.max' => 'Jumlah dibayar tidak boleh melebihi sisa tagihan.',
'paid_at.before_or_equal' => 'Tanggal & waktu bayar tidak boleh melebihi waktu saat ini.',
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\Admin\Manage;
use App\Enums\UserRole;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class AnnouncementRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-announcements' : 'update-announcements');
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:200'],
'content' => ['required', 'string'],
'target_roles' => ['required', 'array', 'min:1'],
'target_roles.*' => [
Rule::enum(UserRole::class)->only([UserRole::Mahasiswa, UserRole::Dosen]),
],
];
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace App\Http\Requests\Admin\Manage\CourseClass;
use App\Enums\ClassMethod;
use App\Enums\Semester;
use App\Models\AcademicTerm;
use App\Models\Course;
use App\Models\Lecturer;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class CourseClassRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-course-classes' : 'update-course-classes');
}
public function rules(): array
{
$academicTermId = $this->input('academic_term_id');
$academicTerm = $academicTermId ? AcademicTerm::find($academicTermId) : null;
$semesterParityRule = function ($attribute, $value, $fail) use ($academicTerm) {
if (! $academicTerm) {
return;
}
$semesterNumber = Course::find($value)?->semester_number;
if ($semesterNumber === null) {
return;
}
$isOddSemester = $semesterNumber % 2 === 1;
$matches = $academicTerm->semester === Semester::odd ? $isOddSemester : ! $isOddSemester;
if (! $matches) {
$fail('Mata kuliah semester '.$semesterNumber.' tidak sesuai dengan periode '.$academicTerm->semester->label().'.');
}
};
if ($this->isMethod('post')) {
return [
'course_ids' => ['required', 'array', 'min:1'],
'course_ids.*' => [
'integer',
Rule::exists('courses', 'id'),
Rule::unique('course_classes', 'course_id')
->where('academic_term_id', $academicTermId)
->whereNull('deleted_at'),
$semesterParityRule,
],
'lecturer_id' => [
'required',
'integer',
function ($attribute, $value, $fail) {
$courseIds = (array) $this->input('course_ids', []);
$departmentIds = Course::whereIn('id', $courseIds)->pluck('department_id')->unique();
$lecturer = Lecturer::find($value);
$coveredDepartmentIds = $lecturer
? $lecturer->departments()->pluck('departments.id')
: collect();
if ($departmentIds->diff($coveredDepartmentIds)->isNotEmpty()) {
$fail('Dosen tidak terdaftar di jurusan seluruh mata kuliah yang dipilih.');
}
},
],
'academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
'method' => ['nullable', 'string', Rule::enum(ClassMethod::class)],
];
}
$departmentId = Course::find($this->input('course_id'))?->department_id;
return [
'course_id' => [
'required',
'integer',
Rule::exists('courses', 'id'),
Rule::unique('course_classes', 'course_id')
->where('academic_term_id', $academicTermId)
->whereNull('deleted_at')
->ignore($this->route('course_class')?->id),
$semesterParityRule,
],
'lecturer_id' => [
'required',
'integer',
Rule::exists('lecturer_department', 'lecturer_id')->where('department_id', $departmentId),
],
'academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
'method' => ['nullable', 'string', Rule::enum(ClassMethod::class)],
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Requests\Admin\Manage\CourseClass;
use App\Models\AcademicTerm;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class DuplicateCourseClassesRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create-course-classes');
}
public function rules(): array
{
return [
'source_academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
'target_academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
];
}
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator) {
if ($this->input('source_academic_term_id') === $this->input('target_academic_term_id')) {
$validator->errors()->add('target_academic_term_id', 'Periode tujuan harus berbeda dari periode sumber.');
return;
}
$sourceTerm = AcademicTerm::find($this->input('source_academic_term_id'));
$targetTerm = AcademicTerm::find($this->input('target_academic_term_id'));
if ($sourceTerm && $targetTerm && $sourceTerm->semester !== $targetTerm->semester) {
$validator->errors()->add('target_academic_term_id', 'Periode tujuan harus semester yang sama dengan periode sumber (Ganjil ke Ganjil, Genap ke Genap).');
}
});
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Admin\Manage\CourseRegistration;
use Illuminate\Foundation\Http\FormRequest;
class RejectCourseRegistrationRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('reject-course-registrations')
&& $this->user()->canReviewCourseRegistrationOf($this->route('submission')->student);
}
public function rules(): array
{
return [
'reason' => ['required', 'string', 'max:1000'],
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\Admin\Manage\CourseRegistration;
use Illuminate\Foundation\Http\FormRequest;
class SignAsAdvisorCourseRegistrationRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->isAdvisorOf($this->route('student'));
}
public function rules(): array
{
return [
'signature' => ['required', 'file', 'image', 'max:2048'],
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\Admin\Manage\CourseRegistration;
use Illuminate\Foundation\Http\FormRequest;
class SignAsDepartmentLeaderCourseRegistrationRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->isDepartmentLeaderOf($this->route('student'));
}
public function rules(): array
{
return [
'signature' => ['required', 'file', 'image', 'max:2048'],
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Admin\Manage\CourseRegistration;
use App\Enums\UserRole;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCourseRegistrationRequest extends FormRequest
{
public function authorize(): bool
{
$student = $this->route('student');
return $this->user()->hasRole(UserRole::Mahasiswa->value)
&& $this->user()->student?->id === $student?->id;
}
public function rules(): array
{
return [
'course_class_ids' => ['array'],
'course_class_ids.*' => ['integer'],
'signature' => ['nullable', 'file', 'image', 'max:2048'],
];
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Requests\Admin\Master;
use App\Enums\Semester;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class AcademicTermRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-academic-terms' : 'update-academic-terms');
}
public function rules(): array
{
return [
'academic_year' => [
'required',
'string',
'max:20',
function ($attribute, $value, $fail) {
if (! preg_match('/^(\d{4})\/(\d{4})$/', $value, $matches)) {
$fail('Format tahun ajaran harus YYYY/YYYY, contoh: 2025/2026.');
return;
}
$startYear = (int) $matches[1];
$endYear = (int) $matches[2];
if ($endYear !== $startYear + 1) {
$fail('Tahun kedua harus selisih 1 tahun dari tahun pertama. Contoh: 2025/2026, 2026/2027.');
}
},
],
'semester' => [
'required',
'string',
Rule::enum(Semester::class),
Rule::unique('academic_terms')->where(function ($query) {
return $query->where('academic_year', $this->input('academic_year'));
})->ignore($this->route('academic_term')),
],
'start_date' => [
'required',
'date',
],
'end_date' => [
'required',
'date',
'after_or_equal:start_date',
],
'is_active' => ['boolean'],
'open_semesters' => ['nullable', 'array'],
'open_semesters.*' => ['integer', 'between:1,8'],
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Admin\Master;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class CourseRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-courses' : 'update-courses');
}
public function rules(): array
{
return [
'code' => [
'required',
'string',
'max:20',
Rule::unique('courses')->ignore($this->route('course')),
],
'name' => ['required', 'string', 'max:150'],
'credits' => ['required', 'integer', 'min:1', 'max:10'],
'department_id' => ['required', 'integer', Rule::exists('departments', 'id')],
'semester_number' => ['nullable', 'integer', 'min:1', 'max:14'],
];
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Requests\Admin\Master;
use App\Enums\DegreeLevel;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class DepartmentRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-departments' : 'update-departments');
}
public function rules(): array
{
return [
'code' => [
'required',
'string',
'max:10',
Rule::unique('departments')->ignore($this->route('department')),
],
'name' => ['required', 'string', 'max:100'],
'degree_level' => ['nullable', 'string', Rule::enum(DegreeLevel::class)],
'lecturer_id' => [
'nullable',
Rule::requiredIf($this->route('department') !== null),
'integer',
Rule::exists('lecturer_department', 'lecturer_id')
->where('department_id', $this->route('department')?->id ?? 0),
],
'leadership_started_at' => ['nullable', 'date', 'required_with:lecturer_id'],
];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests\Admin\Services;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class AcademicAdvisingLogRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-academic-advising-logs' : 'update-academic-advising-logs');
}
public function rules(): array
{
return [
'student_ids' => ['required', 'array', 'min:1'],
'student_ids.*' => ['integer', Rule::exists('students', 'id')],
'lecturer_id' => ['required', 'integer', Rule::exists('lecturers', 'id')],
'topic' => ['required', 'string', 'max:150'],
'notes' => ['nullable', 'string'],
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Admin\Services\LetterRequest;
use App\Enums\LetterStatus;
use Illuminate\Foundation\Http\FormRequest;
class LetterRequestRequest extends FormRequest
{
public function authorize(): bool
{
if ($this->isMethod('post')) {
return $this->user()->can('create-letter-requests');
}
$letterRequest = $this->route('letter_request');
return $this->user()->can('update-letter-requests')
&& $letterRequest->user_id === $this->user()->id
&& $letterRequest->status === LetterStatus::Submitted;
}
public function rules(): array
{
return [
'letter_type' => ['required', 'string', 'max:100'],
'purpose' => ['nullable', 'string'],
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Admin\Services\LetterRequest;
use App\Enums\LetterStatus;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class LetterRequestStatusRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('update-letter-requests-status');
}
public function rules(): array
{
return [
'status' => ['nullable', 'required_without:result', Rule::enum(LetterStatus::class)],
'result' => ['nullable', 'file', 'max:10240', 'mimes:pdf,jpg,jpeg,png,doc,docx'],
];
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Settings;
namespace App\Http\Requests\Admin\Settings;
use App\Concerns\PasswordValidationRules;
use Illuminate\Contracts\Validation\ValidationRule;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Settings;
namespace App\Http\Requests\Admin\Settings;
use App\Concerns\PasswordValidationRules;
use Illuminate\Contracts\Validation\ValidationRule;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Settings;
namespace App\Http\Requests\Admin\Settings;
use App\Concerns\ProfileValidationRules;
use Illuminate\Contracts\Validation\ValidationRule;

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class UpdateStatusRequest extends FormRequest
{
public function authorize(): bool
{
$permission = match ($this->route()?->getName()) {
'admin.master.academic-terms.update_status' => 'update-academic-terms-status',
'admin.users.lecturers.update_user_status' => 'update-lecturers-status',
'admin.users.students.update_user_status' => 'update-students-account-status',
'admin.users.administrators.update_user_status' => 'update-administrators-status',
default => null,
};
return $permission !== null && $this->user()->can($permission);
}
public function rules(): array
{
return [
'is_active' => ['required', 'boolean'],
];
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Requests\Admin\Users;
use App\Enums\Gender;
use App\Enums\UserRole;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class AdministratorRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-administrators' : 'update-administrators');
}
public function rules(): array
{
$userId = $this->route('user')?->id;
return [
'username' => [
'required',
'string',
'max:20',
'min:5',
'alpha_dash',
$userId === null
? Rule::unique('users')
: Rule::unique('users')->ignore($userId),
],
'email' => [
'required',
'string',
'email',
'max:255',
$userId === null
? Rule::unique('users')
: Rule::unique('users')->ignore($userId),
],
'role' => [
'required',
'string',
Rule::enum(UserRole::class)->only([UserRole::StaffAdmin, UserRole::StaffKeuangan]),
],
// Profile
'full_name' => ['required', 'string', 'max:150'],
'phone_number' => ['required', 'string', 'max:20'],
'address' => ['required', 'string'],
'gender' => ['required', 'string', Rule::enum(Gender::class)],
'birth_date' => ['required', 'date'],
'birth_place' => ['required', 'string', 'max:100'],
];
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Requests\Admin\Users;
use App\Enums\Gender;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class LecturerRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-lecturers' : 'update-lecturers');
}
public function rules(): array
{
$userId = $this->route('user')?->id;
return [
'username' => [
'required',
'string',
'max:20',
'min:5',
'alpha_dash',
$userId === null
? Rule::unique('users')
: Rule::unique('users')->ignore($userId),
],
'email' => [
'required',
'string',
'email',
'max:255',
$userId === null
? Rule::unique('users')
: Rule::unique('users')->ignore($userId),
],
// Profile
'full_name' => ['required', 'string', 'max:150'],
'phone_number' => ['required', 'string', 'max:20'],
'address' => ['required', 'string'],
'gender' => ['required', 'string', Rule::enum(Gender::class)],
'birth_date' => ['required', 'date'],
'birth_place' => ['required', 'string', 'max:100'],
// Lecturer fields
'lecturer_number' => [
'required',
'string',
'max:20',
Rule::unique('lecturers')->ignore($userId, 'user_id'),
],
'department_ids' => ['required', 'array', 'min:1'],
'department_ids.*' => ['integer', Rule::exists('departments', 'id')],
];
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Http\Requests\Admin\Users\Student;
use App\Enums\Gender;
use App\Enums\StudentStatus;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StudentRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can($this->isMethod('post') ? 'create-students' : 'update-students');
}
public function rules(): array
{
$userId = $this->route('user')?->id;
return [
'username' => [
'required',
'string',
'max:20',
'min:5',
'alpha_dash',
$userId === null
? Rule::unique('users')
: Rule::unique('users')->ignore($userId),
],
'email' => [
'required',
'string',
'email',
'max:255',
$userId === null
? Rule::unique('users')
: Rule::unique('users')->ignore($userId),
],
// Profile
'full_name' => ['required', 'string', 'max:150'],
'phone_number' => ['required', 'string', 'max:20'],
'address' => ['required', 'string'],
'gender' => ['required', 'string', Rule::enum(Gender::class)],
'birth_date' => ['required', 'date'],
'birth_place' => ['required', 'string', 'max:100'],
// Student fields
'student_number' => [
'required',
'string',
'max:10',
Rule::unique('students')->ignore($userId, 'user_id'),
],
'department_id' => [
'required',
'integer',
Rule::exists('departments', 'id'),
],
'enrollment_year' => [
'required',
'integer',
'digits:4',
],
'current_semester' => [
'required',
'integer',
'min:1',
'max:14',
],
'academic_advisor_id' => [
'required',
'integer',
Rule::exists('lecturer_department', 'lecturer_id')
->where('department_id', $this->input('department_id')),
],
'status' => [
'nullable',
'string',
Rule::enum(StudentStatus::class),
],
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\Admin\Users\Student;
use App\Enums\StudentStatus;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StudentStatusRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('update-students-academic-status');
}
public function rules(): array
{
return [
'status' => ['required', 'string', Rule::enum(StudentStatus::class)],
];
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class MediaUploadRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'file' => ['required', 'image', 'max:10240', 'mimes:jpg,jpeg,png,gif,webp'],
'collection' => ['nullable', 'string', 'max:50'],
];
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Requests;
use App\Enums\ClassMethod;
use App\Enums\Gender;
use App\Enums\PaymentMethod;
use App\Enums\Semester;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PaginatedRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'per_page' => ['nullable', 'integer', 'in:25,50,100,999999'],
'search' => ['nullable', 'string', 'max:255'],
'highlight' => ['nullable', 'integer'],
'gender' => ['nullable', 'string', Rule::enum(Gender::class)],
'department_id' => ['nullable', 'integer'],
'status' => ['nullable', 'string'],
'enrollment_year' => ['nullable', 'integer'],
'semester_number' => ['nullable', 'integer'],
'semester' => ['nullable', 'string', Rule::enum(Semester::class)],
'is_active' => ['nullable', Rule::in(['true', 'false'])],
'academic_term_id' => ['nullable', 'integer'],
'method' => ['nullable', 'string', Rule::enum(ClassMethod::class)],
'course_class_id' => ['nullable', 'integer'],
'lecturer_id' => ['nullable', 'integer'],
'type' => ['nullable', 'string'],
'payment_method' => ['nullable', 'string', Rule::enum(PaymentMethod::class)],
'file' => ['nullable', 'string'],
'level' => ['nullable', 'string'],
];
}
public function validatedWithDefaults(): array
{
$validated = $this->validated();
return [
'perPage' => $validated['per_page'] ?? 25,
'search' => $validated['search'] ?? '',
];
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Http\Requests\Settings;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Laravel\Fortify\InteractsWithTwoFactorState;
class TwoFactorAuthenticationRequest extends FormRequest
{
use InteractsWithTwoFactorState;
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
#[Guarded(['id'])]
class AcademicAdvisingLog extends Model
{
use HasFactory;
public function lecturer(): BelongsTo
{
return $this->belongsTo(Lecturer::class);
}
public function students(): BelongsToMany
{
return $this->belongsToMany(Student::class);
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Models;
use App\Enums\Semester;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Guarded(['id'])]
#[Appends(['formatted_start_date', 'formatted_end_date'])]
class AcademicTerm extends Model
{
use HasFactory;
public function courseClasses(): HasMany
{
return $this->hasMany(CourseClass::class);
}
public function courseRegistrations(): HasMany
{
return $this->hasMany(CourseRegistration::class);
}
public function courseRegistrationSubmissions(): HasMany
{
return $this->hasMany(CourseRegistrationSubmission::class);
}
public function tuitionInvoices(): HasMany
{
return $this->hasMany(TuitionInvoice::class);
}
protected function casts(): array
{
return [
'semester' => Semester::class,
'start_date' => 'date',
'end_date' => 'date',
'is_active' => 'boolean',
'open_semesters' => 'array',
];
}
protected function formattedStartDate(): Attribute
{
return Attribute::make(
get: fn () => $this->start_date->format('d M Y'),
);
}
protected function formattedEndDate(): Attribute
{
return Attribute::make(
get: fn () => $this->end_date->format('d M Y'),
);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
#[Guarded(['id'])]
class Announcement extends Model
{
use HasFactory, SoftDeletes;
protected function casts(): array
{
return [
'target_roles' => 'array',
];
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
}

59
app/Models/Assignment.php Normal file
View File

@ -0,0 +1,59 @@
<?php
namespace App\Models;
use App\Enums\AssignmentStatus;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
#[Guarded(['id'])]
#[Appends(['attachment_url', 'attachment_name'])]
class Assignment extends Model implements HasMedia
{
use HasFactory, InteractsWithMedia, SoftDeletes;
protected function casts(): array
{
return [
'deadline' => 'datetime',
'status' => AssignmentStatus::class,
];
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('assignment_attachment')->singleFile();
}
public function courseClass(): BelongsTo
{
return $this->belongsTo(CourseClass::class);
}
public function submissions(): HasMany
{
return $this->hasMany(Submission::class);
}
protected function attachmentUrl(): Attribute
{
return Attribute::make(
get: fn () => $this->getFirstMediaUrl('assignment_attachment') ?: null,
);
}
protected function attachmentName(): Attribute
{
return Attribute::make(
get: fn () => $this->getFirstMedia('assignment_attachment')?->file_name,
);
}
}

Some files were not shown because too many files have changed in this diff Show More