├── public ├── favicon.ico ├── robots.txt ├── index.php └── .htaccess ├── database ├── .gitignore ├── seeders │ ├── TicketSeeder.php │ ├── VendorSeeder.php │ ├── DepartmentSeeder.php │ ├── DatabaseSeeder.php │ └── TicketStatusSeeder.php ├── factories │ ├── DepartmentFactory.php │ ├── VendorFactory.php │ ├── TicketFactory.php │ └── UserFactory.php └── migrations │ ├── 2024_06_09_000007_create_categories_table.php │ ├── 2024_06_09_000002_create_ticket_statuses_table.php │ ├── 2024_06_09_000000_create_departments_table.php │ ├── 2024_06_09_000009_add_user_type_to_users_table.php │ ├── 2024_06_09_000010_add_color_to_ticket_statuses_table.php │ ├── 2024_06_09_000011_add_default_status_to_ticket_statuses_table.php │ ├── 2024_06_09_000001_create_vendors_table.php │ ├── 2024_06_09_000008_add_category_id_to_tickets_table.php │ ├── 2025_11_02_020503_allow_null_user_id_in_comments_table.php │ ├── 2025_11_02_012518_create_notifications_table.php │ ├── 2024_06_09_000005_create_ticket_user_table.php │ ├── 2024_06_09_000006_create_comments_table.php │ ├── 2024_06_09_000004_create_tickets_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 2025_11_02_021315_add_default_admin_user.php │ ├── 2024_06_09_000003_add_vendor_id_and_department_id_to_users_table.php │ ├── 0001_01_01_000000_create_users_table.php │ └── 0001_01_01_000002_create_jobs_table.php ├── bootstrap ├── cache │ └── .gitignore ├── providers.php └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── private │ │ └── .gitignore │ ├── public │ │ └── .gitignore │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── ith-screenshot.jpg ├── ith-screenshot2.png ├── tests ├── Unit │ └── ExampleTest.php ├── Feature │ ├── ExampleTest.php │ ├── Auth │ │ ├── RegistrationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── AuthenticationTest.php │ │ ├── PasswordUpdateTest.php │ │ ├── EmailVerificationTest.php │ │ └── PasswordResetTest.php │ └── ProfileTest.php ├── TestCase.php └── Pest.php ├── postcss.config.js ├── resources ├── js │ ├── app.js │ └── bootstrap.js ├── views │ ├── components │ │ ├── input-label.blade.php │ │ ├── text-input.blade.php │ │ ├── auth-session-status.blade.php │ │ ├── dropdown-link.blade.php │ │ ├── input-error.blade.php │ │ ├── danger-button.blade.php │ │ ├── secondary-button.blade.php │ │ ├── primary-button.blade.php │ │ ├── nav-link.blade.php │ │ ├── responsive-nav-link.blade.php │ │ ├── application-logo.blade.php │ │ ├── dropdown.blade.php │ │ └── modal.blade.php │ ├── auth │ │ ├── confirm-password.blade.php │ │ ├── forgot-password.blade.php │ │ ├── verify-email.blade.php │ │ ├── reset-password.blade.php │ │ ├── login.blade.php │ │ └── register.blade.php │ ├── profile │ │ ├── edit.blade.php │ │ └── partials │ │ │ ├── update-password-form.blade.php │ │ │ ├── delete-user-form.blade.php │ │ │ └── update-profile-information-form.blade.php │ ├── layouts │ │ ├── guest.blade.php │ │ └── app.blade.php │ ├── category │ │ ├── create.blade.php │ │ ├── edit.blade.php │ │ └── index.blade.php │ ├── department │ │ ├── create.blade.php │ │ ├── edit.blade.php │ │ └── index.blade.php │ ├── ticket │ │ └── create.blade.php │ ├── ticket_status │ │ ├── create.blade.php │ │ ├── edit.blade.php │ │ └── index.blade.php │ ├── vendors │ │ ├── create.blade.php │ │ ├── edit.blade.php │ │ ├── show.blade.php │ │ └── index.blade.php │ └── welcome.blade.php └── css │ └── app.css ├── app ├── Http │ ├── Controllers │ │ ├── Controller.php │ │ ├── Auth │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ ├── VerifyEmailController.php │ │ │ ├── PasswordController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── PasswordResetLinkController.php │ │ │ ├── RegisteredUserController.php │ │ │ ├── AuthenticatedSessionController.php │ │ │ └── NewPasswordController.php │ │ ├── NotificationController.php │ │ ├── CategoryController.php │ │ ├── DepartmentController.php │ │ ├── ProfileController.php │ │ ├── Traits │ │ │ └── ManagesTicketSorting.php │ │ ├── DashboardController.php │ │ ├── VendorController.php │ │ ├── TicketStatusController.php │ │ └── UserController.php │ └── Requests │ │ ├── ProfileUpdateRequest.php │ │ └── Auth │ │ └── LoginRequest.php ├── Models │ ├── Category.php │ ├── Department.php │ ├── TicketStatus.php │ ├── Comment.php │ ├── Vendor.php │ ├── User.php │ └── Ticket.php ├── View │ └── Components │ │ ├── AppLayout.php │ │ └── GuestLayout.php ├── Providers │ └── AppServiceProvider.php ├── Rules │ └── ValidAssignee.php ├── Console │ └── Commands │ │ └── AutoCloseResolvedTickets.php ├── Notifications │ ├── TicketCreated.php │ └── TicketUpdated.php └── Policies │ └── TicketPolicy.php ├── .gitattributes ├── vite.config.js ├── .editorconfig ├── .gitignore ├── routes ├── console.php ├── auth.php └── web.php ├── artisan ├── package.json ├── tailwind.config.js ├── LICENSE ├── config ├── services.php ├── filesystems.php ├── cache.php ├── mail.php ├── queue.php ├── auth.php └── app.php ├── phpunit.xml ├── .env.example └── composer.json /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /ith-screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidino/ith/HEAD/ith-screenshot.jpg -------------------------------------------------------------------------------- /ith-screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidino/ith/HEAD/ith-screenshot2.png -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | 3 | import Alpine from 'alpinejs'; 4 | 5 | window.Alpine = Alpine; 6 | 7 | Alpine.start(); 8 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /resources/views/components/input-label.blade.php: -------------------------------------------------------------------------------- 1 | @props(['value']) 2 | 3 | 6 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | false]) 2 | 3 | merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}> 4 | -------------------------------------------------------------------------------- /resources/views/components/auth-session-status.blade.php: -------------------------------------------------------------------------------- 1 | @props(['status']) 2 | 3 | @if ($status) 4 |
merge(['class' => 'font-medium text-sm text-green-600']) }}> 5 | {{ $status }} 6 |
7 | @endif 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /resources/views/components/dropdown-link.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }} 2 | -------------------------------------------------------------------------------- /database/seeders/TicketSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /database/seeders/VendorSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /database/seeders/DepartmentSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | laravel({ 7 | input: ['resources/css/app.css', 'resources/js/app.js'], 8 | refresh: true, 9 | }), 10 | ], 11 | }); 12 | -------------------------------------------------------------------------------- /resources/views/components/input-error.blade.php: -------------------------------------------------------------------------------- 1 | @props(['messages']) 2 | 3 | @if ($messages) 4 | 9 | @endif 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /app/Models/Category.php: -------------------------------------------------------------------------------- 1 | hasMany(Ticket::class); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | .env 4 | .env.backup 5 | .env.production 6 | .phpactor.json 7 | .phpunit.result.cache 8 | /.fleet 9 | /.idea 10 | /.nova 11 | /.phpunit.cache 12 | /.vscode 13 | /.zed 14 | /auth.json 15 | /node_modules 16 | /public/build 17 | /public/hot 18 | /public/storage 19 | /storage/*.key 20 | /storage/pail 21 | /vendor 22 | Homestead.json 23 | Homestead.yaml 24 | Thumbs.db 25 | -------------------------------------------------------------------------------- /app/View/Components/AppLayout.php: -------------------------------------------------------------------------------- 1 | span { 8 | background-color: #2563eb !important; /* blue-600 */ 9 | color: #fff !important; 10 | border-color: #2563eb !important; 11 | font-weight: bold; 12 | z-index: 10; 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /resources/views/components/danger-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/secondary-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 9 | })->purpose('Display an inspiring quote'); 10 | 11 | // Schedule auto-closure of resolved tickets daily at 2 AM 12 | Schedule::command('tickets:auto-close-resolved')->dailyAt('02:00'); 13 | -------------------------------------------------------------------------------- /resources/views/components/primary-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/Models/Department.php: -------------------------------------------------------------------------------- 1 | hasMany(User::class); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 17 | 18 | exit($status); 19 | -------------------------------------------------------------------------------- /app/Models/TicketStatus.php: -------------------------------------------------------------------------------- 1 | 'boolean', 15 | ]; 16 | 17 | public function tickets(): HasMany 18 | { 19 | return $this->hasMany(Ticket::class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /database/factories/DepartmentFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class DepartmentFactory extends Factory 11 | { 12 | public function definition(): array 13 | { 14 | return [ 15 | 'code' => strtoupper(fake()->unique()->lexify('DEPT??')), 16 | 'name' => fake()->company . ' Department', 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Models/Comment.php: -------------------------------------------------------------------------------- 1 | belongsTo(Ticket::class); 19 | } 20 | 21 | public function user(): BelongsTo 22 | { 23 | return $this->belongsTo(User::class); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 9 | web: __DIR__.'/../routes/web.php', 10 | commands: __DIR__.'/../routes/console.php', 11 | health: '/up', 12 | ) 13 | ->withMiddleware(function (Middleware $middleware): void { 14 | // 15 | }) 16 | ->withExceptions(function (Exceptions $exceptions): void { 17 | // 18 | })->create(); 19 | -------------------------------------------------------------------------------- /tests/Feature/Auth/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | 9 | test('new users can register', function () { 10 | $response = $this->post('/register', [ 11 | 'name' => 'Test User', 12 | 'email' => 'test@example.com', 13 | 'password' => 'password', 14 | 'password_confirmation' => 'password', 15 | ]); 16 | 17 | $this->assertAuthenticated(); 18 | $response->assertRedirect(route('dashboard', absolute: false)); 19 | }); 20 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000007_create_categories_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('name'); 14 | $table->timestamps(); 15 | }); 16 | } 17 | 18 | public function down() 19 | { 20 | Schema::dropIfExists('categories'); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000002_create_ticket_statuses_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('name'); 14 | $table->timestamps(); 15 | }); 16 | } 17 | 18 | public function down() 19 | { 20 | Schema::dropIfExists('ticket_statuses'); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/package.json", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "vite build", 7 | "dev": "vite" 8 | }, 9 | "devDependencies": { 10 | "@tailwindcss/forms": "^0.5.2", 11 | "@tailwindcss/vite": "^4.0.0", 12 | "alpinejs": "^3.4.2", 13 | "autoprefixer": "^10.4.2", 14 | "axios": "^1.8.2", 15 | "concurrently": "^9.0.1", 16 | "laravel-vite-plugin": "^1.2.0", 17 | "postcss": "^8.4.31", 18 | "tailwindcss": "^3.1.0", 19 | "vite": "^6.2.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'tailwindcss/defaultTheme'; 2 | import forms from '@tailwindcss/forms'; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | content: [ 7 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 8 | './storage/framework/views/*.php', 9 | './resources/views/**/*.blade.php', 10 | ], 11 | 12 | theme: { 13 | extend: { 14 | fontFamily: { 15 | sans: ['Figtree', ...defaultTheme.fontFamily.sans], 16 | }, 17 | }, 18 | }, 19 | 20 | plugins: [forms], 21 | }; 22 | -------------------------------------------------------------------------------- /app/Models/Vendor.php: -------------------------------------------------------------------------------- 1 | hasMany(User::class); 21 | } 22 | 23 | public function tickets(): HasMany 24 | { 25 | return $this->hasMany(Ticket::class); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000000_create_departments_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('code')->unique(); 14 | $table->string('name'); 15 | $table->timestamps(); 16 | }); 17 | } 18 | 19 | public function down() 20 | { 21 | Schema::dropIfExists('departments'); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000009_add_user_type_to_users_table.php: -------------------------------------------------------------------------------- 1 | string('user_type')->nullable()->after('remember_token'); 13 | }); 14 | } 15 | 16 | public function down() 17 | { 18 | Schema::table('users', function (Blueprint $table) { 19 | $table->dropColumn('user_type'); 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /resources/views/components/nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' 6 | : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000010_add_color_to_ticket_statuses_table.php: -------------------------------------------------------------------------------- 1 | string('color')->nullable()->after('name'); 13 | }); 14 | } 15 | 16 | public function down() 17 | { 18 | Schema::table('ticket_statuses', function (Blueprint $table) { 19 | $table->dropColumn('color'); 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 18 | ? redirect()->intended(route('dashboard', absolute: false)) 19 | : view('auth.verify-email'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000011_add_default_status_to_ticket_statuses_table.php: -------------------------------------------------------------------------------- 1 | boolean('default_status')->default(false)->after('color'); 13 | }); 14 | } 15 | 16 | public function down() 17 | { 18 | Schema::table('ticket_statuses', function (Blueprint $table) { 19 | $table->dropColumn('default_status'); 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /database/factories/VendorFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class VendorFactory extends Factory 11 | { 12 | public function definition(): array 13 | { 14 | return [ 15 | 'code' => strtoupper(fake()->unique()->lexify('VEND??')), 16 | 'name' => fake()->company, 17 | 'phone_number' => fake()->phoneNumber, 18 | 'address' => fake()->address, 19 | 'person_in_charge' => fake()->name, 20 | 'email' => fake()->unique()->companyEmail, 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/views/components/responsive-nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out' 6 | : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /resources/views/components/application-logo.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ITH 11 | 12 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationNotificationController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 17 | return redirect()->intended(route('dashboard', absolute: false)); 18 | } 19 | 20 | $request->user()->sendEmailVerificationNotification(); 21 | 22 | return back()->with('status', 'verification-link-sent'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(TicketStatusSeeder::class); 21 | $this->call(DepartmentSeeder::class); 22 | $this->call(VendorSeeder::class); 23 | User::factory(40)->create(); 24 | $this->call(TicketSeeder::class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000001_create_vendors_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('code')->unique(); 14 | $table->string('name'); 15 | $table->string('phone_number'); 16 | $table->string('address'); 17 | $table->string('person_in_charge'); 18 | $table->string('email'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::dropIfExists('vendors'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000008_add_category_id_to_tickets_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('category_id')->nullable()->after('id'); 13 | $table->foreign('category_id')->references('id')->on('categories')->onDelete('set null'); 14 | }); 15 | } 16 | 17 | public function down() 18 | { 19 | Schema::table('tickets', function (Blueprint $table) { 20 | $table->dropForeign(['category_id']); 21 | $table->dropColumn('category_id'); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/2025_11_02_020503_allow_null_user_id_in_comments_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('user_id')->nullable()->change(); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('comments', function (Blueprint $table) { 25 | $table->unsignedBigInteger('user_id')->nullable(false)->change(); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Handle X-XSRF-Token Header 13 | RewriteCond %{HTTP:x-xsrf-token} . 14 | RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] 15 | 16 | # Redirect Trailing Slashes If Not A Folder... 17 | RewriteCond %{REQUEST_FILENAME} !-d 18 | RewriteCond %{REQUEST_URI} (.+)/$ 19 | RewriteRule ^ %1 [L,R=301] 20 | 21 | # Send Requests To Front Controller... 22 | RewriteCond %{REQUEST_FILENAME} !-d 23 | RewriteCond %{REQUEST_FILENAME} !-f 24 | RewriteRule ^ index.php [L] 25 | 26 | -------------------------------------------------------------------------------- /app/Http/Requests/ProfileUpdateRequest.php: -------------------------------------------------------------------------------- 1 | |string> 15 | */ 16 | public function rules(): array 17 | { 18 | return [ 19 | 'name' => ['required', 'string', 'max:255'], 20 | 'email' => [ 21 | 'required', 22 | 'string', 23 | 'lowercase', 24 | 'email', 25 | 'max:255', 26 | Rule::unique(User::class)->ignore($this->user()->id), 27 | ], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2025_11_02_012518_create_notifications_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 16 | $table->string('type'); 17 | $table->morphs('notifiable'); 18 | $table->text('data'); 19 | $table->timestamp('read_at')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('notifications'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000005_create_ticket_user_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('ticket_id'); 13 | $table->unsignedBigInteger('user_id'); 14 | $table->timestamps(); 15 | 16 | $table->primary(['ticket_id', 'user_id']); 17 | $table->foreign('ticket_id')->references('id')->on('tickets')->onDelete('cascade'); 18 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 19 | }); 20 | } 21 | 22 | public function down() 23 | { 24 | Schema::dropIfExists('ticket_user'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000006_create_comments_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->unsignedBigInteger('ticket_id'); 14 | $table->unsignedBigInteger('user_id')->nullable(); 15 | $table->text('comment'); 16 | $table->timestamps(); 17 | 18 | $table->foreign('ticket_id')->references('id')->on('tickets')->onDelete('cascade'); 19 | $table->foreign('user_id')->references('id')->on('users')->onDelete('set null'); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::dropIfExists('comments'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 18 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 19 | } 20 | 21 | if ($request->user()->markEmailAsVerified()) { 22 | event(new Verified($request->user())); 23 | } 24 | 25 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordController.php: -------------------------------------------------------------------------------- 1 | validateWithBag('updatePassword', [ 19 | 'current_password' => ['required', 'current_password'], 20 | 'password' => ['required', Password::defaults(), 'confirmed'], 21 | ]); 22 | 23 | $request->user()->update([ 24 | 'password' => Hash::make($validated['password']), 25 | ]); 26 | 27 | return back()->with('status', 'password-updated'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 7 | 8 | $response = $this->actingAs($user)->get('/confirm-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('password can be confirmed', function () { 14 | $user = User::factory()->create(); 15 | 16 | $response = $this->actingAs($user)->post('/confirm-password', [ 17 | 'password' => 'password', 18 | ]); 19 | 20 | $response->assertRedirect(); 21 | $response->assertSessionHasNoErrors(); 22 | }); 23 | 24 | test('password is not confirmed with invalid password', function () { 25 | $user = User::factory()->create(); 26 | 27 | $response = $this->actingAs($user)->post('/confirm-password', [ 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $response->assertSessionHasErrors(); 32 | }); 33 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | protected $policies = [ 19 | Ticket::class => TicketPolicy::class, 20 | ]; 21 | 22 | /** 23 | * Register any application services. 24 | */ 25 | public function register(): void 26 | { 27 | // 28 | } 29 | 30 | /** 31 | * Bootstrap any application services. 32 | */ 33 | public function boot(): void 34 | { 35 | Gate::define('admin-only', function (User $user) { 36 | return $user->user_type === 'admin'; 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000004_create_tickets_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('title'); 14 | $table->text('description')->nullable(); 15 | $table->unsignedBigInteger('user_id')->nullable(); 16 | 17 | $table->unsignedBigInteger('ticket_status_id')->nullable(); 18 | $table->timestamps(); 19 | 20 | $table->foreign('user_id')->references('id')->on('users')->onDelete('set null'); 21 | $table->foreign('ticket_status_id')->references('id')->on('ticket_statuses')->onDelete('set null'); 22 | }); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists('tickets'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /resources/views/auth/confirm-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} 4 |
5 | 6 |
7 | @csrf 8 | 9 | 10 |
11 | 12 | 13 | 17 | 18 | 19 |
20 | 21 |
22 | 23 | {{ __('Confirm') }} 24 | 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /database/seeders/TicketStatusSeeder.php: -------------------------------------------------------------------------------- 1 | 'new', 'color' => '#2563eb', 'default_status' => true], 14 | ['name' => 'in_progress', 'color' => '#f59e42', 'default_status' => false], 15 | ['name' => 'pending_vendor', 'color' => '#fbbf24', 'default_status' => false], 16 | ['name' => 'pending_user', 'color' => '#fbbf24', 'default_status' => false], 17 | ['name' => 'resolved', 'color' => '#10b981', 'default_status' => false], 18 | ['name' => 'closed', 'color' => '#6b7280', 'default_status' => false], 19 | ['name' => 'reopen', 'color' => '#ef4444', 'default_status' => false], 20 | ]; 21 | 22 | foreach ($statuses as $status) { 23 | TicketStatus::create($status); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2025_11_02_021315_add_default_admin_user.php: -------------------------------------------------------------------------------- 1 | 'Administrator', 19 | 'email' => 'admin@admin.com', 20 | 'email_verified_at' => now(), 21 | 'password' => Hash::make('Admin123'), 22 | 'user_type' => 'admin', 23 | 'department_id' => null, 24 | 'vendor_id' => null, 25 | ]); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | // Remove the default admin user 34 | User::where('email', 'admin@admin.com')->delete(); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /resources/views/auth/forgot-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} 4 |
5 | 6 | 7 | 8 | 9 |
10 | @csrf 11 | 12 | 13 |
14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | {{ __('Email Password Reset Link') }} 22 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /resources/views/profile/edit.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Profile') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 |
12 | @include('profile.partials.update-profile-information-form') 13 |
14 |
15 | 16 |
17 |
18 | @include('profile.partials.update-password-form') 19 |
20 |
21 | 22 |
23 |
24 | @include('profile.partials.delete-user-form') 25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /database/migrations/2024_06_09_000003_add_vendor_id_and_department_id_to_users_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('vendor_id')->nullable()->after('id'); 13 | $table->unsignedBigInteger('department_id')->nullable()->after('vendor_id'); 14 | 15 | $table->foreign('vendor_id')->references('id')->on('vendors')->onDelete('set null'); 16 | $table->foreign('department_id')->references('id')->on('departments')->onDelete('set null'); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::table('users', function (Blueprint $table) { 23 | $table->dropForeign(['vendor_id']); 24 | $table->dropForeign(['department_id']); 25 | $table->dropColumn(['vendor_id', 'department_id']); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 7 | 8 | $response->assertStatus(200); 9 | }); 10 | 11 | test('users can authenticate using the login screen', function () { 12 | $user = User::factory()->create(); 13 | 14 | $response = $this->post('/login', [ 15 | 'email' => $user->email, 16 | 'password' => 'password', 17 | ]); 18 | 19 | $this->assertAuthenticated(); 20 | $response->assertRedirect(route('dashboard', absolute: false)); 21 | }); 22 | 23 | test('users can not authenticate with invalid password', function () { 24 | $user = User::factory()->create(); 25 | 26 | $this->post('/login', [ 27 | 'email' => $user->email, 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $this->assertGuest(); 32 | }); 33 | 34 | test('users can logout', function () { 35 | $user = User::factory()->create(); 36 | 37 | $response = $this->actingAs($user)->post('/logout'); 38 | 39 | $this->assertGuest(); 40 | $response->assertRedirect('/'); 41 | }); 42 | -------------------------------------------------------------------------------- /app/Http/Controllers/NotificationController.php: -------------------------------------------------------------------------------- 1 | notifications()->paginate(20); 16 | return view('notifications.index', compact('notifications')); 17 | } 18 | 19 | /** 20 | * Mark a specific notification as read. 21 | */ 22 | public function markAsRead($notificationId) 23 | { 24 | $notification = Auth::user()->notifications()->findOrFail($notificationId); 25 | $notification->markAsRead(); 26 | 27 | return redirect()->back()->with('success', 'Notification marked as read.'); 28 | } 29 | 30 | /** 31 | * Mark all notifications as read. 32 | */ 33 | public function markAllAsRead() 34 | { 35 | Auth::user()->unreadNotifications->markAsRead(); 36 | 37 | return redirect()->back()->with('success', 'All notifications marked as read.'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => env('POSTMARK_TOKEN'), 19 | ], 20 | 21 | 'resend' => [ 22 | 'key' => env('RESEND_KEY'), 23 | ], 24 | 25 | 'ses' => [ 26 | 'key' => env('AWS_ACCESS_KEY_ID'), 27 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 28 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 29 | ], 30 | 31 | 'slack' => [ 32 | 'notifications' => [ 33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 35 | ], 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /app/Rules/ValidAssignee.php: -------------------------------------------------------------------------------- 1 | user_type, ['it', 'vendor'])) { 31 | $fail('The selected :attribute is not eligible to be an assignee (must be IT or Vendor).'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 28 | 'email' => $request->user()->email, 29 | 'password' => $request->password, 30 | ])) { 31 | throw ValidationException::withMessages([ 32 | 'password' => __('auth.password'), 33 | ]); 34 | } 35 | 36 | $request->session()->put('auth.password_confirmed_at', time()); 37 | 38 | return redirect()->intended(route('dashboard', absolute: false)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /database/factories/TicketFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class TicketFactory extends Factory 16 | { 17 | public function definition(): array 18 | { 19 | return [ 20 | 'title' => fake()->sentence(6), 21 | 'description' => fake()->paragraph, 22 | 'user_id' => User::whereIn('user_type', ['admin', 'it', 'user'])->inRandomOrder()->value('id'), 23 | 'category_id' => Category::inRandomOrder()->value('id'), 24 | 'ticket_status_id' => TicketStatus::inRandomOrder()->value('id'), 25 | ]; 26 | } 27 | 28 | public function configure() 29 | { 30 | return $this->afterCreating(function ($ticket) { 31 | // Attach 1-3 random users as assignees 32 | $userIds = User::inRandomOrder()->limit(rand(1, 5))->pluck('id'); 33 | $ticket->assignees()->attach($userIds); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /resources/views/layouts/guest.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel') }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | @vite(['resources/css/app.css', 'resources/js/app.js']) 16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 | 25 |
26 | {{ $slot }} 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordUpdateTest.php: -------------------------------------------------------------------------------- 1 | create(); 8 | 9 | $response = $this 10 | ->actingAs($user) 11 | ->from('/profile') 12 | ->put('/password', [ 13 | 'current_password' => 'password', 14 | 'password' => 'new-password', 15 | 'password_confirmation' => 'new-password', 16 | ]); 17 | 18 | $response 19 | ->assertSessionHasNoErrors() 20 | ->assertRedirect('/profile'); 21 | 22 | $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); 23 | }); 24 | 25 | test('correct password must be provided to update password', function () { 26 | $user = User::factory()->create(); 27 | 28 | $response = $this 29 | ->actingAs($user) 30 | ->from('/profile') 31 | ->put('/password', [ 32 | 'current_password' => 'wrong-password', 33 | 'password' => 'new-password', 34 | 'password_confirmation' => 'new-password', 35 | ]); 36 | 37 | $response 38 | ->assertSessionHasErrorsIn('updatePassword', 'current_password') 39 | ->assertRedirect('/profile'); 40 | }); 41 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | APP_LOCALE=en 8 | APP_FALLBACK_LOCALE=en 9 | APP_FAKER_LOCALE=en_US 10 | 11 | APP_MAINTENANCE_DRIVER=file 12 | # APP_MAINTENANCE_STORE=database 13 | 14 | PHP_CLI_SERVER_WORKERS=4 15 | 16 | BCRYPT_ROUNDS=12 17 | 18 | LOG_CHANNEL=stack 19 | LOG_STACK=single 20 | LOG_DEPRECATIONS_CHANNEL=null 21 | LOG_LEVEL=debug 22 | 23 | DB_CONNECTION=mysql 24 | DB_HOST=127.0.0.1 25 | DB_PORT=3306 26 | DB_DATABASE=ith 27 | DB_USERNAME=root 28 | DB_PASSWORD= 29 | 30 | SESSION_DRIVER=database 31 | SESSION_LIFETIME=120 32 | SESSION_ENCRYPT=false 33 | SESSION_PATH=/ 34 | SESSION_DOMAIN=null 35 | 36 | BROADCAST_CONNECTION=log 37 | FILESYSTEM_DISK=local 38 | QUEUE_CONNECTION=database 39 | 40 | CACHE_STORE=database 41 | # CACHE_PREFIX= 42 | 43 | MEMCACHED_HOST=127.0.0.1 44 | 45 | REDIS_CLIENT=phpredis 46 | REDIS_HOST=127.0.0.1 47 | REDIS_PASSWORD=null 48 | REDIS_PORT=6379 49 | 50 | MAIL_MAILER=log 51 | MAIL_SCHEME=null 52 | MAIL_HOST=127.0.0.1 53 | MAIL_PORT=2525 54 | MAIL_USERNAME=null 55 | MAIL_PASSWORD=null 56 | MAIL_FROM_ADDRESS="hello@example.com" 57 | MAIL_FROM_NAME="${APP_NAME}" 58 | 59 | AWS_ACCESS_KEY_ID= 60 | AWS_SECRET_ACCESS_KEY= 61 | AWS_DEFAULT_REGION=us-east-1 62 | AWS_BUCKET= 63 | AWS_USE_PATH_STYLE_ENDPOINT=false 64 | 65 | VITE_APP_NAME="${APP_NAME}" 66 | -------------------------------------------------------------------------------- /resources/views/category/create.blade.php: -------------------------------------------------------------------------------- 1 | {{-- filepath: c:\laragon\www\ith\resources\views\category\create.blade.php --}} 2 | 3 | 4 |

5 | {{ __('Add Category') }} 6 |

7 |
8 |
9 |
10 |
11 |
12 | @csrf 13 |
14 | 15 | 16 | @error('name')
{{ $message }}
@enderror 17 |
18 |
19 | 20 | Cancel 21 |
22 |
23 |
24 |
25 |
26 |
-------------------------------------------------------------------------------- /resources/views/department/create.blade.php: -------------------------------------------------------------------------------- 1 | {{-- filepath: c:\laragon\www\ith\resources\views\department\create.blade.php --}} 2 | 3 | 4 |

5 | {{ __('Add Department') }} 6 |

7 |
8 |
9 |
10 |
11 | 12 | 13 |
14 | @csrf 15 |
16 | 17 | 18 | @error('name')
{{ $message }}
@enderror 19 |
20 |
21 | 22 | Cancel 23 |
24 |
25 |
26 |
27 |
28 |
-------------------------------------------------------------------------------- /resources/views/auth/verify-email.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} 4 |
5 | 6 | @if (session('status') == 'verification-link-sent') 7 |
8 | {{ __('A new verification link has been sent to the email address you provided during registration.') }} 9 |
10 | @endif 11 | 12 |
13 |
14 | @csrf 15 | 16 |
17 | 18 | {{ __('Resend Verification Email') }} 19 | 20 |
21 |
22 | 23 |
24 | @csrf 25 | 26 | 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /resources/views/components/dropdown.blade.php: -------------------------------------------------------------------------------- 1 | @props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white']) 2 | 3 | @php 4 | $alignmentClasses = match ($align) { 5 | 'left' => 'ltr:origin-top-left rtl:origin-top-right start-0', 6 | 'top' => 'origin-top', 7 | default => 'ltr:origin-top-right rtl:origin-top-left end-0', 8 | }; 9 | 10 | $width = match ($width) { 11 | '48' => 'w-48', 12 | default => $width, 13 | }; 14 | @endphp 15 | 16 |
17 |
18 | {{ $trigger }} 19 |
20 | 21 | 35 |
36 | -------------------------------------------------------------------------------- /resources/views/category/edit.blade.php: -------------------------------------------------------------------------------- 1 | {{-- filepath: c:\laragon\www\ith\resources\views\category\edit.blade.php --}} 2 | 3 | 4 |

5 | {{ __('Edit Category') }} 6 |

7 |
8 |
9 |
10 |
11 |
12 | @csrf 13 | @method('PUT') 14 |
15 | 16 | 17 | @error('name')
{{ $message }}
@enderror 18 |
19 |
20 | 21 | Cancel 22 |
23 |
24 |
25 |
26 |
27 |
-------------------------------------------------------------------------------- /resources/views/department/edit.blade.php: -------------------------------------------------------------------------------- 1 | {{-- filepath: c:\laragon\www\ith\resources\views\department\edit.blade.php --}} 2 | 3 | 4 |

5 | {{ __('Edit Department') }} 6 |

7 |
8 |
9 |
10 |
11 |
12 | @csrf 13 | @method('PUT') 14 |
15 | 16 | 17 | @error('name')
{{ $message }}
@enderror 18 |
19 |
20 | 21 | Cancel 22 |
23 |
24 |
25 |
26 |
27 |
-------------------------------------------------------------------------------- /app/Console/Commands/AutoCloseResolvedTickets.php: -------------------------------------------------------------------------------- 1 | automationService = $automationService; 30 | } 31 | 32 | /** 33 | * Execute the console command. 34 | */ 35 | public function handle(): int 36 | { 37 | $this->info('Starting auto-closure of resolved tickets...'); 38 | 39 | $closedCount = $this->automationService->autoCloseResolvedTickets(); 40 | 41 | if ($closedCount > 0) { 42 | $this->info("Successfully auto-closed {$closedCount} resolved tickets."); 43 | } else { 44 | $this->info('No resolved tickets found that need auto-closure.'); 45 | } 46 | 47 | return Command::SUCCESS; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | validate([ 29 | 'email' => ['required', 'email'], 30 | ]); 31 | 32 | // We will send the password reset link to this user. Once we have attempted 33 | // to send the link, we will examine the response then see the message we 34 | // need to show to the user. Finally, we'll send out a proper response. 35 | $status = Password::sendResetLink( 36 | $request->only('email') 37 | ); 38 | 39 | return $status == Password::RESET_LINK_SENT 40 | ? back()->with('status', __($status)) 41 | : back()->withInput($request->only('email')) 42 | ->withErrors(['email' => __($status)]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Feature/Auth/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | unverified()->create(); 10 | 11 | $response = $this->actingAs($user)->get('/verify-email'); 12 | 13 | $response->assertStatus(200); 14 | }); 15 | 16 | test('email can be verified', function () { 17 | $user = User::factory()->unverified()->create(); 18 | 19 | Event::fake(); 20 | 21 | $verificationUrl = URL::temporarySignedRoute( 22 | 'verification.verify', 23 | now()->addMinutes(60), 24 | ['id' => $user->id, 'hash' => sha1($user->email)] 25 | ); 26 | 27 | $response = $this->actingAs($user)->get($verificationUrl); 28 | 29 | Event::assertDispatched(Verified::class); 30 | expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); 31 | $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); 32 | }); 33 | 34 | test('email is not verified with invalid hash', function () { 35 | $user = User::factory()->unverified()->create(); 36 | 37 | $verificationUrl = URL::temporarySignedRoute( 38 | 'verification.verify', 39 | now()->addMinutes(60), 40 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 41 | ); 42 | 43 | $this->actingAs($user)->get($verificationUrl); 44 | 45 | expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); 46 | }); 47 | -------------------------------------------------------------------------------- /app/Http/Controllers/CategoryController.php: -------------------------------------------------------------------------------- 1 | paginate(10); 15 | return view('category.index', compact('categories')); 16 | } 17 | 18 | public function create() 19 | { 20 | return view('category.create'); 21 | } 22 | 23 | public function store(Request $request) 24 | { 25 | $data = $request->validate([ 26 | 'name' => 'required|string|max:255', 27 | ]); 28 | Category::create($data); 29 | return redirect()->route('categories.index')->with('success', 'Category created.'); 30 | } 31 | 32 | public function edit(Category $category) 33 | { 34 | return view('category.edit', compact('category')); 35 | } 36 | 37 | public function update(Request $request, Category $category) 38 | { 39 | $data = $request->validate([ 40 | 'name' => 'required|string|max:255', 41 | ]); 42 | $category->update($data); 43 | return redirect()->route('categories.index')->with('success', 'Category updated.'); 44 | } 45 | 46 | public function destroy(Category $category) 47 | { 48 | $category->delete(); 49 | return redirect()->route('categories.index')->with('success', 'Category deleted.'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | validate([ 33 | 'name' => ['required', 'string', 'max:255'], 34 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 35 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 36 | ]); 37 | 38 | $user = User::create([ 39 | 'name' => $request->name, 40 | 'email' => $request->email, 41 | 'password' => Hash::make($request->password), 42 | ]); 43 | 44 | event(new Registered($user)); 45 | 46 | Auth::login($user); 47 | 48 | return redirect(route('dashboard', absolute: false)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Http/Controllers/DepartmentController.php: -------------------------------------------------------------------------------- 1 | paginate(10); 15 | return view('department.index', compact('departments')); 16 | } 17 | 18 | public function create() 19 | { 20 | return view('department.create'); 21 | } 22 | 23 | public function store(Request $request) 24 | { 25 | $data = $request->validate([ 26 | 'name' => 'required|string|max:255', 27 | ]); 28 | Department::create($data); 29 | return redirect()->route('departments.index')->with('success', 'Department created.'); 30 | } 31 | 32 | public function edit(Department $department) 33 | { 34 | return view('department.edit', compact('department')); 35 | } 36 | 37 | public function update(Request $request, Department $department) 38 | { 39 | $data = $request->validate([ 40 | 'name' => 'required|string|max:255', 41 | ]); 42 | $department->update($data); 43 | return redirect()->route('departments.index')->with('success', 'Department updated.'); 44 | } 45 | 46 | public function destroy(Department $department) 47 | { 48 | $department->delete(); 49 | return redirect()->route('departments.index')->with('success', 'Department deleted.'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Controllers/ProfileController.php: -------------------------------------------------------------------------------- 1 | $request->user(), 21 | ]); 22 | } 23 | 24 | /** 25 | * Update the user's profile information. 26 | */ 27 | public function update(ProfileUpdateRequest $request): RedirectResponse 28 | { 29 | $request->user()->fill($request->validated()); 30 | 31 | if ($request->user()->isDirty('email')) { 32 | $request->user()->email_verified_at = null; 33 | } 34 | 35 | $request->user()->save(); 36 | 37 | return Redirect::route('profile.edit')->with('status', 'profile-updated'); 38 | } 39 | 40 | /** 41 | * Delete the user's account. 42 | */ 43 | public function destroy(Request $request): RedirectResponse 44 | { 45 | $request->validateWithBag('userDeletion', [ 46 | 'password' => ['required', 'current_password'], 47 | ]); 48 | 49 | $user = $request->user(); 50 | 51 | Auth::logout(); 52 | 53 | $user->delete(); 54 | 55 | $request->session()->invalidate(); 56 | $request->session()->regenerateToken(); 57 | 58 | return Redirect::to('/'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | 24 | Schema::create('password_reset_tokens', function (Blueprint $table) { 25 | $table->string('email')->primary(); 26 | $table->string('token'); 27 | $table->timestamp('created_at')->nullable(); 28 | }); 29 | 30 | Schema::create('sessions', function (Blueprint $table) { 31 | $table->string('id')->primary(); 32 | $table->foreignId('user_id')->nullable()->index(); 33 | $table->string('ip_address', 45)->nullable(); 34 | $table->text('user_agent')->nullable(); 35 | $table->longText('payload'); 36 | $table->integer('last_activity')->index(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | */ 43 | public function down(): void 44 | { 45 | Schema::dropIfExists('users'); 46 | Schema::dropIfExists('password_reset_tokens'); 47 | Schema::dropIfExists('sessions'); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class) 15 | ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) 16 | ->in('Feature'); 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Expectations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When you're writing tests, you often need to check that values meet certain conditions. The 24 | | "expect()" function gives you access to a set of "expectations" methods that you can use 25 | | to assert different things. Of course, you may extend the Expectation API at any time. 26 | | 27 | */ 28 | 29 | expect()->extend('toBeOne', function () { 30 | return $this->toBe(1); 31 | }); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Functions 36 | |-------------------------------------------------------------------------- 37 | | 38 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 39 | | project that you don't want to repeat in every file. Here you can also expose helpers as 40 | | global functions to help you to reduce the number of lines of code in your test files. 41 | | 42 | */ 43 | 44 | function something() 45 | { 46 | // .. 47 | } 48 | -------------------------------------------------------------------------------- /resources/views/auth/reset-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | @csrf 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 | 23 |
24 | 25 | 26 | 29 | 30 | 31 |
32 | 33 |
34 | 35 | {{ __('Reset Password') }} 36 | 37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class UserFactory extends Factory 15 | { 16 | /** 17 | * The current password being used by the factory. 18 | */ 19 | protected static ?string $password; 20 | 21 | /** 22 | * Define the model's default state. 23 | * 24 | * @return array 25 | */ 26 | public function definition(): array 27 | { 28 | $userType = fake()->randomElement(['admin', 'it', 'vendor', 'user']); 29 | $departmentId = null; 30 | $vendorId = null; 31 | 32 | if ($userType === 'user') { 33 | $departmentId = Department::inRandomOrder()->value('id'); 34 | } elseif ($userType === 'vendor') { 35 | $vendorId = Vendor::inRandomOrder()->value('id'); 36 | } 37 | 38 | return [ 39 | 'name' => fake()->name(), 40 | 'email' => fake()->unique()->safeEmail(), 41 | 'email_verified_at' => now(), 42 | 'password' => static::$password ??= Hash::make('password'), 43 | 'remember_token' => Str::random(10), 44 | 'user_type' => $userType, 45 | 'department_id' => $departmentId, 46 | 'vendor_id' => $vendorId, 47 | ]; 48 | } 49 | 50 | /** 51 | * Indicate that the model's email address should be unverified. 52 | */ 53 | public function unverified(): static 54 | { 55 | return $this->state(fn (array $attributes) => [ 56 | 'email_verified_at' => null, 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('reset password link can be requested', function () { 14 | Notification::fake(); 15 | 16 | $user = User::factory()->create(); 17 | 18 | $this->post('/forgot-password', ['email' => $user->email]); 19 | 20 | Notification::assertSentTo($user, ResetPassword::class); 21 | }); 22 | 23 | test('reset password screen can be rendered', function () { 24 | Notification::fake(); 25 | 26 | $user = User::factory()->create(); 27 | 28 | $this->post('/forgot-password', ['email' => $user->email]); 29 | 30 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) { 31 | $response = $this->get('/reset-password/'.$notification->token); 32 | 33 | $response->assertStatus(200); 34 | 35 | return true; 36 | }); 37 | }); 38 | 39 | test('password can be reset with valid token', function () { 40 | Notification::fake(); 41 | 42 | $user = User::factory()->create(); 43 | 44 | $this->post('/forgot-password', ['email' => $user->email]); 45 | 46 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 47 | $response = $this->post('/reset-password', [ 48 | 'token' => $notification->token, 49 | 'email' => $user->email, 50 | 'password' => 'password', 51 | 'password_confirmation' => 'password', 52 | ]); 53 | 54 | $response 55 | ->assertSessionHasNoErrors() 56 | ->assertRedirect(route('login')); 57 | 58 | return true; 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | validate([ 28 | 'email' => ['required', 'string', 'email'], 29 | 'password' => ['required', 'string'], 30 | ]); 31 | 32 | if (! Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) { 33 | return back()->withErrors([ 34 | 'email' => __('auth.failed'), 35 | ])->onlyInput('email'); 36 | } 37 | 38 | $request->session()->regenerate(); 39 | 40 | $user = Auth::user(); 41 | if ($user->user_type === 'user') { 42 | return redirect()->route('tickets.mine'); 43 | } elseif ($user->user_type === 'vendor') { 44 | return redirect()->route('tickets.tasks'); 45 | } else { 46 | return redirect()->intended(route('dashboard')); 47 | } 48 | } 49 | 50 | /** 51 | * Destroy an authenticated session. 52 | */ 53 | public function destroy(Request $request): RedirectResponse 54 | { 55 | Auth::guard('web')->logout(); 56 | 57 | $request->session()->invalidate(); 58 | 59 | $request->session()->regenerateToken(); 60 | 61 | return redirect('/'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Http/Controllers/Traits/ManagesTicketSorting.php: -------------------------------------------------------------------------------- 1 | orderBy('title', $direction); 17 | break; 18 | case 'status': 19 | $query->leftJoin('ticket_statuses', 'tickets.ticket_status_id', '=', 'ticket_statuses.id') 20 | ->orderBy('ticket_statuses.name', $direction) 21 | ->select('tickets.*'); 22 | break; 23 | case 'category': 24 | $query->leftJoin('categories', 'tickets.category_id', '=', 'categories.id') 25 | ->orderBy('categories.name', $direction) 26 | ->select('tickets.*'); 27 | break; 28 | case 'department': 29 | // This is the corrected join from the previous step 30 | $query->leftJoin('users', 'tickets.user_id', '=', 'users.id') 31 | ->leftJoin('departments', 'users.department_id', '=', 'departments.id') 32 | ->orderBy('departments.name', $direction) 33 | ->select('tickets.*'); 34 | break; 35 | case 'user': 36 | $query->leftJoin('users as ticket_users', 'tickets.user_id', '=', 'ticket_users.id') 37 | ->orderBy('ticket_users.name', $direction) 38 | ->select('tickets.*'); 39 | break; 40 | case 'created_at': 41 | default: 42 | $query->orderBy('created_at', $direction); 43 | break; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | */ 16 | use HasFactory, Notifiable; 17 | 18 | /** 19 | * The attributes that are mass assignable. 20 | * 21 | * @var list 22 | */ 23 | protected $fillable = [ 24 | 'name', 25 | 'email', 26 | 'password', 27 | 'department_id', 28 | 'vendor_id', 29 | 'user_type', 30 | ]; 31 | 32 | /** 33 | * The attributes that should be hidden for serialization. 34 | * 35 | * @var list 36 | */ 37 | protected $hidden = [ 38 | 'password', 39 | 'remember_token', 40 | ]; 41 | 42 | /** 43 | * Get the attributes that should be cast. 44 | * 45 | * @return array 46 | */ 47 | protected function casts(): array 48 | { 49 | return [ 50 | 'email_verified_at' => 'datetime', 51 | 'password' => 'hashed', 52 | ]; 53 | } 54 | 55 | public function department(): BelongsTo 56 | { 57 | return $this->belongsTo(Department::class); 58 | } 59 | 60 | public function vendor(): BelongsTo 61 | { 62 | return $this->belongsTo(Vendor::class); 63 | } 64 | 65 | public function tickets(): HasMany 66 | { 67 | return $this->hasMany(Ticket::class); 68 | } 69 | 70 | public function comments(): HasMany 71 | { 72 | return $this->hasMany(Comment::class); 73 | } 74 | 75 | public function assignedTickets(): BelongsToMany 76 | { 77 | return $this->belongsToMany(Ticket::class, 'ticket_user'); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /resources/views/auth/login.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | @csrf 7 | 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 | 16 |
17 | 18 | 19 | 23 | 24 | 25 |
26 | 27 | 28 |
29 | 33 |
34 | 35 |
36 | @if (Route::has('password.request')) 37 | 38 | {{ __('Forgot your password?') }} 39 | 40 | @endif 41 | 42 | 43 | {{ __('Log in') }} 44 | 45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /resources/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel') }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | @vite(['resources/css/app.css', 'resources/js/app.js']) 16 | 17 | 18 |
19 | @include('layouts.navigation') 20 | 21 | 22 | @isset($header) 23 |
24 |
25 | {{ $header }} 26 |
27 |
28 | @endisset 29 | 30 | 31 | @if(session('success')) 32 |
33 |
34 | {{ session('success') }} 35 | 40 |
41 |
42 | @endif 43 | 44 | 45 |
46 | {{ $slot }} 47 |
48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /app/Http/Controllers/DashboardController.php: -------------------------------------------------------------------------------- 1 | leftJoin('tickets', 'tickets.category_id', '=', 'categories.id') 20 | ->groupBy('categories.id', 'categories.name') 21 | ->selectRaw('count(tickets.id) as count') 22 | ->get(); 23 | 24 | $statuses = TicketStatus::select('id', 'name', 'color') 25 | ->get() 26 | ->keyBy('name'); 27 | 28 | $statusCounts = TicketStatus::select('name') 29 | ->leftJoin('tickets', 'tickets.ticket_status_id', '=', 'ticket_statuses.id') 30 | ->groupBy('ticket_statuses.id', 'ticket_statuses.name') 31 | ->selectRaw('count(tickets.id) as count') 32 | ->pluck('count', 'name'); 33 | 34 | $statusColors = $statuses->mapWithKeys(function($status) { 35 | return [$status->name => $status->color]; 36 | }); 37 | 38 | $departmentCounts = \App\Models\Department::select('departments.name') 39 | ->leftJoin('users', 'users.department_id', '=', 'departments.id') 40 | ->leftJoin('tickets', 'tickets.user_id', '=', 'users.id') 41 | ->groupBy('departments.id', 'departments.name') 42 | ->selectRaw('count(tickets.id) as count') 43 | ->get(); 44 | 45 | $user = Auth::user(); 46 | $assignedToMeCount = 0; 47 | $createdByMeCount = 0; 48 | if ($user) { 49 | $assignedToMeCount = $user->assignedTickets()->count(); 50 | $createdByMeCount = $user->tickets()->count(); 51 | } 52 | 53 | return view('dashboard', compact( 54 | 'categoryCounts', 55 | 'statusCounts', 56 | 'statusColors', 57 | 'departmentCounts', 58 | 'assignedToMeCount', 59 | 'createdByMeCount' 60 | )); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Models/Ticket.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 28 | } 29 | 30 | public function category(): BelongsTo 31 | { 32 | return $this->belongsTo(Category::class); 33 | } 34 | 35 | public function status(): BelongsTo 36 | { 37 | return $this->belongsTo(TicketStatus::class, 'ticket_status_id'); 38 | } 39 | 40 | public function comments(): HasMany 41 | { 42 | return $this->hasMany(Comment::class); 43 | } 44 | 45 | public function assignees(): BelongsToMany 46 | { 47 | return $this->belongsToMany(User::class, 'ticket_user'); 48 | } 49 | 50 | /** 51 | * Scope a query to only include tickets of a given status. 52 | * 53 | * @param \Illuminate\Database\Eloquent\Builder $query 54 | * @param mixed $statusId 55 | * @return \Illuminate\Database\Eloquent\Builder 56 | */ 57 | public function scopeFilterByStatus($query, $statusId) 58 | { 59 | if ($statusId) { 60 | return $query->where('ticket_status_id', $statusId); 61 | } 62 | return $query; 63 | } 64 | 65 | /** 66 | * Scope a query to only include tickets of a given category. 67 | * 68 | * @param \Illuminate\Database\Eloquent\Builder $query 69 | * @param mixed $categoryId 70 | * @return \Illuminate\Database\Eloquent\Builder 71 | */ 72 | public function scopeFilterByCategory($query, $categoryId) 73 | { 74 | if ($categoryId) { 75 | return $query->where('category_id', $categoryId); 76 | } 77 | return $query; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /resources/views/profile/partials/update-password-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('Update Password') }} 5 |

6 | 7 |

8 | {{ __('Ensure your account is using a long, random password to stay secure.') }} 9 |

10 |
11 | 12 |
13 | @csrf 14 | @method('put') 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 |
33 | 34 |
35 | {{ __('Save') }} 36 | 37 | @if (session('status') === 'password-updated') 38 |

{{ __('Saved.') }}

45 | @endif 46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /app/Notifications/TicketCreated.php: -------------------------------------------------------------------------------- 1 | ticket = $ticket; 23 | } 24 | 25 | /** 26 | * Get the notification's delivery channels. 27 | * 28 | * @return array 29 | */ 30 | public function via(object $notifiable): array 31 | { 32 | return ['database', 'mail']; 33 | } 34 | 35 | /** 36 | * Get the mail representation of the notification. 37 | */ 38 | public function toMail(object $notifiable): MailMessage 39 | { 40 | return (new MailMessage) 41 | ->subject('New Ticket Created: ' . $this->ticket->title) 42 | ->line('A new ticket has been created.') 43 | ->line('Title: ' . $this->ticket->title) 44 | ->line('Category: ' . $this->ticket->category->name) 45 | ->line('Created by: ' . $this->ticket->user->name) 46 | ->action('View Ticket', route('tickets.show', $this->ticket)) 47 | ->line('Please review and take appropriate action.'); 48 | } 49 | 50 | /** 51 | * Get the database representation of the notification. 52 | * 53 | * @return array 54 | */ 55 | public function toDatabase(object $notifiable): array 56 | { 57 | return [ 58 | 'ticket_id' => $this->ticket->id, 59 | 'ticket_title' => $this->ticket->title, 60 | 'created_by' => $this->ticket->user->name, 61 | 'category' => $this->ticket->category->name, 62 | 'message' => "New ticket '{$this->ticket->title}' has been created by {$this->ticket->user->name}", 63 | ]; 64 | } 65 | 66 | /** 67 | * Get the array representation of the notification. 68 | * 69 | * @return array 70 | */ 71 | public function toArray(object $notifiable): array 72 | { 73 | return $this->toDatabase($notifiable); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /resources/views/profile/partials/delete-user-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('Delete Account') }} 5 |

6 | 7 |

8 | {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} 9 |

10 |
11 | 12 | {{ __('Delete Account') }} 16 | 17 | 18 |
19 | @csrf 20 | @method('delete') 21 | 22 |

23 | {{ __('Are you sure you want to delete your account?') }} 24 |

25 | 26 |

27 | {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} 28 |

29 | 30 |
31 | 32 | 33 | 40 | 41 | 42 |
43 | 44 |
45 | 46 | {{ __('Cancel') }} 47 | 48 | 49 | 50 | {{ __('Delete Account') }} 51 | 52 |
53 |
54 |
55 |
56 | -------------------------------------------------------------------------------- /resources/views/auth/register.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | @csrf 4 | 5 | 6 |
7 | 8 | 9 | 10 |
11 | 12 | 13 |
14 | 15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 | 27 | 28 | 29 |
30 | 31 | 32 |
33 | 34 | 35 | 38 | 39 | 40 |
41 | 42 |
43 | 44 | {{ __('Already registered?') }} 45 | 46 | 47 | 48 | {{ __('Register') }} 49 | 50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /tests/Feature/ProfileTest.php: -------------------------------------------------------------------------------- 1 | create(); 7 | 8 | $response = $this 9 | ->actingAs($user) 10 | ->get('/profile'); 11 | 12 | $response->assertOk(); 13 | }); 14 | 15 | test('profile information can be updated', function () { 16 | $user = User::factory()->create(); 17 | 18 | $response = $this 19 | ->actingAs($user) 20 | ->patch('/profile', [ 21 | 'name' => 'Test User', 22 | 'email' => 'test@example.com', 23 | ]); 24 | 25 | $response 26 | ->assertSessionHasNoErrors() 27 | ->assertRedirect('/profile'); 28 | 29 | $user->refresh(); 30 | 31 | $this->assertSame('Test User', $user->name); 32 | $this->assertSame('test@example.com', $user->email); 33 | $this->assertNull($user->email_verified_at); 34 | }); 35 | 36 | test('email verification status is unchanged when the email address is unchanged', function () { 37 | $user = User::factory()->create(); 38 | 39 | $response = $this 40 | ->actingAs($user) 41 | ->patch('/profile', [ 42 | 'name' => 'Test User', 43 | 'email' => $user->email, 44 | ]); 45 | 46 | $response 47 | ->assertSessionHasNoErrors() 48 | ->assertRedirect('/profile'); 49 | 50 | $this->assertNotNull($user->refresh()->email_verified_at); 51 | }); 52 | 53 | test('user can delete their account', function () { 54 | $user = User::factory()->create(); 55 | 56 | $response = $this 57 | ->actingAs($user) 58 | ->delete('/profile', [ 59 | 'password' => 'password', 60 | ]); 61 | 62 | $response 63 | ->assertSessionHasNoErrors() 64 | ->assertRedirect('/'); 65 | 66 | $this->assertGuest(); 67 | $this->assertNull($user->fresh()); 68 | }); 69 | 70 | test('correct password must be provided to delete account', function () { 71 | $user = User::factory()->create(); 72 | 73 | $response = $this 74 | ->actingAs($user) 75 | ->from('/profile') 76 | ->delete('/profile', [ 77 | 'password' => 'wrong-password', 78 | ]); 79 | 80 | $response 81 | ->assertSessionHasErrorsIn('userDeletion', 'password') 82 | ->assertRedirect('/profile'); 83 | 84 | $this->assertNotNull($user->fresh()); 85 | }); 86 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/NewPasswordController.php: -------------------------------------------------------------------------------- 1 | $request]); 24 | } 25 | 26 | /** 27 | * Handle an incoming new password request. 28 | * 29 | * @throws \Illuminate\Validation\ValidationException 30 | */ 31 | public function store(Request $request): RedirectResponse 32 | { 33 | $request->validate([ 34 | 'token' => ['required'], 35 | 'email' => ['required', 'email'], 36 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 37 | ]); 38 | 39 | // Here we will attempt to reset the user's password. If it is successful we 40 | // will update the password on an actual user model and persist it to the 41 | // database. Otherwise we will parse the error and return the response. 42 | $status = Password::reset( 43 | $request->only('email', 'password', 'password_confirmation', 'token'), 44 | function (User $user) use ($request) { 45 | $user->forceFill([ 46 | 'password' => Hash::make($request->password), 47 | 'remember_token' => Str::random(60), 48 | ])->save(); 49 | 50 | event(new PasswordReset($user)); 51 | } 52 | ); 53 | 54 | // If the password was successfully reset, we will redirect the user back to 55 | // the application's home authenticated view. If there is an error we can 56 | // redirect them back to where they came from with their error message. 57 | return $status == Password::PASSWORD_RESET 58 | ? redirect()->route('login')->with('status', __($status)) 59 | : back()->withInput($request->only('email')) 60 | ->withErrors(['email' => __($status)]); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Http/Controllers/VendorController.php: -------------------------------------------------------------------------------- 1 | paginate(10); 15 | return view('vendors.index', compact('vendors')); 16 | } 17 | 18 | public function create() 19 | { 20 | return view('vendors.create'); 21 | } 22 | 23 | public function store(Request $request) 24 | { 25 | $request->validate([ 26 | 'code' => 'required|string|max:255|unique:vendors', 27 | 'name' => 'required|string|max:255', 28 | 'phone_number' => 'nullable|string|max:255', 29 | 'address' => 'nullable|string|max:255', 30 | 'person_in_charge' => 'nullable|string|max:255', 31 | 'email' => 'nullable|string|email|max:255', 32 | ]); 33 | 34 | Vendor::create($request->only('code', 'name', 'phone_number', 'address', 'person_in_charge', 'email')); 35 | 36 | return redirect()->route('vendors.index')->with('success', 'Vendor created successfully.'); 37 | } 38 | 39 | public function edit(Vendor $vendor) 40 | { 41 | return view('vendors.edit', compact('vendor')); 42 | } 43 | 44 | public function update(Request $request, Vendor $vendor) 45 | { 46 | $request->validate([ 47 | 'code' => 'required|string|max:255|unique:vendors,code,' . $vendor->id, 48 | 'name' => 'required|string|max:255', 49 | 'phone_number' => 'nullable|string|max:255', 50 | 'address' => 'nullable|string|max:255', 51 | 'person_in_charge' => 'nullable|string|max:255', 52 | 'email' => 'nullable|string|email|max:255', 53 | ]); 54 | 55 | $vendor->update($request->only('code', 'name', 'phone_number', 'address', 'person_in_charge', 'email')); 56 | 57 | return redirect()->route('vendors.index')->with('success', 'Vendor updated successfully.'); 58 | } 59 | 60 | public function destroy(Vendor $vendor) 61 | { 62 | $vendor->delete(); 63 | return redirect()->route('vendors.index')->with('success', 'Vendor deleted successfully.'); 64 | } 65 | 66 | public function show(Vendor $vendor) 67 | { 68 | $vendor->load('users'); 69 | return view('vendors.show', compact('vendor')); 70 | } 71 | } -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | group(function () { 15 | Route::get('register', [RegisteredUserController::class, 'create']) 16 | ->name('register'); 17 | 18 | Route::post('register', [RegisteredUserController::class, 'store']); 19 | 20 | Route::get('login', [AuthenticatedSessionController::class, 'create']) 21 | ->name('login'); 22 | 23 | Route::post('login', [AuthenticatedSessionController::class, 'store']); 24 | 25 | Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) 26 | ->name('password.request'); 27 | 28 | Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) 29 | ->name('password.email'); 30 | 31 | Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) 32 | ->name('password.reset'); 33 | 34 | Route::post('reset-password', [NewPasswordController::class, 'store']) 35 | ->name('password.store'); 36 | }); 37 | 38 | Route::middleware('auth')->group(function () { 39 | Route::get('verify-email', EmailVerificationPromptController::class) 40 | ->name('verification.notice'); 41 | 42 | Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) 43 | ->middleware(['signed', 'throttle:6,1']) 44 | ->name('verification.verify'); 45 | 46 | Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) 47 | ->middleware('throttle:6,1') 48 | ->name('verification.send'); 49 | 50 | Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) 51 | ->name('password.confirm'); 52 | 53 | Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); 54 | 55 | Route::put('password', [PasswordController::class, 'update'])->name('password.update'); 56 | 57 | Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) 58 | ->name('logout'); 59 | }); 60 | -------------------------------------------------------------------------------- /resources/views/ticket/create.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Create New Ticket') }} 5 |

6 |
7 |
8 |
9 |
10 | 11 | 12 |
13 | @csrf 14 |
15 | 16 | 17 | @error('title')
{{ $message }}
@enderror 18 |
19 | 20 | 21 |
22 | 23 | 31 | @error('category_id')
{{ $message }}
@enderror 32 |
33 | 34 |
35 | 36 | 37 | @error('description')
{{ $message }}
@enderror 38 |
39 | 40 | 41 | 42 | 43 |
44 | 45 | Cancel 46 |
47 |
48 |
49 |
50 |
51 |
-------------------------------------------------------------------------------- /app/Http/Requests/Auth/LoginRequest.php: -------------------------------------------------------------------------------- 1 | |string> 26 | */ 27 | public function rules(): array 28 | { 29 | return [ 30 | 'email' => ['required', 'string', 'email'], 31 | 'password' => ['required', 'string'], 32 | ]; 33 | } 34 | 35 | /** 36 | * Attempt to authenticate the request's credentials. 37 | * 38 | * @throws \Illuminate\Validation\ValidationException 39 | */ 40 | public function authenticate(): void 41 | { 42 | $this->ensureIsNotRateLimited(); 43 | 44 | if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { 45 | RateLimiter::hit($this->throttleKey()); 46 | 47 | throw ValidationException::withMessages([ 48 | 'email' => trans('auth.failed'), 49 | ]); 50 | } 51 | 52 | RateLimiter::clear($this->throttleKey()); 53 | } 54 | 55 | /** 56 | * Ensure the login request is not rate limited. 57 | * 58 | * @throws \Illuminate\Validation\ValidationException 59 | */ 60 | public function ensureIsNotRateLimited(): void 61 | { 62 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { 63 | return; 64 | } 65 | 66 | event(new Lockout($this)); 67 | 68 | $seconds = RateLimiter::availableIn($this->throttleKey()); 69 | 70 | throw ValidationException::withMessages([ 71 | 'email' => trans('auth.throttle', [ 72 | 'seconds' => $seconds, 73 | 'minutes' => ceil($seconds / 60), 74 | ]), 75 | ]); 76 | } 77 | 78 | /** 79 | * Get the rate limiting throttle key for the request. 80 | */ 81 | public function throttleKey(): string 82 | { 83 | return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /resources/views/ticket_status/create.blade.php: -------------------------------------------------------------------------------- 1 | {{-- filepath: c:\laragon\www\ith\resources\views\ticket_status\create.blade.php --}} 2 | 3 | 4 |

5 | {{ __('Add Ticket Status') }} 6 |

7 |
8 | 9 | 10 |
11 |
12 |
13 |
14 | 15 |
16 | @csrf 17 |
18 |
19 | 20 | 21 | @error('name')
{{ $message }}
@enderror 22 |
23 |
24 | 25 | 26 | @error('color')
{{ $message }}
@enderror 27 |
28 |
29 |
30 | 34 | @error('default_status')
{{ $message }}
@enderror 35 |
36 |
37 | 38 | Cancel 39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
-------------------------------------------------------------------------------- /app/Policies/TicketPolicy.php: -------------------------------------------------------------------------------- 1 | user_type, ['admin', 'it']); 13 | } 14 | 15 | public function view(User $user, Ticket $ticket) 16 | { 17 | // Admin and IT can view all, user can view own, vendor can view assigned 18 | if ($user->user_type === 'admin' || $user->user_type === 'it') { 19 | return true; 20 | } 21 | if ($user->user_type === 'user') { 22 | return $ticket->user_id === $user->id; 23 | } 24 | if ($user->user_type === 'vendor') { 25 | return $ticket->assignees->contains($user->id); 26 | } 27 | return false; 28 | } 29 | 30 | public function update(User $user, Ticket $ticket) 31 | { 32 | // Admin and IT can update all, user can update own, vendor can update assigned 33 | return $this->view($user, $ticket); 34 | } 35 | 36 | public function delete(User $user, Ticket $ticket) 37 | { 38 | // Only admin or IT can delete 39 | return $user->user_type === 'admin' || $user->user_type === 'it'; 40 | } 41 | 42 | public function create(User $user) 43 | { 44 | // All authenticated users can create 45 | return in_array($user->user_type, ['admin', 'it', 'user']); 46 | } 47 | 48 | public function updateStatus(User $user, Ticket $ticket) 49 | { 50 | // Admin, IT, or assigned Vendor can update status 51 | if (in_array($user->user_type, ['admin', 'it'])) { 52 | return true; 53 | } 54 | if ($user->user_type === 'vendor') { 55 | return $ticket->assignees->contains($user->id); 56 | } 57 | return false; 58 | } 59 | 60 | public function addComment(User $user, Ticket $ticket) 61 | { 62 | // Anyone who can view the ticket can add a comment 63 | return $this->view($user, $ticket); 64 | } 65 | 66 | public function assignUser(User $user, Ticket $ticket) 67 | { 68 | // Only Admin or IT can assign users 69 | return in_array($user->user_type, ['admin', 'it']); 70 | } 71 | 72 | public function updateCategory(User $user, Ticket $ticket) 73 | { 74 | // Only Admin or IT can update category 75 | return in_array($user->user_type, ['admin', 'it']); 76 | } 77 | 78 | public function resolveTicket(User $user, Ticket $ticket) 79 | { 80 | // Only Admin can mark tickets as resolved 81 | return $user->user_type === 'admin'; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /resources/views/ticket_status/edit.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Edit Ticket Status') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 |
12 | 13 |
14 | @csrf 15 | @method('PUT') 16 |
17 |
18 | 19 | 20 | @error('name')
{{ $message }}
@enderror 21 |
22 |
23 | 24 | 25 | @error('color')
{{ $message }}
@enderror 26 |
27 |
28 |
29 | 34 | @error('default_status')
{{ $message }}
@enderror 35 |
36 |
37 | 38 | Cancel 39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
-------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://getcomposer.org/schema.json", 3 | "name": "laravel/laravel", 4 | "type": "project", 5 | "description": "The skeleton application for the Laravel framework.", 6 | "keywords": [ 7 | "laravel", 8 | "framework" 9 | ], 10 | "license": "MIT", 11 | "require": { 12 | "php": "^8.2", 13 | "laravel/framework": "^12.0", 14 | "laravel/tinker": "^2.10.1" 15 | }, 16 | "require-dev": { 17 | "fakerphp/faker": "^1.23", 18 | "laravel/breeze": "^2.3", 19 | "laravel/pail": "^1.2.2", 20 | "laravel/pint": "^1.13", 21 | "laravel/sail": "^1.41", 22 | "mockery/mockery": "^1.6", 23 | "nunomaduro/collision": "^8.6", 24 | "pestphp/pest": "^3.8", 25 | "pestphp/pest-plugin-laravel": "^3.2" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "App\\": "app/", 30 | "Database\\Factories\\": "database/factories/", 31 | "Database\\Seeders\\": "database/seeders/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Tests\\": "tests/" 37 | } 38 | }, 39 | "scripts": { 40 | "post-autoload-dump": [ 41 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 42 | "@php artisan package:discover --ansi" 43 | ], 44 | "post-update-cmd": [ 45 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 46 | ], 47 | "post-root-package-install": [ 48 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 49 | ], 50 | "post-create-project-cmd": [ 51 | "@php artisan key:generate --ansi", 52 | "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", 53 | "@php artisan migrate --graceful --ansi" 54 | ], 55 | "dev": [ 56 | "Composer\\Config::disableProcessTimeout", 57 | "npx concurrently -c \"#93c5fd,#c4b5fd,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"npm run dev\" --names='server,queue,vite'" 58 | ], 59 | "test": [ 60 | "@php artisan config:clear --ansi", 61 | "@php artisan test" 62 | ] 63 | }, 64 | "extra": { 65 | "laravel": { 66 | "dont-discover": [] 67 | } 68 | }, 69 | "config": { 70 | "optimize-autoloader": true, 71 | "preferred-install": "dist", 72 | "sort-packages": true, 73 | "allow-plugins": { 74 | "pestphp/pest-plugin": true, 75 | "php-http/discovery": true 76 | } 77 | }, 78 | "minimum-stability": "stable", 79 | "prefer-stable": true 80 | } 81 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Below you may configure as many filesystem disks as necessary, and you 24 | | may even configure multiple disks for the same driver. Examples for 25 | | most supported storage drivers are configured here for reference. 26 | | 27 | | Supported drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app/private'), 36 | 'serve' => true, 37 | 'throw' => false, 38 | 'report' => false, 39 | ], 40 | 41 | 'public' => [ 42 | 'driver' => 'local', 43 | 'root' => storage_path('app/public'), 44 | 'url' => env('APP_URL').'/storage', 45 | 'visibility' => 'public', 46 | 'throw' => false, 47 | 'report' => false, 48 | ], 49 | 50 | 's3' => [ 51 | 'driver' => 's3', 52 | 'key' => env('AWS_ACCESS_KEY_ID'), 53 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 54 | 'region' => env('AWS_DEFAULT_REGION'), 55 | 'bucket' => env('AWS_BUCKET'), 56 | 'url' => env('AWS_URL'), 57 | 'endpoint' => env('AWS_ENDPOINT'), 58 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 59 | 'throw' => false, 60 | 'report' => false, 61 | ], 62 | 63 | ], 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Symbolic Links 68 | |-------------------------------------------------------------------------- 69 | | 70 | | Here you may configure the symbolic links that will be created when the 71 | | `storage:link` Artisan command is executed. The array keys should be 72 | | the locations of the links and the values should be their targets. 73 | | 74 | */ 75 | 76 | 'links' => [ 77 | public_path('storage') => storage_path('app/public'), 78 | ], 79 | 80 | ]; 81 | -------------------------------------------------------------------------------- /app/Http/Controllers/TicketStatusController.php: -------------------------------------------------------------------------------- 1 | user_type !== 'admin') { 14 | abort(403, 'Unauthorized'); 15 | } 16 | } 17 | 18 | public function index() 19 | { 20 | $statuses = TicketStatus::orderBy('id')->paginate(10); 21 | return view('ticket_status.index', compact('statuses')); 22 | } 23 | 24 | public function create() 25 | { 26 | return view('ticket_status.create'); 27 | } 28 | 29 | public function store(Request $request) 30 | { 31 | $data = $request->validate([ 32 | 'name' => 'required|string|max:255', 33 | 'color' => 'nullable|string|max:32', 34 | 'default_status' => 'nullable|boolean', 35 | ]); 36 | $data['default_status'] = $request->has('default_status') ? (bool)$request->default_status : false; 37 | 38 | if ($data['default_status']) { 39 | TicketStatus::query()->update(['default_status' => false]); 40 | } 41 | 42 | TicketStatus::create($data); 43 | return redirect()->route('ticket-statuses.index')->with('success', 'Status created.'); 44 | } 45 | 46 | public function show(TicketStatus $ticket_status) 47 | { 48 | return view('ticket_status.show', compact('ticket_status')); 49 | } 50 | 51 | public function edit(TicketStatus $ticket_status) 52 | { 53 | return view('ticket_status.edit', compact('ticket_status')); 54 | } 55 | 56 | public function update(Request $request, TicketStatus $ticket_status) 57 | { 58 | $data = $request->validate([ 59 | 'name' => 'required|string|max:255', 60 | 'color' => 'nullable|string|max:32', 61 | 'default_status' => 'nullable|boolean', 62 | ]); 63 | 64 | $data['default_status'] = $request->has('default_status') ? (bool)$request->default_status : false; 65 | 66 | if ($data['default_status']) { 67 | $ostatus = TicketStatus::query()->where('id', '!=', $ticket_status->id)->first(); 68 | if ($ostatus) { 69 | $ostatus->update(['default_status' => false]); 70 | } 71 | } 72 | 73 | $ticket_status->update($data); 74 | return redirect()->route('ticket-statuses.index')->with('success', 'Status updated.'); 75 | } 76 | 77 | public function destroy(TicketStatus $ticket_status) 78 | { 79 | $ticket_status->delete(); 80 | return redirect()->route('ticket-statuses.index')->with('success', 'Status deleted.'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /resources/views/profile/partials/update-profile-information-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('Profile Information') }} 5 |

6 | 7 |

8 | {{ __("Update your account's profile information and email address.") }} 9 |

10 |
11 | 12 |
13 | @csrf 14 |
15 | 16 |
17 | @csrf 18 | @method('patch') 19 | 20 |
21 | 22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 | 30 | 31 | @if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail()) 32 |
33 |

