diff --git a/app/Filament/Resources/VisitorEntryResource.php b/app/Filament/Resources/VisitorEntryResource.php new file mode 100644 index 0000000..dcf4c71 --- /dev/null +++ b/app/Filament/Resources/VisitorEntryResource.php @@ -0,0 +1,267 @@ +schema([ + Forms\Components\TextInput::make('mobile_number') + ->label('Mobile Number') + ->length(10) + ->reactive() + ->extraInputAttributes([ + 'oninput' => 'this.value = this.value.replace(/[^0-9]/g, "").slice(0, 10)', // blocks non-numbers + limits to 10 chars + 'maxlength' => 10, + ]) + ->required() + ->extraAttributes([ + 'id' => 'mobile_number_input', + 'x-data' => '{ value: "" }', + 'x-model' => 'value', + 'wire:keydown.enter.prevent' => 'processMobile(value)', + ]), + Forms\Components\TextInput::make('name') + ->label('Name') + ->required() + ->reactive() + ->extraInputAttributes([ + 'oninput' => 'this.value = this.value.replace(/[^a-zA-Z\s]/g, "")', + ]), + Forms\Components\Select::make('type') + ->label('Type') + ->reactive() + ->options([ + 'Student' => 'Student', + 'Consultant' => 'Consultant', + 'Vendor' => 'Vendor', + 'Other' => 'Other', + ]) + ->required() + ->dehydrateStateUsing(function ($state, callable $get) { + return $state == 'Other' + ? $get('other_type') + : $state; + }), + Forms\Components\TextInput::make('other_type') + ->label('Specify Type') + ->reactive() + ->visible(fn (callable $get) => $get('type') == 'Other') + ->required(fn (callable $get) => $get('type') == 'Other') + ->dehydrated(false), + Forms\Components\TextInput::make('company') + ->label('Company') + ->required(), + Forms\Components\Select::make('department') + ->label('Employee Department') + ->options( + \App\Models\EmployeeMaster::distinct() + ->pluck('department', 'department') + ) + ->required() + ->reactive() + ->afterStateUpdated(function (callable $set) { + $set('employee_master_id', null); + $set('code', null); + }), + // Forms\Components\Select::make('employee_master_id') + // ->label('Recipient Employee') + // ->required() + // ->options(function (callable $get) { + // $department = $get('department'); + + // if (!$department) { + // return []; + // } + + // return \App\Models\EmployeeMaster::where('department', $department) + // ->pluck('name', 'id'); + // }) + // ->reactive() + // ->afterStateUpdated(function (callable $set, callable $get, ?string $state) { + // $department = $get('department'); + + // $employee = \App\Models\EmployeeMaster::where('id', $state) + // ->where('department', $department) + // ->first(); + + // $set('code', $employee ? $employee->code : ''); + // }), + + Forms\Components\Select::make('employee_master_id') + ->label('Recipient Employee') + ->required() + ->options(function (callable $get) { + $department = $get('department'); + // Always load ALL employees, filter by department if set + if ($department) { + return \App\Models\EmployeeMaster::where('department', $department) + ->pluck('name', 'id'); + } + // Fallback: load all so fill() can always match the ID + return \App\Models\EmployeeMaster::pluck('name', 'id'); + }) + ->reactive() + ->afterStateUpdated(function (callable $set, ?string $state) { + $employee = \App\Models\EmployeeMaster::find($state); + $set('code', $employee?->code ?? ''); + }), + + Forms\Components\TextInput::make('code') + ->label('Employee Code') + ->readOnly(), + Forms\Components\Textarea::make('purpose_of_visit') + ->label('Purpose of Visit') + ->required(), + Forms\Components\TextInput::make('number_of_person') + ->numeric() + ->default(1) + ->required(), + Forms\Components\DateTimePicker::make('in_time') + ->label('In Time'), + Forms\Components\DateTimePicker::make('out_time') + ->label('Out Time'), + Forms\Components\View::make('components.webcam-field') + ->columnSpanFull(), + Forms\Components\Hidden::make('photo'), + Forms\Components\Hidden::make('created_by') + ->label('created_by') + ->default(Filament::auth()->user()?->name), + Forms\Components\Hidden::make('updated_by') + ->label('updated_by') + ->default(Filament::auth()->user()?->name), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('No.') + ->label('NO') + ->alignCenter() + ->getStateUsing(function ($record, $livewire, $column, $rowLoop) { + $paginator = $livewire->getTableRecords(); + $perPage = method_exists($paginator, 'perPage') ? $paginator->perPage() : 10; + $currentPage = method_exists($paginator, 'currentPage') ? $paginator->currentPage() : 1; + + return ($currentPage - 1) * $perPage + $rowLoop->iteration; + }), + Tables\Columns\TextColumn::make('type') + ->label('Visitor Type') + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('name') + ->label('Visitor Name') + ->sortable() + ->alignCenter() + ->searchable(), + Tables\Columns\TextColumn::make('mobile_number') + ->label('Visitor Mobile Number') + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('employeeMaster.name') + ->label('Recipient Name') + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('employeeMaster.code') + ->label('Receipient ID') + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('employeeMaster.department') + ->label('Receipient Department') + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('number_of_person') + ->label('Number of Person') + ->numeric() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('in_time') + ->label('In Time') + ->dateTime() + ->sortable() + ->alignCenter(), + Tables\Columns\TextColumn::make('out_time') + ->label('Out Time') + ->dateTime() + ->sortable() + ->alignCenter(), + Tables\Columns\TextColumn::make('created_at') + ->label('Created At') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true) + ->alignCenter(), + Tables\Columns\TextColumn::make('updated_at') + ->label('Updated At') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true) + ->alignCenter(), + Tables\Columns\TextColumn::make('deleted_at') + ->label('Deleted At') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true) + ->alignCenter(), + ]) + ->filters([ + Tables\Filters\TrashedFilter::make(), + ]) + ->actions([ + Tables\Actions\ViewAction::make(), + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\ForceDeleteBulkAction::make(), + Tables\Actions\RestoreBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListVisitorEntries::route('/'), + 'create' => Pages\CreateVisitorEntry::route('/create'), + 'view' => Pages\ViewVisitorEntry::route('/{record}'), + 'edit' => Pages\EditVisitorEntry::route('/{record}/edit'), + ]; + } + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Resources/VisitorEntryResource/Pages/CreateVisitorEntry.php b/app/Filament/Resources/VisitorEntryResource/Pages/CreateVisitorEntry.php new file mode 100644 index 0000000..1413c41 --- /dev/null +++ b/app/Filament/Resources/VisitorEntryResource/Pages/CreateVisitorEntry.php @@ -0,0 +1,78 @@ +data['photo'] = $photo; + } + + public function processMobile($mobile) + { + $visitor = VisitorEntry::where('mobile_number', $mobile)->latest()->first(); + + if ($visitor) { + + $employee = EmployeeMaster::where('id', $visitor->employee_master_id)->first(); + + $this->form->fill([ + 'mobile_number' => $mobile ?? '', + 'name' => $visitor->name ?? '', + 'company' => $visitor->company ?? '', + 'type' => $visitor->type ?? '', + 'department' => $employee->department ?? '', + 'employee_master_id' => $visitor->employee_master_id->name ?? '', + 'code' => $employee->code ?? '', + ]); + } + else { + + $this->form->fill([ + 'mobile_number' => $mobile ?? '', + 'name' => $visitor->name ?? '', + 'company' => $visitor->company ?? '', + 'type' => $visitor->type ?? '', + 'department' => $employee->department ?? '', + 'employee_master_id' => $visitor->employee_master_id->name ?? '', + 'code' => $employee->code ?? '', + ]); + } + } + + protected function mutateFormDataBeforeCreate(array $data): array + { + if ( + !empty($data['photo']) && + str_starts_with($data['photo'], 'data:image') + ) { + // Step A: Strip the "data:image/jpeg;base64," prefix + $imageData = explode(',', $data['photo'])[1]; + + // Step B: Generate a unique filename + $filename = 'visitor_' . time() . '_' . uniqid() . '.jpg'; + + // Step C: Decode Base64 and save as a real .jpg file + $path = 'visitor-photos/' . $filename; + Storage::disk('public')->put($path, base64_decode($imageData)); + + // Step D: Replace the Base64 string with just the file path + $data['photo'] = $path; + } + + return $data; + } +} diff --git a/app/Filament/Resources/VisitorEntryResource/Pages/EditVisitorEntry.php b/app/Filament/Resources/VisitorEntryResource/Pages/EditVisitorEntry.php new file mode 100644 index 0000000..5126830 --- /dev/null +++ b/app/Filament/Resources/VisitorEntryResource/Pages/EditVisitorEntry.php @@ -0,0 +1,54 @@ +data['photo'] = $photo; + } + + protected function mutateFormDataBeforeSave(array $data): array + { + if ( + !empty($data['photo']) && + str_starts_with($data['photo'], 'data:image') + ) { + // Delete the old photo file if one exists + $oldPhoto = $this->record->photo; + if ($oldPhoto && Storage::disk('public')->exists($oldPhoto)) { + Storage::disk('public')->delete($oldPhoto); + } + + // Save the new photo + $imageData = explode(',', $data['photo'])[1]; + $filename = 'visitor_' . time() . '_' . uniqid() . '.jpg'; + $path = 'visitor-photos/' . $filename; + Storage::disk('public')->put($path, base64_decode($imageData)); + + $data['photo'] = $path; + } + + return $data; + } +} diff --git a/app/Filament/Resources/VisitorEntryResource/Pages/ListVisitorEntries.php b/app/Filament/Resources/VisitorEntryResource/Pages/ListVisitorEntries.php new file mode 100644 index 0000000..05e6266 --- /dev/null +++ b/app/Filament/Resources/VisitorEntryResource/Pages/ListVisitorEntries.php @@ -0,0 +1,19 @@ +capturedPhoto = $photo; + + // Fires a browser event that the Filament form will listen to + $this->dispatch('photo-captured', photo: $photo); + } + + // Called from JavaScript when user clicks "Retake" + public function clearPhoto(): void + { + $this->capturedPhoto = ''; + + $this->dispatch('photo-captured', photo: ''); + } + public function render() + { + return view('livewire.webcam'); + } +} diff --git a/app/Models/VisitorEntry.php b/app/Models/VisitorEntry.php new file mode 100644 index 0000000..706e787 --- /dev/null +++ b/app/Models/VisitorEntry.php @@ -0,0 +1,30 @@ +belongsTo(EmployeeMaster::class); + } +} diff --git a/database/migrations/2026_05_25_110842_create_visitor_entries_table.php b/database/migrations/2026_05_25_110842_create_visitor_entries_table.php new file mode 100644 index 0000000..2a0963d --- /dev/null +++ b/database/migrations/2026_05_25_110842_create_visitor_entries_table.php @@ -0,0 +1,47 @@ + + + + Visitor Photo + + + + + + diff --git a/resources/views/livewire/webcam.blade.php b/resources/views/livewire/webcam.blade.php new file mode 100644 index 0000000..cd71ebf --- /dev/null +++ b/resources/views/livewire/webcam.blade.php @@ -0,0 +1,189 @@ +
+ {{-- ── Error message ── --}} + + + {{-- ── Live video feed (shown while camera is active) ── --}} +
+ +
+ + {{-- ── Captured photo preview (shown after capture) ── --}} +
+ Captured visitor photo +
✓ Photo captured
+
+ + {{-- ── Placeholder (before camera starts) ── --}} +
+ 📷 Camera not started yet +
+ + {{-- ── Hidden canvas used for capturing the frame ── --}} + + + {{-- ── Buttons ── --}} +
+ + {{-- Start Camera button --}} + + + {{-- Capture button --}} + + + {{-- Retake button --}} + + +
+