Skip to content

Commit 769ac9c

Browse files
author
Itamar Junior
committed
feat: add contract reminders system with custom scheduling options
1 parent 0a3ac75 commit 769ac9c

File tree

10 files changed

+317
-29
lines changed

10 files changed

+317
-29
lines changed

app/Livewire/Company/BudgetCalendar.php

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ public function mount()
3535
}
3636

3737
public function showDueItems($date)
38-
{
39-
$this->selectedDate = $date;
40-
$this->selectedDueItems = $this->dueDatesByDay[$date] ?? [];
41-
}
38+
{
39+
$this->selectedDate = $date;
40+
$this->selectedDueItems = $this->dueDatesByDay[$date] ?? [];
41+
}
4242

4343
public function updatedViewMode($value)
4444
{
@@ -78,13 +78,52 @@ public function loadMonthlyContracts()
7878

7979
if (!$this->selectedCompanyId) return;
8080

81-
$contracts = CompanyServiceContract::with(['provider', 'category'])
81+
$contracts = CompanyServiceContract::with(['provider', 'category', 'reminders.contract'])
8282
->where('company_id', $this->selectedCompanyId)
8383
->get();
8484

8585
foreach ($contracts as $contract) {
86-
$date = Carbon::parse($contract->next_due_date)->toDateString();
87-
$this->dueDatesByDay[$date][] = $contract;
86+
// Add contract end_date (if any)
87+
if ($contract->end_date) {
88+
$date = Carbon::parse($contract->end_date)->toDateString();
89+
$this->dueDatesByDay[$date][] = [
90+
'type' => 'contract',
91+
'title' => "{$contract->category->name} - {$contract->provider->name}",
92+
'model' => $contract,
93+
'due_date' => $date,
94+
];
95+
}
96+
97+
98+
foreach ($contract->reminders as $reminder) {
99+
$allDates = [];
100+
101+
// Always include the primary due_date first
102+
if ($reminder->due_date) {
103+
$allDates[] = Carbon::parse($reminder->due_date)->toDateString();
104+
}
105+
106+
// Add custom dates if manual
107+
if ($reminder->frequency === 'manual') {
108+
$customDates = is_string($reminder->custom_dates) ? json_decode($reminder->custom_dates) : $reminder->custom_dates;
109+
110+
if (is_array($customDates)) {
111+
foreach ($customDates as $customDate) {
112+
$allDates[] = Carbon::parse($customDate)->toDateString();
113+
}
114+
}
115+
}
116+
117+
// Add all to dueDatesByDay
118+
foreach (array_unique($allDates) as $date) {
119+
$this->dueDatesByDay[$date][] = [
120+
'type' => 'reminder',
121+
'title' => $reminder->title,
122+
'model' => $reminder,
123+
'due_date' => $date,
124+
];
125+
}
126+
}
88127
}
89128
}
90129

app/Livewire/Company/Service/ContractManager.php

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Livewire\Component;
77
use App\Models\ServiceCategory;
88
use App\Models\ServiceProvider;
9+
use App\Models\ContractReminder;
910
use Illuminate\Support\Facades\Auth;
1011
use App\Models\CompanyServiceContract;
1112
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@@ -24,7 +25,7 @@ class ContractManager extends Component
2425
public $service_provider_id;
2526
public $budget;
2627
public $start_date;
27-
public $next_due_date;
28+
public $end_date;
2829
public $status = 'active';
2930
public $notes;
3031

@@ -36,6 +37,17 @@ class ContractManager extends Component
3637
public ?CompanyServiceContract $editingContract = null;
3738
public ?CompanyServiceContract $deletingContract = null;
3839

40+
// Reminder properties
41+
public bool $showReminderModal = false;
42+
public ?int $contractReminderContractId = null;
43+
public $reminder_title, $reminder_due_date, $reminder_frequency = 'manual';
44+
public $reminder_day_of_month, $reminder_custom_dates = [], $reminder_months_active = [];
45+
public $reminder_days_before = 7, $reminder_days_after = 0, $reminder_notes;
46+
public string $reminder_months_active_string = '';
47+
public string $reminder_custom_dates_string = '';
48+
public ?int $reminder_id = null;
49+
50+
3951
public function mount()
4052
{
4153
$this->companies = Company::where('business_id', Auth::user()->current_business_id)->orderBy('name')->get();
@@ -53,14 +65,14 @@ public function updatedServiceCategoryId($categoryId)
5365

5466
public function loadContracts()
5567
{
56-
$this->contracts = CompanyServiceContract::with(['company', 'provider', 'category'])
68+
$this->contracts = CompanyServiceContract::with(['company', 'provider', 'category', 'reminders'])
5769
->whereHas('company', fn($q) => $q->where('business_id', Auth::user()->current_business_id))
5870
->latest()->get();
5971
}
6072

6173
public function openCreateModal()
6274
{
63-
$this->reset(['company_id', 'service_category_id', 'service_provider_id', 'budget', 'start_date', 'next_due_date', 'status', 'notes']);
75+
$this->reset(['company_id', 'service_category_id', 'service_provider_id', 'budget', 'start_date', 'end_date', 'status', 'notes']);
6476
$this->resetValidation();
6577
$this->showCreateModal = true;
6678
}
@@ -73,7 +85,7 @@ public function create()
7385
'service_provider_id' => 'required|exists:service_providers,id',
7486
'budget' => 'nullable|numeric',
7587
'start_date' => 'nullable|date',
76-
'next_due_date' => 'nullable|date',
88+
'end_date' => 'nullable|date',
7789
'status' => 'required|in:active,inactive,terminated',
7890
]);
7991