34 | {{ __('Your email address is unverified.') }} 35 | 36 | 39 |

40 | 41 | @if (session('status') === 'verification-link-sent') 42 |

43 | {{ __('A new verification link has been sent to your email address.') }} 44 |

45 | @endif 46 |
47 | @endif 48 |
49 | 50 |
51 | {{ __('Save') }} 52 | 53 | @if (session('status') === 'profile-updated') 54 |

{{ __('Saved.') }}

61 | @endif 62 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /resources/views/category/index.blade.php: -------------------------------------------------------------------------------- 1 | {{-- filepath: c:\laragon\www\ith\resources\views\category\index.blade.php --}} 2 | 3 | 4 |
5 |

6 | {{ __('Categories') }} 7 |

8 | 9 | + Add Category 10 | 11 |
12 |
13 |
14 |
15 |
16 | 17 | @if($categories->hasPages()) 18 | 21 | @endif 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | @forelse($categories as $category) 33 | 34 | 35 | 36 | 44 | 45 | @empty 46 | 47 | 48 | 49 | @endforelse 50 | 51 |
IDNameActions
{{ $category->id }}{{ $category->name }} 37 | Edit 38 |
39 | @csrf 40 | @method('DELETE') 41 | 42 |
43 |
No categories found.
52 |
53 |
54 |
55 |
-------------------------------------------------------------------------------- /resources/views/department/index.blade.php: -------------------------------------------------------------------------------- 1 | {{-- filepath: c:\laragon\www\ith\resources\views\department\index.blade.php --}} 2 | 3 | 4 |
5 |

