diff --git a/app/Filament/Resources/ProductionOrderResource.php b/app/Filament/Resources/ProductionOrderResource.php new file mode 100644 index 0000000..84014c0 --- /dev/null +++ b/app/Filament/Resources/ProductionOrderResource.php @@ -0,0 +1,327 @@ +schema([ + Section::make('') + ->schema([ + Forms\Components\Select::make('plant_id') + ->label('Plant Name') + ->relationship('plant', 'name') + ->searchable() + ->options(function (callable $get) { + $userHas = Filament::auth()->user()->plant_id; + + return ($userHas && strlen($userHas) > 0) ? Plant::where('id', $userHas)->pluck('name', 'id')->toArray() : Plant::orderBy('code')->pluck('name', 'id')->toArray(); + }) + ->disabled(fn (Get $get) => ! empty($get('id'))) + ->default(function () { + $userHas = Filament::auth()->user()->plant_id; + + return ($userHas && strlen($userHas) > 0) ? $userHas : optional(ProductionOrder::latest()->first())->plant_id; + }) + ->reactive() + ->extraAttributes(fn ($get) => [ + 'class' => $get('poPlantError') ? 'border-red-500' : '', + ]) + ->hint(fn ($get) => $get('poPlantError') ? $get('poPlantError') : null) + ->hintColor('danger') + ->required() + ->afterStateUpdated(function ($state, callable $set) { + $set('item_id', null); + $set('quantity', null); + $set('show_extra_fields', false); + $set('start_date', null); + $set('end_date', null); + $set('production_order', null); + $set('from_serial_number', null); + $set('to_serial_number', null); + }), + Forms\Components\Select::make('item_id') + ->label('Item Code') + ->searchable() + ->reactive() + ->options(function (callable $get) { + $plantId = $get('plant_id'); + if (empty($plantId)) { + return []; + } + + return Item::where('plant_id', $plantId)->pluck('code', 'id'); + }) + ->disabled(fn (Get $get) => ! empty($get('id'))) + ->default(function () { + $userHas = Filament::auth()->user()->plant_id; + + return ($userHas && strlen($userHas) > 0) ? optional(ProductionOrder::where('plant_id', $userHas)->latest()->first())->plant_id : optional(ProductionOrder::latest()->first())->plant_id; + }) + ->required() + ->afterStateUpdated(function ($state, callable $set) { + $set('quantity', null); + $set('show_extra_fields', false); + $set('start_date', null); + $set('end_date', null); + $set('production_order', null); + $set('from_serial_number', null); + $set('to_serial_number', null); + }), + Forms\Components\TextInput::make('quantity') + ->label('Quantity') + ->reactive() + ->integer() + ->required() + ->readOnly(fn ($get) => ($get('plant_id') == null || $get('item_id') == null)) + ->afterStateUpdated(function ($state, callable $set, $get) { + if (! empty($state) && $state > 0) { + $set('show_extra_fields', true); + $now = Carbon::now(); + $plantId = $get('plant_id'); + $quantity = $get('quantity'); + $set('start_date', null); + $set('end_date', null); + $plantCode = Plant::find($plantId)->code; + + $year = $now->format('y'); // Year (last 2 digits) + $month = $now->format('m'); + + $monthText = strtoupper($now->format('M')); // APR + $monthNumber = ''; + + foreach (str_split($monthText) as $char) { + $curSeq = ord($char) - 64; + $monthNumber .= str_pad($curSeq, 2, '0', STR_PAD_LEFT); + } + + $prefix = $year.$monthNumber; + + $last = ProductionOrder::where('production_order', 'like', "{$prefix}%")->orderByDesc('production_order')->first(); // ProductionOrder::where('production_order', 'like', $prefix.'%')->orderBy('production_order', 'desc')->first(); + + if ($last) { + $lastSeq = substr($last->production_order, -4); + $nextSeq = str_pad(((int) $lastSeq + 1), 4, '0', STR_PAD_LEFT); + } else { + $nextSeq = '0001'; + } + + $productionOrder = $prefix.$nextSeq; + + $set('production_order', $productionOrder); + + $prefixSerial = $plantCode.$year.$month; + + $last = ProductionOrder::where('to_serial_number', 'like', "{$prefixSerial}%")->orderByDesc('to_serial_number')->first(); + + if ($last) { + $lastSeq = (int) substr($last->to_serial_number, -6); + $startSeq = $lastSeq + 1; + } else { + $startSeq = 1; + } + + $endSeq = $startSeq + $quantity - 1; + + $fromSerial = $prefixSerial.str_pad($startSeq, 6, '0', STR_PAD_LEFT); + $toSerial = $prefixSerial.str_pad($endSeq, 6, '0', STR_PAD_LEFT); + + $set('from_serial_number', $fromSerial); + $set('to_serial_number', $toSerial); + + } else { + $set('show_extra_fields', false); + + $set('production_order', null); + $set('start_date', null); + $set('end_date', null); + $set('from_serial_number', null); + $set('to_serial_number', null); + } + }), + Forms\Components\Hidden::make('show_extra_fields') + ->default(false), + Forms\Components\DateTimePicker::make('start_date') + ->label('Start Date'), + Forms\Components\DateTimePicker::make('end_date') + ->label('End Date'), + Forms\Components\TextInput::make('production_order') + ->label('Production Order') + ->readOnly() + ->visible(fn ($get) => $get('show_extra_fields')), + Forms\Components\TextInput::make('from_serial_number') + ->label('From Serial Number') + ->readOnly() + ->visible(fn ($get) => $get('show_extra_fields')), + Forms\Components\TextInput::make('to_serial_number') + ->label('To Serial Number') + ->readOnly() + ->visible(fn ($get) => $get('show_extra_fields')), + 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), + Forms\Components\View::make('forms.components.save-production-order-button'), + ]) + ->columns(5), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('No.') + ->label('No.') + ->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('plant.name') + ->label('Plant Name') + ->alignCenter() + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('item.code') + ->label('Item Code') + ->searchable() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('quantity') + ->label('Quantity') + ->searchable() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('start_date') + ->label('Start Date') + ->searchable() + ->alignCenter() + ->dateTime() + ->sortable(), + Tables\Columns\TextColumn::make('end_date') + ->label('End Date') + ->searchable() + ->alignCenter() + ->dateTime() + ->sortable(), + Tables\Columns\TextColumn::make('production_order') + ->label('Production Order') + ->searchable() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('from_serial_number') + ->label('From Serial Number') + ->searchable() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('to_serial_number') + ->label('To Serial Number') + ->searchable() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('created_at') + ->label('Created At') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_at') + ->label('Updated At') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('deleted_at') + ->label('Deleted At') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->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(), + ]), + ]) + ->headerActions([ + ImportAction::make() + ->label('Import Production Orders') + ->color('warning') + ->importer(ProductionOrderImporter::class) + ->visible(function () { + return Filament::auth()->user()->can('view import production orders'); + }), + ExportAction::make() + ->label('Export Production Orders') + ->color('warning') + ->exporter(ProductionOrderExporter::class) + ->visible(function () { + return Filament::auth()->user()->can('view export production orders'); + }), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListProductionOrders::route('/'), + 'create' => Pages\CreateProductionOrder::route('/create'), + 'view' => Pages\ViewProductionOrder::route('/{record}'), + 'edit' => Pages\EditProductionOrder::route('/{record}/edit'), + ]; + } + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Resources/ProductionOrderResource/Pages/CreateProductionOrder.php b/app/Filament/Resources/ProductionOrderResource/Pages/CreateProductionOrder.php new file mode 100644 index 0000000..5c3ce77 --- /dev/null +++ b/app/Filament/Resources/ProductionOrderResource/Pages/CreateProductionOrder.php @@ -0,0 +1,186 @@ +form->getState()['plant_id'] ?? '') ?? null; + + $itemId = trim($this->form->getState()['item_id'] ?? '') ?? null; + + $quantity = trim($this->form->getState()['quantity'] ?? '') ?? null; + + $startDate = trim($this->form->getState()['start_date'] ?? '') ?? null; + + $endDate = trim($this->form->getState()['end_date'] ?? '') ?? null; + + $pOrder = trim($this->form->getState()['production_order'] ?? '') ?? null; + + $fSerNo = trim($this->form->getState()['from_serial_number'] ?? '') ?? null; + + $tSerNo = trim($this->form->getState()['to_serial_number'] ?? '') ?? null; + + $operatorName = Filament::auth()->user()?->name; + + if (empty($plantId)) { + Notification::make() + ->title('Plant name cannot be empty!') + ->danger() + ->send(); + + return; + } elseif (empty($itemId)) { + Notification::make() + ->title('Item code cannot be empty!') + ->danger() + ->send(); + + return; + } elseif (empty($quantity)) { + Notification::make() + ->title('Quantity cannot be empty!') + ->danger() + ->send(); + + return; + } elseif (empty($startDate)) { + Notification::make() + ->title('Please choose a Start Date!') + ->danger() + ->send(); + + return; + } elseif (empty($endDate)) { + Notification::make() + ->title('Please choose a End Date!') + ->danger() + ->send(); + + return; + } elseif (empty($pOrder)) { + Notification::make() + ->title('Production order cannot be empty!') + ->danger() + ->send(); + + return; + } elseif (empty($fSerNo)) { + Notification::make() + ->title('From serial number cannot be empty!') + ->danger() + ->send(); + + return; + } elseif (empty($tSerNo)) { + Notification::make() + ->title('To serial number cannot be empty!') + ->danger() + ->send(); + + return; + } elseif (empty($operatorName)) { + Notification::make() + ->title('Operator Name cannot be empty!') + ->danger() + ->send(); + + return; + } + + $dupProd = ProductionOrder::where('plant_id', $plantId)->where('production_order', $pOrder)->first(); + if ($dupProd) { + Notification::make() + ->title("Production Order '{$pOrder}' already exists in database!") + ->danger() + ->send(); + + return; + } + + $insert = ProductionOrder::create([ + 'plant_id' => $plantId ?? null, + 'item_id' => $itemId ?? null, + 'quantity' => $quantity ?? null, + 'start_date' => $startDate ?? null, + 'end_date' => $endDate ?? null, + 'production_order' => $pOrder ?? null, + 'from_serial_number' => $fSerNo ?? null, + 'to_serial_number' => $tSerNo ?? null, + 'created_by' => $operatorName ?? null, + ]); + + if ($insert) { + Notification::make() + ->title("Production Order '{$pOrder}' saved successfully.") + ->success() + ->send(); + + $this->form->fill([ + 'plant_id' => $plantId, + 'item_id' => $itemId, + 'quantity' => $quantity, + 'start_date' => $startDate, + 'end_date' => $endDate, + 'production_order' => $pOrder, + 'from_serial_number' => $fSerNo, + 'to_serial_number' => $tSerNo, + 'show_extra_fields' => true, + ]); + + return; + + } else { + Notification::make() + ->title("Failed to save Production Order '{$pOrder}'!") + ->danger() + ->send(); + $this->form->fill([ + 'plant_id' => $plantId, + 'item_id' => $itemId, + 'quantity' => null, + 'start_date' => null, + 'end_date' => null, + 'production_order' => null, + 'from_serial_number' => null, + 'to_serial_number' => null, + ]); + + return; + } + } + + public function printProductionOrder() + { + $pOrder = $this->form->getState()['production_order']; + + $pOrder = trim($pOrder) ?? null; + + $pOrderExists = ProductionOrder::where('production_order', $pOrder)->first(); + + if (! $pOrderExists) { + Notification::make() + ->title("Production Order '{$pOrder}' does not exist to get print") + ->danger() + ->send(); + + return; + } else { + return redirect()->route('production-orders.print', ['production_order' => $pOrder]); + } + } + + protected function getFormActions(): array + { + return []; + } +} diff --git a/app/Filament/Resources/ProductionOrderResource/Pages/EditProductionOrder.php b/app/Filament/Resources/ProductionOrderResource/Pages/EditProductionOrder.php new file mode 100644 index 0000000..7d0a85f --- /dev/null +++ b/app/Filament/Resources/ProductionOrderResource/Pages/EditProductionOrder.php @@ -0,0 +1,22 @@ +first(); + + if (!$order) { + abort(404, 'Production Order not found'); + } + else{ + $fromSerial = (int) $order->from_serial_number; + $toSerial = (int) $order->to_serial_number; + $itemCode = $order->item->code ?? ''; + $itemDes = $order->item->description ?? ''; + + $stickers = []; + + for ($i = $fromSerial; $i <= $toSerial; $i++) + { + + $serial = str_pad($i, 6, '0', STR_PAD_LEFT); + + $qrData = $itemCode . '|' . $serial; + + $qrBase64 = base64_encode( + QrCode::format('png')->size(100)->generate($qrData) + ); + + $stickers[] = [ + 'serial' => $serial, + 'qr' => 'data:image/png;base64,' . $qrBase64, + 'production_order' => $production_order, + 'description' => $itemDes ?? '' + ]; + } + + $pdf = Pdf::loadView('production-orders.print', compact('stickers')) + ->setPaper([0, 0, 170, 40]); + + return $pdf->stream('stickers.pdf'); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + // + } + + /** + * Display the specified resource. + */ + public function show(string $id) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, string $id) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(string $id) + { + // + } +} diff --git a/app/Models/ProductionOrder.php b/app/Models/ProductionOrder.php new file mode 100644 index 0000000..8a8518b --- /dev/null +++ b/app/Models/ProductionOrder.php @@ -0,0 +1,31 @@ +belongsTo(Plant::class); + } + + public function item() + { + return $this->belongsTo(Item::class); + } +} diff --git a/app/Policies/ProductionOrderPolicy.php b/app/Policies/ProductionOrderPolicy.php new file mode 100644 index 0000000..1729460 --- /dev/null +++ b/app/Policies/ProductionOrderPolicy.php @@ -0,0 +1,106 @@ +checkPermissionTo('view-any ProductionOrder'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, ProductionOrder $productionorder): bool + { + return $user->checkPermissionTo('view ProductionOrder'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->checkPermissionTo('create ProductionOrder'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, ProductionOrder $productionorder): bool + { + return $user->checkPermissionTo('update ProductionOrder'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, ProductionOrder $productionorder): bool + { + return $user->checkPermissionTo('delete ProductionOrder'); + } + + /** + * Determine whether the user can delete any models. + */ + public function deleteAny(User $user): bool + { + return $user->checkPermissionTo('delete-any ProductionOrder'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, ProductionOrder $productionorder): bool + { + return $user->checkPermissionTo('restore ProductionOrder'); + } + + /** + * Determine whether the user can restore any models. + */ + public function restoreAny(User $user): bool + { + return $user->checkPermissionTo('restore-any ProductionOrder'); + } + + /** + * Determine whether the user can replicate the model. + */ + public function replicate(User $user, ProductionOrder $productionorder): bool + { + return $user->checkPermissionTo('replicate ProductionOrder'); + } + + /** + * Determine whether the user can reorder the models. + */ + public function reorder(User $user): bool + { + return $user->checkPermissionTo('reorder ProductionOrder'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, ProductionOrder $productionorder): bool + { + return $user->checkPermissionTo('force-delete ProductionOrder'); + } + + /** + * Determine whether the user can permanently delete any models. + */ + public function forceDeleteAny(User $user): bool + { + return $user->checkPermissionTo('force-delete-any ProductionOrder'); + } +} diff --git a/database/migrations/2026_04_29_100633_create_production_orders_table.php b/database/migrations/2026_04_29_100633_create_production_orders_table.php new file mode 100644 index 0000000..2edc0c0 --- /dev/null +++ b/database/migrations/2026_04_29_100633_create_production_orders_table.php @@ -0,0 +1,47 @@ + +Save + + + + diff --git a/resources/views/production-orders/print.blade.php b/resources/views/production-orders/print.blade.php new file mode 100644 index 0000000..f2b068a --- /dev/null +++ b/resources/views/production-orders/print.blade.php @@ -0,0 +1,105 @@ + + + + + + + +@foreach($stickers as $sticker) +
+ + + + + +
+ + +
{{ $sticker['serial'] }}
+
{{ $sticker['production_order'] }}
+
{{ $sticker['description'] }}
+
+
+@endforeach + + + diff --git a/routes/web.php b/routes/web.php index fba8a81..97e0ae8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ name('approval.approve.success'); +Route::get('/production-orders/print/{production_order}', + [ProductionOrderController::class, 'print'] +)->name('production-orders.print'); + // Route::get('/characteristic/approve', [CharacteristicApprovalController::class, 'approve']) // ->name('characteristic.approve') // ->middleware('signed'); @@ -67,6 +72,7 @@ Route::get('/approval/approve-success', function () { // ->middleware('signed'); // routes/web.php + Route::post('/save-serials-to-session', function (Request $request) { session(['serial_numbers' => $request->serial_numbers]); @@ -86,6 +92,16 @@ Route::get('/part-validation-image/{filename}', function ($filename) { return response()->file($path); })->name('part.validation.image'); +Route::get('/workflow-image/{filename}', function ($filename) { + $path = storage_path("app/private/uploads/LaserDocs/{$filename}"); + + if (! file_exists($path)) { + abort(404, 'Image not found'); + } + + return response()->file($path); +})->name('workflow.image'); + // web.php Route::post('/temp-upload', function (Request $request) { if (! $request->hasFile('photo')) {