@@ -83,7 +95,7 @@ public function create()
8395
'service_provider_id' => $this->service_provider_id,
8496
'budget' => $this->budget,
8597
'start_date' => $this->start_date,
86-
'next_due_date' => $this->next_due_date,
98+
'end_date' => $this->end_date,
8799
'status' => $this->status,
88100
'notes' => $this->notes,
89101
]);
@@ -103,7 +115,7 @@ public function edit($id)
103115
$this->service_provider_id = $this->editingContract->service_provider_id;
104116
$this->budget = $this->editingContract->budget;
105117
$this->start_date = $this->editingContract->start_date;
106-
$this->next_due_date = $this->editingContract->next_due_date;
118+
$this->end_date = $this->editingContract->end_date;
107119
$this->status = $this->editingContract->status;
108120
$this->notes = $this->editingContract->notes;
109121

@@ -118,7 +130,7 @@ public function update()
118130
'service_provider_id' => 'required|exists:service_providers,id',
119131
'budget' => 'nullable|numeric',
120132
'start_date' => 'nullable|date',
121-
'next_due_date' => 'nullable|date',
133+
'end_date' => 'nullable|date',
122134
'status' => 'required|in:active,inactive,terminated',
123135
]);
124136

@@ -130,7 +142,7 @@ public function update()
130142
'service_provider_id' => $this->service_provider_id,
131143
'budget' => $this->budget,
132144
'start_date' => $this->start_date,
133-
'next_due_date' => $this->next_due_date,
145+
'end_date' => $this->end_date,
134146
'status' => $this->status,
135147
'notes' => $this->notes,
136148
]);
@@ -158,6 +170,91 @@ public function delete()
158170
session()->flash('success', 'Contract deleted.');
159171
}
160172

173+
public function openReminderModal($contractIdToAttachToReminder)
174+
{
175+
$this->reset([
176+
'reminder_title',
177+
'reminder_due_date',
178+
'reminder_frequency',
179+
'reminder_day_of_month',
180+
'reminder_custom_dates',
181+
'reminder_months_active',
182+
'reminder_days_before',
183+
'reminder_days_after',
184+
'reminder_notes',
185+
'reminder_id',
186+
]);
187+
188+
$this->contractReminderContractId = $contractIdToAttachToReminder;
189+
$this->showReminderModal = true;
190+
}
191+
192+
public function saveReminder()
193+
{
194+
$months = collect(explode(',', $this->reminder_months_active_string))
195+
->filter()
196+
->map(fn($m) => (int) trim($m))
197+
->filter(fn($m) => $m >= 1 && $m <= 12)
198+
->values()
199+
->toArray();
200+
201+
$dates = collect(explode(',', $this->reminder_custom_dates_string))
202+
->map(fn($d) => trim($d))
203+
->filter(function ($d) {
204+
try {
205+
return \Carbon\Carbon::createFromFormat('Y-m-d', $d) !== false;
206+
} catch (\Exception $e) {
207+
return false;
208+
}
209+
})
210+
->values()
211+
->toArray();
212+
213+
$data = [
214+
'company_service_contract_id' => $this->contractReminderContractId,
215+
'title' => $this->reminder_title,
216+
'due_date' => $this->reminder_due_date,
217+
'frequency' => $this->reminder_frequency,
218+
'day_of_month' => $this->reminder_day_of_month,
219+
'custom_dates' => $dates,
220+
'months_active' => $months,
221+
'reminder_days_before' => $this->reminder_days_before,
222+
'reminder_days_after' => $this->reminder_days_after,
223+
'notes' => $this->reminder_notes,
224+
];
225+
226+
if ($this->reminder_id) {
227+
ContractReminder::find($this->reminder_id)?->update($data);
228+
session()->flash('success', 'Reminder updated.');
229+
} else {
230+
ContractReminder::create($data);
231+
session()->flash('success', 'Reminder created.');
232+
}
233+
234+
$this->reset(['showReminderModal', 'reminder_id']);
235+
$this->loadContracts();
236+
}
237+
238+
public function editReminder($reminderId)
239+
{
240+
$reminder = ContractReminder::findOrFail($reminderId);
241+
242+
$this->reminder_id = $reminder->id;
243+
$this->contractReminderContractId = $reminder->company_service_contract_id;
244+
245+
$this->reminder_title = $reminder->title;
246+
$this->reminder_due_date = $reminder->due_date;
247+
$this->reminder_frequency = $reminder->frequency;
248+
$this->reminder_day_of_month = $reminder->day_of_month;
249+
$this->reminder_custom_dates_string = collect($reminder->custom_dates)->implode(',');
250+
$this->reminder_months_active_string = collect($reminder->months_active)->implode(',');
251+
$this->reminder_days_before = $reminder->reminder_days_before;
252+
$this->reminder_days_after = $reminder->reminder_days_after;
253+
$this->reminder_notes = $reminder->notes;
254+
255+
$this->showReminderModal = true;
256+
}
257+
161258
public function render()
162259
{
163260
return view('livewire.company.service.contract-manager');

app/Models/CompanyServiceContract.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class CompanyServiceContract extends Model
1515
'service_category_id',
1616
'budget',
1717
'start_date',
18-
'next_due_date',
18+
'end_date',
1919
'status',
2020
'notes',
2121
];
@@ -34,4 +34,9 @@ public function category(): \Illuminate\Database\Eloquent\Relations\BelongsTo
3434
{
3535
return $this->belongsTo(ServiceCategory::class, 'service_category_id');
3636
}
37+
38+
public function reminders(): \Illuminate\Database\Eloquent\Relations\HasMany
39+
{
40+
return $this->hasMany(ContractReminder::class, 'company_service_contract_id');
41+
}
3742
}