6 | {{ __('Departments') }} 7 |

8 | 9 | + Add Department 10 | 11 |
12 |
13 |
14 |
15 |
16 | 17 | @if($departments->hasPages()) 18 | 21 | @endif 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | @forelse($departments as $department) 33 | 34 | 35 | 36 | 44 | 45 | @empty 46 | 47 | 48 | 49 | @endforelse 50 | 51 |
IDNameActions
{{ $department->id }}{{ $department->name }} 37 | Edit 38 |
39 | @csrf 40 | @method('DELETE') 41 | 42 |
43 |
No departments found.
52 |
53 |
54 |
55 |
-------------------------------------------------------------------------------- /resources/views/vendors/create.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Add Vendor') }} 5 |

6 |
7 |
8 |
9 |
10 |
11 | @csrf 12 |
13 | 14 | 15 | @error('code')
{{ $message }}
@enderror 16 |
17 |
18 | 19 | 20 | @error('name')
{{ $message }}
@enderror 21 |
22 |
23 | 24 | 25 | @error('phone_number')
{{ $message }}
@enderror 26 |
27 |
28 | 29 | 30 | @error('address')
{{ $message }}
@enderror 31 |
32 |
33 | 34 | 35 | @error('person_in_charge')
{{ $message }}
@enderror 36 |
37 |
38 | 39 | 40 | @error('email')
{{ $message }}
@enderror 41 |
42 |
43 | 44 | Cancel 45 |
46 |
47 |
48 |
49 |
50 |
-------------------------------------------------------------------------------- /app/Notifications/TicketUpdated.php: -------------------------------------------------------------------------------- 1 | ticket = $ticket; 24 | $this->updateType = $updateType; 25 | } 26 | 27 | /** 28 | * Get the notification's delivery channels. 29 | * 30 | * @return array 31 | */ 32 | public function via(object $notifiable): array 33 | { 34 | return ['database', 'mail']; 35 | } 36 | 37 | /** 38 | * Get the mail representation of the notification. 39 | */ 40 | public function toMail(object $notifiable): MailMessage 41 | { 42 | $updateMessage = 'Ticket has been updated'; 43 | 44 | if ($this->updateType === 'status') { 45 | $updateMessage = 'Status has been updated to: ' . ($this->ticket->status->name ?? 'Unknown'); 46 | } elseif ($this->updateType === 'category') { 47 | $updateMessage = 'Category has been updated to: ' . ($this->ticket->category->name ?? 'Unknown'); 48 | } elseif ($this->updateType === 'comment') { 49 | $updateMessage = 'A new comment has been added'; 50 | } 51 | 52 | return (new MailMessage) 53 | ->subject('Ticket Updated: ' . $this->ticket->title) 54 | ->line('A ticket has been updated.') 55 | ->line('Title: ' . $this->ticket->title) 56 | ->line('Update: ' . $updateMessage) 57 | ->action('View Ticket', route('tickets.show', $this->ticket)) 58 | ->line('Please review the changes.'); 59 | } 60 | 61 | /** 62 | * Get the database representation of the notification. 63 | * 64 | * @return array 65 | */ 66 | public function toDatabase(object $notifiable): array 67 | { 68 | $updateMessage = 'Ticket has been updated'; 69 | 70 | if ($this->updateType === 'status') { 71 | $updateMessage = 'Status updated to: ' . ($this->ticket->status->name ?? 'Unknown'); 72 | } elseif ($this->updateType === 'category') { 73 | $updateMessage = 'Category updated to: ' . ($this->ticket->category->name ?? 'Unknown'); 74 | } elseif ($this->updateType === 'comment') { 75 | $updateMessage = 'New comment added'; 76 | } 77 | 78 | return [ 79 | 'ticket_id' => $this->ticket->id, 80 | 'ticket_title' => $this->ticket->title, 81 | 'update_type' => $this->updateType, 82 | 'message' => $updateMessage, 83 | ]; 84 | } 85 | 86 | /** 87 | * Get the array representation of the notification. 88 | * 89 | * @return array 90 | */ 91 | public function toArray(object $notifiable): array 92 | { 93 | return $this->toDatabase($notifiable); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | middleware(['auth', 'verified'])->name('dashboard'); 19 | 20 | Route::middleware('auth')->group(function () { 21 | Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); 22 | Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); 23 | Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); 24 | }); 25 | 26 | Route::middleware(['auth', 'verified'])->group(function () { 27 | Route::get('/tickets', [TicketController::class, 'index'])->name('tickets.index'); 28 | Route::get('/tickets/kanban', [TicketController::class, 'kanban'])->name('tickets.kanban'); 29 | Route::get('/tickets/d/mine', [TicketController::class, 'myTickets'])->name('tickets.mine'); 30 | Route::get('/tickets/d/tasks', [TicketController::class, 'myTasks'])->name('tickets.tasks'); 31 | Route::Resource('tickets', TicketController::class)->except(['update', 'destroy']); 32 | Route::patch('/tickets/{ticket}/category', [TicketController::class, 'updateCategory'])->name('tickets.updateCategory'); 33 | 34 | Route::get('/users/autocomplete', [UserController::class, 'autocomplete'])->name('users.autocomplete'); 35 | 36 | // Ticket status updates and comments 37 | Route::middleware(['can:update,ticket'])->group(function () { 38 | Route::patch('/tickets/{ticket}/status', [TicketController::class, 'updateStatus'])->name('tickets.updateStatus'); 39 | Route::post('/tickets/{ticket}/assignees', [TicketController::class, 'addAssignee'])->name('tickets.addAssignee'); 40 | Route::delete('/tickets/{ticket}/assignees/{user}', [TicketController::class, 'removeAssignee'])->name('tickets.removeAssignee'); 41 | Route::post('/tickets/{ticket}/comment', [TicketController::class, 'addComment'])->name('tickets.addComment'); 42 | }); 43 | }); 44 | 45 | Route::middleware(['auth', 'verified', 'can:admin-only'])->group(function () { 46 | Route::resource('ticket-statuses', TicketStatusController::class); 47 | Route::resource('categories', CategoryController::class); 48 | Route::resource('departments', DepartmentController::class); 49 | Route::resource('users', UserController::class); 50 | Route::resource('vendors', VendorController::class); 51 | }); 52 | 53 | Route::middleware(['auth', 'verified'])->group(function () { 54 | Route::get('/notifications', [NotificationController::class, 'index'])->name('notifications.index'); 55 | Route::post('/notifications/{notification}/mark-read', [NotificationController::class, 'markAsRead'])->name('notifications.mark-read'); 56 | Route::post('/notifications/mark-all-read', [NotificationController::class, 'markAllAsRead'])->name('notifications.mark-all-read'); 57 | }); 58 | 59 | require __DIR__.'/auth.php'; 60 | -------------------------------------------------------------------------------- /resources/views/vendors/edit.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Edit Vendor') }} 5 |

6 |
7 |
8 |
9 |
10 |
11 | @csrf 12 | @method('PUT') 13 |
14 | 15 | 16 | @error('code')
{{ $message }}
@enderror 17 |
18 |
19 | 20 | 21 | @error('name')
{{ $message }}
@enderror 22 |
23 |
24 | 25 | 26 | @error('phone_number')
{{ $message }}
@enderror 27 |
28 |
29 | 30 | 31 | @error('address')
{{ $message }}
@enderror 32 |
33 |
34 | 35 | 36 | @error('person_in_charge')
{{ $message }}
@enderror 37 |
38 |
39 | 40 | 41 | @error('email')
{{ $message }}
@enderror 42 |
43 |
44 | 45 | Cancel 46 |
47 |
48 |
49 |
50 |
51 |
-------------------------------------------------------------------------------- /resources/views/components/modal.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'name', 3 | 'show' => false, 4 | 'maxWidth' => '2xl' 5 | ]) 6 | 7 | @php 8 | $maxWidth = [ 9 | 'sm' => 'sm:max-w-sm', 10 | 'md' => 'sm:max-w-md', 11 | 'lg' => 'sm:max-w-lg', 12 | 'xl' => 'sm:max-w-xl', 13 | '2xl' => 'sm:max-w-2xl', 14 | ][$maxWidth]; 15 | @endphp 16 | 17 |
52 |
63 |
64 |
65 | 66 |
76 | {{ $slot }} 77 |
78 |
79 | -------------------------------------------------------------------------------- /resources/views/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ITH Helpdesk System 7 | 8 | @vite(['resources/css/app.css', 'resources/js/app.js']) 9 | 15 | 16 | 17 |
18 |
19 |

