diff --git a/app/Http/Controllers/FieldJobSettingController.php b/app/Http/Controllers/FieldJobSettingController.php new file mode 100644 index 0000000..41fcdbf --- /dev/null +++ b/app/Http/Controllers/FieldJobSettingController.php @@ -0,0 +1,21 @@ +with(['segment', 'asignDecision', 'completeDecision']) + ->get(); + + return Inertia::render('Settings/FieldJob/Index', [ + 'settings' => $settings, + ]); + } +} diff --git a/app/Http/Controllers/SegmentController.php b/app/Http/Controllers/SegmentController.php index 0dc580c..96a2f92 100644 --- a/app/Http/Controllers/SegmentController.php +++ b/app/Http/Controllers/SegmentController.php @@ -2,9 +2,17 @@ namespace App\Http\Controllers; +use App\Models\Segment; use Illuminate\Http\Request; +use Inertia\Inertia; class SegmentController extends Controller { - // + public function settings(Request $request) + { + return Inertia::render('Settings/Segments/Index', [ + 'segments' => Segment::query()->get(), + ]); + } } + diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 06635a4..72a97d2 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -10,127 +10,8 @@ class SettingController extends Controller // public function index(Request $request){ - - return Inertia::render('Settings/Index', [ - 'actions' => \App\Models\Action::query() - ->with(['decisions', 'segment']) - ->get(), - 'decisions' => \App\Models\Decision::query() - ->with('actions') - ->get(), - 'segments' => \App\Models\Segment::query() - ->get() - ] - ); + return Inertia::render('Settings/Index'); } - public function storeAction(Request $request) - { - $attributes = $request->validate([ - 'name' => 'required|string|max:50', - 'color_tag' => 'nullable|string|max:25', - 'segment_id' => 'nullable|integer|exists:segments,id', - 'decisions' => 'nullable|array', - 'decisions.*.id' => 'required_with:decisions.*|integer|exists:decisions,id', - 'decisions.*.name' => 'required_with:decisions.*|string|max:50', - ]); - - $decisionIds = collect($attributes['decisions'] ?? [])->pluck('id')->toArray(); - - \DB::transaction(function () use ($attributes, $decisionIds) { - /** @var \App\Models\Action $row */ - $row = \App\Models\Action::create([ - 'name' => $attributes['name'], - 'color_tag' => $attributes['color_tag'] ?? null, - 'segment_id' => $attributes['segment_id'] ?? null, - ]); - - if (!empty($decisionIds)) { - $row->decisions()->sync($decisionIds); - } - }); - - return to_route('settings')->with('success', 'Action created successfully!'); - } - - public function updateAction(int $id, Request $request) { - - $row = \App\Models\Action::findOrFail($id); - - $attributes = $request->validate([ - 'name' => 'required|string|max:50', - 'color_tag' => 'nullable|string|max:25', - 'segment_id' => 'nullable|integer|exists:segments,id', - 'decisions' => 'nullable|array', - 'decisions.*.id' => 'required_with:decisions.*|integer|exists:decisions,id', - 'decisions.*.name' => 'required_with:decisions.*|string|max:50' - ]); - - $decisionIds = collect($attributes['decisions'] ?? [])->pluck('id')->toArray(); - - \DB::transaction(function() use ($attributes, $decisionIds, $row) { - $row->update([ - 'name' => $attributes['name'], - 'color_tag' => $attributes['color_tag'], - 'segment_id' => $attributes['segment_id'] ?? null, - ]); - - $row->decisions()->sync($decisionIds); - }); - logger()->info('Model updated successfully', ['model_id' => $row->id]); - return to_route('settings')->with('success', 'Update successful!'); - - } - - public function storeDecision(Request $request) - { - $attributes = $request->validate([ - 'name' => 'required|string|max:50', - 'color_tag' => 'nullable|string|max:25', - 'actions' => 'nullable|array', - 'actions.*.id' => 'required_with:actions.*|integer|exists:actions,id', - 'actions.*.name' => 'required_with:actions.*|string|max:50', - ]); - - $actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray(); - - \DB::transaction(function () use ($attributes, $actionIds) { - /** @var \App\Models\Decision $row */ - $row = \App\Models\Decision::create([ - 'name' => $attributes['name'], - 'color_tag' => $attributes['color_tag'] ?? null, - ]); - - if (!empty($actionIds)) { - $row->actions()->sync($actionIds); - } - }); - - return to_route('settings')->with('success', 'Decision created successfully!'); - } - - public function updateDecision(int $id, Request $request) - { - $row = \App\Models\Decision::findOrFail($id); - - $attributes = $request->validate([ - 'name' => 'required|string|max:50', - 'color_tag' => 'nullable|string|max:25', - 'actions' => 'nullable|array', - 'actions.*.id' => 'required_with:actions.*|integer|exists:actions,id', - 'actions.*.name' => 'required_with:actions.*|string|max:50', - ]); - - $actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray(); - - \DB::transaction(function () use ($attributes, $actionIds, $row) { - $row->update([ - 'name' => $attributes['name'], - 'color_tag' => $attributes['color_tag'] ?? null, - ]); - $row->actions()->sync($actionIds); - }); - - return to_route('settings')->with('success', 'Decision updated successfully!'); - } + // Workflow actions/decisions moved to WorkflowController } diff --git a/app/Http/Controllers/WorkflowController.php b/app/Http/Controllers/WorkflowController.php new file mode 100644 index 0000000..b4178d0 --- /dev/null +++ b/app/Http/Controllers/WorkflowController.php @@ -0,0 +1,130 @@ + Action::query()->with(['decisions', 'segment'])->get(), + 'decisions' => Decision::query()->with('actions')->get(), + 'segments' => Segment::query()->get(), + ]); + } + + public function storeAction(Request $request) + { + $attributes = $request->validate([ + 'name' => 'required|string|max:50', + 'color_tag' => 'nullable|string|max:25', + 'segment_id' => 'nullable|integer|exists:segments,id', + 'decisions' => 'nullable|array', + 'decisions.*.id' => 'required_with:decisions.*|integer|exists:decisions,id', + 'decisions.*.name' => 'required_with:decisions.*|string|max:50', + ]); + + $decisionIds = collect($attributes['decisions'] ?? [])->pluck('id')->toArray(); + + \DB::transaction(function () use ($attributes, $decisionIds) { + /** @var \App\Models\Action $row */ + $row = Action::create([ + 'name' => $attributes['name'], + 'color_tag' => $attributes['color_tag'] ?? null, + 'segment_id' => $attributes['segment_id'] ?? null, + ]); + + if (!empty($decisionIds)) { + $row->decisions()->sync($decisionIds); + } + }); + + return to_route('settings.workflow')->with('success', 'Action created successfully!'); + } + + public function updateAction(int $id, Request $request) + { + $row = Action::findOrFail($id); + + $attributes = $request->validate([ + 'name' => 'required|string|max:50', + 'color_tag' => 'nullable|string|max:25', + 'segment_id' => 'nullable|integer|exists:segments,id', + 'decisions' => 'nullable|array', + 'decisions.*.id' => 'required_with:decisions.*|integer|exists:decisions,id', + 'decisions.*.name' => 'required_with:decisions.*|string|max:50' + ]); + + $decisionIds = collect($attributes['decisions'] ?? [])->pluck('id')->toArray(); + + \DB::transaction(function() use ($attributes, $decisionIds, $row) { + $row->update([ + 'name' => $attributes['name'], + 'color_tag' => $attributes['color_tag'], + 'segment_id' => $attributes['segment_id'] ?? null, + ]); + $row->decisions()->sync($decisionIds); + }); + + return to_route('settings.workflow')->with('success', 'Update successful!'); + } + + public function storeDecision(Request $request) + { + $attributes = $request->validate([ + 'name' => 'required|string|max:50', + 'color_tag' => 'nullable|string|max:25', + 'actions' => 'nullable|array', + 'actions.*.id' => 'required_with:actions.*|integer|exists:actions,id', + 'actions.*.name' => 'required_with:actions.*|string|max:50', + ]); + + $actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray(); + + \DB::transaction(function () use ($attributes, $actionIds) { + /** @var \App\Models\Decision $row */ + $row = Decision::create([ + 'name' => $attributes['name'], + 'color_tag' => $attributes['color_tag'] ?? null, + ]); + + if (!empty($actionIds)) { + $row->actions()->sync($actionIds); + } + }); + + return to_route('settings.workflow')->with('success', 'Decision created successfully!'); + } + + public function updateDecision(int $id, Request $request) + { + $row = Decision::findOrFail($id); + + $attributes = $request->validate([ + 'name' => 'required|string|max:50', + 'color_tag' => 'nullable|string|max:25', + 'actions' => 'nullable|array', + 'actions.*.id' => 'required_with:actions.*|integer|exists:actions,id', + 'actions.*.name' => 'required_with:actions.*|string|max:50', + ]); + + $actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray(); + + \DB::transaction(function () use ($attributes, $actionIds, $row) { + $row->update([ + 'name' => $attributes['name'], + 'color_tag' => $attributes['color_tag'] ?? null, + ]); + $row->actions()->sync($actionIds); + }); + + return to_route('settings.workflow')->with('success', 'Decision updated successfully!'); + } +} diff --git a/app/Jobs/GenerateDocumentPreview.php b/app/Jobs/GenerateDocumentPreview.php index 312e356..07dc0b7 100644 --- a/app/Jobs/GenerateDocumentPreview.php +++ b/app/Jobs/GenerateDocumentPreview.php @@ -15,6 +15,12 @@ class GenerateDocumentPreview implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + /** + * Max seconds this job may run before the worker kills it. + * Does not speed up the job, but protects the queue and surfaces timeouts. + */ + public $timeout = 180; // 3 minutes + public function __construct(public int $documentId) { } @@ -30,6 +36,22 @@ public function handle(): void return; $ext = strtolower(pathinfo($doc->original_name ?: $doc->file_name, PATHINFO_EXTENSION)); + + // If a preview was already generated after the document was last updated, skip re-generation + if ($doc->preview_generated_at && $doc->updated_at && $doc->preview_path) { + $previewDisk = config('files.preview_disk', 'public'); + if (Storage::disk($previewDisk)->exists($doc->preview_path)) { + if ($doc->updated_at->lte($doc->preview_generated_at)) { + Log::info('Skipping preview generation (already up to date)', [ + 'document_id' => $doc->id, + 'preview_path' => $doc->preview_path, + 'updated_at' => (string) $doc->updated_at, + 'preview_generated_at' => (string) $doc->preview_generated_at, + ]); + return; + } + } + } if (!in_array($ext, ['doc', 'docx'])) return; // only convert office docs here @@ -53,6 +75,14 @@ public function handle(): void // Run soffice headless to convert to PDF $binCfg = config('files.libreoffice_bin'); $bin = $binCfg ? (string) $binCfg : 'soffice'; + // If an absolute path is configured, ensure it exists to avoid long PATH resolution delays + if ($binCfg && preg_match('/^[a-zA-Z]:\\\\|^\//', $bin) && !file_exists($bin)) { + Log::warning('Configured LibreOffice binary not found; falling back to PATH', [ + 'document_id' => $doc->id, + 'bin' => $bin, + ]); + $bin = 'soffice'; + } // Windows quoting differs from POSIX. Build command parts safely. $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; if ($isWin) { @@ -81,6 +111,7 @@ public function handle(): void // Capture stderr as well for diagnostics $cmdWithStderr = $cmd . ' 2>&1'; + $t0 = microtime(true); Log::info('Starting LibreOffice preview conversion', [ 'document_id' => $doc->id, 'cmd' => $cmd, @@ -99,6 +130,7 @@ public function handle(): void @unlink($tmpIn); return; } + $elapsed = (int) round((microtime(true) - $t0) * 1000); $pdfPathLocal = $tmpIn . '.pdf'; // LibreOffice writes output with source filename base; derive path @@ -133,6 +165,11 @@ public function handle(): void $doc->preview_mime = 'application/pdf'; $doc->preview_generated_at = now(); $doc->save(); + Log::info('Preview generated and stored', [ + 'document_id' => $doc->id, + 'preview_path' => $doc->preview_path, + 'elapsed_ms' => $elapsed, + ]); } @unlink($tmpIn); diff --git a/app/Models/FieldJob.php b/app/Models/FieldJob.php new file mode 100644 index 0000000..2df7446 --- /dev/null +++ b/app/Models/FieldJob.php @@ -0,0 +1,42 @@ + 'date', + 'end_date' => 'date', + ]; + + protected static function booted(){ + static::creating(function (FieldJob $fieldJob) { + if(!isset($fieldJob->user_id)){ + $fieldJob->user_id = auth()->id(); + } + }); + } + + public function setting(): BelongsTo + { + return $this->belongsTo(FieldJobSetting::class, 'field_job_setting_id'); + } + + public function assignedUser(): BelongsTo + { + return $this->belongsTo(User::class, 'asigned_user_id'); + } +} diff --git a/app/Models/FieldJobSetting.php b/app/Models/FieldJobSetting.php new file mode 100644 index 0000000..b06bf47 --- /dev/null +++ b/app/Models/FieldJobSetting.php @@ -0,0 +1,39 @@ +belongsTo(Segment::class); + } + + public function asignDecision(): BelongsTo + { + return $this->belongsTo(Decision::class, 'asign_decision_id'); + } + + public function completeDecision(): BelongsTo + { + return $this->belongsTo(Decision::class, 'complete_decision_id'); + } + + public function fieldJobs(): HasMany + { + return $this->hasMany(FieldJob::class); + } +} diff --git a/database/migrations/2025_09_28_000001_create_field_job_settings_table.php b/database/migrations/2025_09_28_000001_create_field_job_settings_table.php new file mode 100644 index 0000000..e9e4cf7 --- /dev/null +++ b/database/migrations/2025_09_28_000001_create_field_job_settings_table.php @@ -0,0 +1,24 @@ +id(); + $table->foreignId('segment_id')->nullable()->constrained('segments')->nullOnDelete(); + $table->foreignId('initial_decision_id')->nullable()->constrained('decisions')->nullOnDelete(); + $table->foreignId('asign_decision_id')->nullable()->constrained('decisions')->nullOnDelete(); + $table->foreignId('complete_decision_id')->nullable()->constrained('decisions')->nullOnDelete(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('field_job_settings'); + } +}; diff --git a/database/migrations/2025_09_28_000002_create_field_jobs_table.php b/database/migrations/2025_09_28_000002_create_field_jobs_table.php new file mode 100644 index 0000000..c5f6560 --- /dev/null +++ b/database/migrations/2025_09_28_000002_create_field_jobs_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('field_job_setting_id')->constrained('field_job_settings')->cascadeOnDelete(); + $table->foreignId('asigned_user_id')->nullable()->constrained('users')->nullOnDelete(); + $table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); + $table->date('assigned_at')->nullable(); + $table->date('completed_at')->nullable(); + $table->date('cancelled_at')->nullable(); + $table->boolean('priority')->default(false); + $table->string('notes', 255)->nullable(); + $table->jsonb('address_snapshot ')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('field_jobs'); + } +}; diff --git a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue index 99aaa4e..3e7f9a0 100644 --- a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue +++ b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue @@ -84,7 +84,8 @@ const store = () => { }, onSuccess: () => { close(); - form.reset(); + // Preserve selected contract across submissions; reset only user-editable fields + form.reset('due_date', 'amount', 'note'); }, onError: (errors) => { console.log('Validation or server error:', errors); @@ -95,6 +96,17 @@ const store = () => { }); } +// When the drawer opens, always sync the current contractUuid into the form, +// even if the value hasn't changed (prevents stale/null contract_uuid after reset) +watch( + () => props.show, + (visible) => { + if (visible) { + form.contract_uuid = props.contractUuid || null; + } + } +); +