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 @@
+
+