ITH Helpdesk System

20 |

A modern IT ticketing and support platform for your organization.

21 | @auth 22 | Go to Dashboard 23 | @else 24 | Login 25 | @endauth 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 |
35 |

Submit IT Requests

36 |

Easily create and track your IT support tickets for any issue or request.

37 |
38 |
39 |
40 | 41 |
42 |

Collaborate & Resolve

43 |

Work with IT, vendors, and users to resolve issues quickly and efficiently.

44 |
45 |
46 |
47 | 48 |
49 |

Track & Analyze

50 |

Monitor ticket status, view analytics, and improve your IT support process.

51 |
52 |
53 |
54 | © {{ date('Y') }} ITH Helpdesk System — Powered by Laravel & GitHub Copilot 55 |
56 | View on GitHub 57 |
58 |
59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_STORE', 'database'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | | Supported drivers: "array", "database", "file", "memcached", 30 | | "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'array' => [ 37 | 'driver' => 'array', 38 | 'serialize' => false, 39 | ], 40 | 41 | 'database' => [ 42 | 'driver' => 'database', 43 | 'connection' => env('DB_CACHE_CONNECTION'), 44 | 'table' => env('DB_CACHE_TABLE', 'cache'), 45 | 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), 46 | 'lock_table' => env('DB_CACHE_LOCK_TABLE'), 47 | ], 48 | 49 | 'file' => [ 50 | 'driver' => 'file', 51 | 'path' => storage_path('framework/cache/data'), 52 | 'lock_path' => storage_path('framework/cache/data'), 53 | ], 54 | 55 | 'memcached' => [ 56 | 'driver' => 'memcached', 57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 58 | 'sasl' => [ 59 | env('MEMCACHED_USERNAME'), 60 | env('MEMCACHED_PASSWORD'), 61 | ], 62 | 'options' => [ 63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 64 | ], 65 | 'servers' => [ 66 | [ 67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 68 | 'port' => env('MEMCACHED_PORT', 11211), 69 | 'weight' => 100, 70 | ], 71 | ], 72 | ], 73 | 74 | 'redis' => [ 75 | 'driver' => 'redis', 76 | 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 77 | 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), 78 | ], 79 | 80 | 'dynamodb' => [ 81 | 'driver' => 'dynamodb', 82 | 'key' => env('AWS_ACCESS_KEY_ID'), 83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 84 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 85 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 86 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 87 | ], 88 | 89 | 'octane' => [ 90 | 'driver' => 'octane', 91 | ], 92 | 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Cache Key Prefix 98 | |-------------------------------------------------------------------------- 99 | | 100 | | When utilizing the APC, database, memcached, Redis, and DynamoDB cache 101 | | stores, there might be other applications using the same cache. For 102 | | that reason, you may prefix every cache key to avoid collisions. 103 | | 104 | */ 105 | 106 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 107 | 108 | ]; 109 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'log'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Mailer Configurations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Here you may configure all of the mailers used by your application plus 25 | | their respective settings. Several examples have been configured for 26 | | you and you are free to add your own as your application requires. 27 | | 28 | | Laravel supports a variety of mail "transport" drivers that can be used 29 | | when delivering an email. You may specify which one you're using for 30 | | your mailers below. You may also add additional mailers if needed. 31 | | 32 | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", 33 | | "postmark", "resend", "log", "array", 34 | | "failover", "roundrobin" 35 | | 36 | */ 37 | 38 | 'mailers' => [ 39 | 40 | 'smtp' => [ 41 | 'transport' => 'smtp', 42 | 'scheme' => env('MAIL_SCHEME'), 43 | 'url' => env('MAIL_URL'), 44 | 'host' => env('MAIL_HOST', '127.0.0.1'), 45 | 'port' => env('MAIL_PORT', 2525), 46 | 'username' => env('MAIL_USERNAME'), 47 | 'password' => env('MAIL_PASSWORD'), 48 | 'timeout' => null, 49 | 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), 50 | ], 51 | 52 | 'ses' => [ 53 | 'transport' => 'ses', 54 | ], 55 | 56 | 'postmark' => [ 57 | 'transport' => 'postmark', 58 | // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), 59 | // 'client' => [ 60 | // 'timeout' => 5, 61 | // ], 62 | ], 63 | 64 | 'resend' => [ 65 | 'transport' => 'resend', 66 | ], 67 | 68 | 'sendmail' => [ 69 | 'transport' => 'sendmail', 70 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), 71 | ], 72 | 73 | 'log' => [ 74 | 'transport' => 'log', 75 | 'channel' => env('MAIL_LOG_CHANNEL'), 76 | ], 77 | 78 | 'array' => [ 79 | 'transport' => 'array', 80 | ], 81 | 82 | 'failover' => [ 83 | 'transport' => 'failover', 84 | 'mailers' => [ 85 | 'smtp', 86 | 'log', 87 | ], 88 | 'retry_after' => 60, 89 | ], 90 | 91 | 'roundrobin' => [ 92 | 'transport' => 'roundrobin', 93 | 'mailers' => [ 94 | 'ses', 95 | 'postmark', 96 | ], 97 | 'retry_after' => 60, 98 | ], 99 | 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Global "From" Address 105 | |-------------------------------------------------------------------------- 106 | | 107 | | You may wish for all emails sent by your application to be sent from 108 | | the same address. Here you may specify a name and address that is 109 | | used globally for all emails that are sent by your application. 110 | | 111 | */ 112 | 113 | 'from' => [ 114 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 115 | 'name' => env('MAIL_FROM_NAME', 'Example'), 116 | ], 117 | 118 | ]; 119 | -------------------------------------------------------------------------------- /resources/views/vendors/show.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Vendor Detail') }} 5 |