app/Models/ContractReminder.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Factories\HasFactory;
7+
8+
class ContractReminder extends Model
9+
{
10+
use HasFactory;
11+
12+
protected $fillable = [
13+
'company_service_contract_id',
14+
'title',
15+
'due_date', // first/next due date
16+
'frequency', // manual, monthly, bimonthly, quarterly, yearly, once
17+
'day_of_month', // e.g., 15th of the month
18+
'months_active', // e.g., ["January", "February", "March"] or ["1", "2", "3"] for Jan, Feb, Mar
19+
'custom_dates', // e.g., ["2025-02-12", "2025-05-08", "2025-11-30"]
20+
'reminder_days_before', // days before the due date to notify
21+
'reminder_days_after', // days after the due date to notify
22+
'notified_before', // whether notification before the due date has been sent
23+
'notified_after', // whether notification after the due date has been sent
24+
'notes', // additional notes for the reminder
25+
];
26+
27+
protected $casts = [
28+
'custom_dates' => 'array',
29+
'months_active' => 'array',
30+
];
31+
32+
public function contract()
33+
{
34+
return $this->belongsTo(CompanyServiceContract::class, 'company_service_contract_id');
35+
}
36+
}

database/migrations/2025_06_16_100437_create_company_service_contracts_table.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function up(): void
2121

2222
$table->decimal('budget', 12, 2)->nullable();
2323
$table->date('start_date')->nullable();
24-
$table->date('next_due_date')->nullable();
24+
$table->date('end_date')->nullable();
2525
$table->enum('status', ['active', 'inactive', 'terminated'])->default('active');
2626
$table->text('notes')->nullable();
2727
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::create('contract_reminders', function (Blueprint $table) {
15+
$table->id();
16+
$table->timestamps();
17+
18+
$table->foreignId('company_service_contract_id')->constrained()->cascadeOnDelete();
19+
$table->string('title')->nullable();
20+
$table->date('due_date'); // first/next due date
21+
$table->enum('frequency', ['manual','weekly', 'biweekly', 'semimonthly', 'monthly', 'bimonthly', 'threemonthly', 'quarterly', 'yearly', 'once'])->nullable();
22+
23+
$table->unsignedTinyInteger('day_of_month')->nullable(); // e.g., 15th of the month
24+
$table->json('months_active')->nullable(); // e.g., ["January", "February", "March"] or ["1", "2", "3"] for Jan, Feb, Mar
25+
$table->json('custom_dates')->nullable(); // e.g., ["2025-02-12", "2025-05-08", "2025-11-30"]
26+
27+
$table->integer('reminder_days_before')->default(0);
28+
$table->integer('reminder_days_after')->default(0);
29+
$table->boolean('notified_before')->default(false);
30+
$table->boolean('notified_after')->default(false);
31+
$table->text('notes')->nullable();
32+
});
33+
}
34+
35+
/**
36+
* Reverse the migrations.
37+
*/
38+
public function down(): void
39+
{
40+
Schema::dropIfExists('contract_reminders');
41+
}
42+
};

0 commit comments

Comments
 (0)