6 |
7 |
8 |
9 |
10 |
11 |

Vendor Information

12 |
13 |
14 |
Code:
15 |
{{ $vendor->code }}
16 |
17 |
18 |
Name:
19 |
{{ $vendor->name }}
20 |
21 |
22 |
Phone Number:
23 |
{{ $vendor->phone_number }}
24 |
25 |
26 |
Email:
27 |
{{ $vendor->email }}
28 |
29 |
30 |
Person In Charge:
31 |
{{ $vendor->person_in_charge }}
32 |
33 |
34 |
Address:
35 |
{{ $vendor->address }}
36 |
37 |
38 |
39 |
40 |

Users

41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | @forelse($vendor->users as $user) 52 | 53 | 54 | 55 | 56 | 59 | 60 | @empty 61 | 62 | 63 | 64 | @endforelse 65 | 66 |
IDNameEmailActions
{{ $user->id }}{{ $user->name }}{{ $user->email }} 57 | Edit 58 |
No users found.
67 |
68 |
69 | Back 70 | Edit 71 |
72 |
73 |
74 |
75 |
-------------------------------------------------------------------------------- /app/Http/Controllers/UserController.php: -------------------------------------------------------------------------------- 1 | where('user_type', $type); 20 | } 21 | $users = $users->orderBy('id')->paginate(10)->withQueryString(); 22 | return view('user.index', compact('users')); 23 | } 24 | 25 | public function create() 26 | { 27 | $departments = Department::all(); 28 | $vendors = Vendor::all(); 29 | return view('user.create', compact('departments', 'vendors')); 30 | } 31 | 32 | public function store(Request $request) 33 | { 34 | $data = $request->validate([ 35 | 'name' => 'required|string|max:255', 36 | 'email' => 'required|email|unique:users,email', 37 | 'password' => 'required|string|min:6|confirmed', 38 | 'user_type' => 'required|in:admin,it,user,vendor', 39 | 'department_id'=> 'nullable|exists:departments,id', 40 | 'vendor_id' => 'nullable|exists:vendors,id', 41 | ]); 42 | $data['password'] = bcrypt($data['password']); 43 | if ($data['user_type'] !== 'user') { 44 | $data['department_id'] = null; 45 | } 46 | if ($data['user_type'] !== 'vendor') { 47 | $data['vendor_id'] = null; 48 | } 49 | User::create($data); 50 | return redirect()->route('users.index')->with('success', 'User created.'); 51 | } 52 | 53 | public function edit(User $user) 54 | { 55 | $departments = Department::all(); 56 | $vendors = Vendor::all(); 57 | return view('user.edit', compact('user', 'departments', 'vendors')); 58 | } 59 | 60 | public function update(Request $request, User $user) 61 | { 62 | $data = $request->validate([ 63 | 'name' => 'required|string|max:255', 64 | 'email' => 'required|email|unique:users,email,' . $user->id, 65 | 'password' => 'nullable|string|min:6|confirmed', 66 | 'user_type' => 'required|in:admin,it,user,vendor', 67 | 'department_id'=> 'nullable|exists:departments,id', 68 | 'vendor_id' => 'nullable|exists:vendors,id', 69 | ]); 70 | if ($data['password']) { 71 | $data['password'] = bcrypt($data['password']); 72 | } else { 73 | unset($data['password']); 74 | } 75 | if ($data['user_type'] !== 'user') { 76 | $data['department_id'] = null; 77 | } 78 | if ($data['user_type'] !== 'vendor') { 79 | $data['vendor_id'] = null; 80 | } 81 | $user->update($data); 82 | return redirect()->route('users.index')->with('success', 'User updated.'); 83 | } 84 | 85 | public function destroy(User $user) 86 | { 87 | $user->delete(); 88 | return redirect()->route('users.index')->with('success', 'User deleted.'); 89 | } 90 | 91 | public function autocomplete(Request $request) 92 | { 93 | $query = $request->get('q', ''); 94 | $exclude = $request->get('exclude', []); 95 | $userTypes = $request->get('user_types', []); 96 | $users = User::where('name', 'like', "%{$query}%") 97 | ->when($exclude, function ($q) use ($exclude) { 98 | $q->whereNotIn('id', (array)$exclude); 99 | }) 100 | ->when($userTypes, function ($q) use ($userTypes) { 101 | $q->whereIn('user_type', $userTypes); 102 | }) 103 | ->with(['department:id,name', 'vendor:id,name']) 104 | ->limit(10) 105 | ->get(['id', 'name', 'email', 'user_type', 'department_id', 'vendor_id']); 106 | return response()->json($users); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /resources/views/vendors/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

5 | {{ __('Vendors') }} 6 |

7 | 8 | + Add Vendor 9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 | @if($vendors->hasPages()) 17 | 20 | @endif 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | @forelse($vendors as $vendor) 34 | 35 | 36 | 37 | 38 | 51 | 60 | 61 | @empty 62 | 63 | 64 | 65 | @endforelse 66 | 67 |
IDCodeNameContactActions
{{ $vendor->id }}{{ $vendor->code }}{{ $vendor->name }} 39 |
40 | @if($vendor->person_in_charge) 41 |
{{ $vendor->person_in_charge }}
42 | @endif 43 | @if($vendor->email) 44 |
{{ $vendor->email }}
45 | @endif 46 | @if($vendor->phone_number) 47 |
{{ $vendor->phone_number }}
48 | @endif 49 |
50 |
52 | View 53 | Edit 54 |
55 | @csrf 56 | @method('DELETE') 57 | 58 |
59 |
No vendors found.
68 |
69 |
70 |
71 |
-------------------------------------------------------------------------------- /resources/views/ticket_status/index.blade.php: -------------------------------------------------------------------------------- 1 | {{-- filepath: c:\laragon\www\ith\resources\views\ticket_status\index.blade.php --}} 2 | 3 | 4 |
5 |

6 | {{ __('Ticket Statuses') }} 7 |

8 | 9 | + Add Status 10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | @if($statuses->hasPages()) 19 | 22 | @endif 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | @forelse($statuses as $status) 36 | 37 | 38 | 39 | 46 | 51 | 60 | 61 | @empty 62 | 63 | 64 | 65 | @endforelse 66 | 67 |
IDNameColorDefaultActions
{{ $status->id }}{{ $status->name }} 40 | @if($status->color) 41 | 42 | {{ $status->color }} 43 | 44 | @endif 45 | 47 | @if($status->default_status) 48 | Default 49 | @endif 50 | 52 | View 53 | Edit 54 |
55 | @csrf 56 | @method('DELETE') 57 | 58 |
59 |
No statuses found.
68 |
69 |
70 |
71 |
-------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'database'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection options for every queue backend 24 | | used by your application. An example configuration is provided for 25 | | each backend supported by Laravel. You're also free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'connection' => env('DB_QUEUE_CONNECTION'), 40 | 'table' => env('DB_QUEUE_TABLE', 'jobs'), 41 | 'queue' => env('DB_QUEUE', 'default'), 42 | 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 43 | 'after_commit' => false, 44 | ], 45 | 46 | 'beanstalkd' => [ 47 | 'driver' => 'beanstalkd', 48 | 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), 49 | 'queue' => env('BEANSTALKD_QUEUE', 'default'), 50 | 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), 51 | 'block_for' => 0, 52 | 'after_commit' => false, 53 | ], 54 | 55 | 'sqs' => [ 56 | 'driver' => 'sqs', 57 | 'key' => env('AWS_ACCESS_KEY_ID'), 58 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 59 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 60 | 'queue' => env('SQS_QUEUE', 'default'), 61 | 'suffix' => env('SQS_SUFFIX'), 62 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 63 | 'after_commit' => false, 64 | ], 65 | 66 | 'redis' => [ 67 | 'driver' => 'redis', 68 | 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 69 | 'queue' => env('REDIS_QUEUE', 'default'), 70 | 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), 71 | 'block_for' => null, 72 | 'after_commit' => false, 73 | ], 74 | 75 | ], 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Job Batching 80 | |-------------------------------------------------------------------------- 81 | | 82 | | The following options configure the database and table that store job 83 | | batching information. These options can be updated to any database 84 | | connection and table which has been defined by your application. 85 | | 86 | */ 87 | 88 | 'batching' => [ 89 | 'database' => env('DB_CONNECTION', 'sqlite'), 90 | 'table' => 'job_batches', 91 | ], 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Failed Queue Jobs 96 | |-------------------------------------------------------------------------- 97 | | 98 | | These options configure the behavior of failed queue job logging so you 99 | | can control how and where failed jobs are stored. Laravel ships with 100 | | support for storing failed jobs in a simple file or in a database. 101 | | 102 | | Supported drivers: "database-uuids", "dynamodb", "file", "null" 103 | | 104 | */ 105 | 106 | 'failed' => [ 107 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 108 | 'database' => env('DB_CONNECTION', 'sqlite'), 109 | 'table' => 'failed_jobs', 110 | ], 111 | 112 | ]; 113 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => env('AUTH_GUARD', 'web'), 18 | 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | which utilizes session storage plus the Eloquent user provider. 29 | | 30 | | All authentication guards have a user provider, which defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | system used by the application. Typically, Eloquent is utilized. 33 | | 34 | | Supported: "session" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | User Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | All authentication guards have a user provider, which defines how the 51 | | users are actually retrieved out of your database or other storage 52 | | system used by the application. Typically, Eloquent is utilized. 53 | | 54 | | If you have multiple user tables or models you may configure multiple 55 | | providers to represent the model / table. These providers may then 56 | | be assigned to any extra authentication guards you have defined. 57 | | 58 | | Supported: "database", "eloquent" 59 | | 60 | */ 61 | 62 | 'providers' => [ 63 | 'users' => [ 64 | 'driver' => 'eloquent', 65 | 'model' => env('AUTH_MODEL', App\Models\User::class), 66 | ], 67 | 68 | // 'users' => [ 69 | // 'driver' => 'database', 70 | // 'table' => 'users', 71 | // ], 72 | ], 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Resetting Passwords 77 | |-------------------------------------------------------------------------- 78 | | 79 | | These configuration options specify the behavior of Laravel's password 80 | | reset functionality, including the table utilized for token storage 81 | | and the user provider that is invoked to actually retrieve users. 82 | | 83 | | The expiry time is the number of minutes that each reset token will be 84 | | considered valid. This security feature keeps tokens short-lived so 85 | | they have less time to be guessed. You may change this as needed. 86 | | 87 | | The throttle setting is the number of seconds a user must wait before 88 | | generating more password reset tokens. This prevents the user from 89 | | quickly generating a very large amount of password reset tokens. 90 | | 91 | */ 92 | 93 | 'passwords' => [ 94 | 'users' => [ 95 | 'provider' => 'users', 96 | 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 97 | 'expire' => 60, 98 | 'throttle' => 60, 99 | ], 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Password Confirmation Timeout 105 | |-------------------------------------------------------------------------- 106 | | 107 | | Here you may define the number of seconds before a password confirmation 108 | | window expires and users are asked to re-enter their password via the 109 | | confirmation screen. By default, the timeout lasts for three hours. 110 | | 111 | */ 112 | 113 | 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), 114 | 115 | ]; 116 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'Laravel'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application Environment 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "environment" your application is currently 24 | | running in. This may determine how you prefer to configure various 25 | | services the application utilizes. Set this in your ".env" file. 26 | | 27 | */ 28 | 29 | 'env' => env('APP_ENV', 'production'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Debug Mode 34 | |-------------------------------------------------------------------------- 35 | | 36 | | When your application is in debug mode, detailed error messages with 37 | | stack traces will be shown on every error that occurs within your 38 | | application. If disabled, a simple generic error page is shown. 39 | | 40 | */ 41 | 42 | 'debug' => (bool) env('APP_DEBUG', false), 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Application URL 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This URL is used by the console to properly generate URLs when using 50 | | the Artisan command line tool. You should set this to the root of 51 | | the application so that it's available within Artisan commands. 52 | | 53 | */ 54 | 55 | 'url' => env('APP_URL', 'http://localhost'), 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Application Timezone 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Here you may specify the default timezone for your application, which 63 | | will be used by the PHP date and date-time functions. The timezone 64 | | is set to "UTC" by default as it is suitable for most use cases. 65 | | 66 | */ 67 | 68 | 'timezone' => 'UTC', 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Application Locale Configuration 73 | |-------------------------------------------------------------------------- 74 | | 75 | | The application locale determines the default locale that will be used 76 | | by Laravel's translation / localization methods. This option can be 77 | | set to any locale for which you plan to have translation strings. 78 | | 79 | */ 80 | 81 | 'locale' => env('APP_LOCALE', 'en'), 82 | 83 | 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), 84 | 85 | 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), 86 | 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | Encryption Key 90 | |-------------------------------------------------------------------------- 91 | | 92 | | This key is utilized by Laravel's encryption services and should be set 93 | | to a random, 32 character string to ensure that all encrypted values 94 | | are secure. You should do this prior to deploying the application. 95 | | 96 | */ 97 | 98 | 'cipher' => 'AES-256-CBC', 99 | 100 | 'key' => env('APP_KEY'), 101 | 102 | 'previous_keys' => [ 103 | ...array_filter( 104 | explode(',', env('APP_PREVIOUS_KEYS', '')) 105 | ), 106 | ], 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Maintenance Mode Driver 111 | |-------------------------------------------------------------------------- 112 | | 113 | | These configuration options determine the driver used to determine and 114 | | manage Laravel's "maintenance mode" status. The "cache" driver will 115 | | allow maintenance mode to be controlled across multiple machines. 116 | | 117 | | Supported drivers: "file", "cache" 118 | | 119 | */ 120 | 121 | 'maintenance' => [ 122 | 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), 123 | 'store' => env('APP_MAINTENANCE_STORE', 'database'), 124 | ], 125 | 126 | ]; 127 | --------------------------------------------------------------------------------