Compare commits
42 Commits
3b284fa4bd
...
previous
| Author | SHA1 | Date | |
|---|---|---|---|
| 229c100cc4 | |||
| 9a4897bf0c | |||
| b2a9350d0f | |||
| 27bdb942ab | |||
| 7eaab16e30 | |||
| 6a2dd860fa | |||
| ca8754cd94 | |||
| 8fdc0d6359 | |||
| 7fc4520dbf | |||
| dc41862afc | |||
| fb6474ab88 | |||
| 2ad24216ae | |||
| 8031501d25 | |||
| adc2a64687 | |||
| 11206fb4f7 | |||
| 39a597f6eb | |||
| 5d4498ac5a | |||
| 622f53e401 | |||
| 96473fd60b | |||
| 5ddca35389 | |||
| 94ad0c0772 | |||
| 2140181a76 | |||
| 06fa443b3e | |||
| 6c45063e47 | |||
| b8c9b51f29 | |||
| a4db37adfa | |||
| 76f76f73b4 | |||
| d69f4dd6f6 | |||
| a596177a68 | |||
| aa40ebed5c | |||
| 79de54eef0 | |||
| 53941c054e | |||
| 1a7d2793b0 | |||
| fa54cf48f3 | |||
| d2287ef963 | |||
| fb7160eb33 | |||
| 44f9f8f9fa | |||
| edbdb64102 | |||
| 8125b4d321 | |||
| 46feba2df7 | |||
| 1395b72ae8 | |||
| ad8e0d5cee |
@@ -24,8 +24,7 @@ public function build($options = null)
|
|||||||
->get();
|
->get();
|
||||||
|
|
||||||
$months = $data->pluck('month')->map(
|
$months = $data->pluck('month')->map(
|
||||||
fn($nu)
|
fn ($nu) => \DateTime::createFromFormat('!m', $nu)->format('F'))->toArray();
|
||||||
=> \DateTime::createFromFormat('!m', $nu)->format('F'))->toArray();
|
|
||||||
|
|
||||||
$newCases = $data->pluck('count')->toArray();
|
$newCases = $data->pluck('count')->toArray();
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use App\Models\Post;
|
use App\Models\Post;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class ImportPosts extends Command
|
class ImportPosts extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'import:posts';
|
protected $signature = 'import:posts';
|
||||||
|
|
||||||
protected $description = 'Import posts into Algolia without clearing the index';
|
protected $description = 'Import posts into Algolia without clearing the index';
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
@@ -22,4 +23,3 @@ public function handle()
|
|||||||
$this->info('Posts have been imported into Algolia.');
|
$this->info('Posts have been imported into Algolia.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,15 @@
|
|||||||
class PruneDocumentPreviews extends Command
|
class PruneDocumentPreviews extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'documents:prune-previews {--days=90 : Delete previews older than this many days} {--dry-run : Show what would be deleted without deleting}';
|
protected $signature = 'documents:prune-previews {--days=90 : Delete previews older than this many days} {--dry-run : Show what would be deleted without deleting}';
|
||||||
|
|
||||||
protected $description = 'Deletes generated document preview files older than N days and clears their metadata.';
|
protected $description = 'Deletes generated document preview files older than N days and clears their metadata.';
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$days = (int) $this->option('days');
|
$days = (int) $this->option('days');
|
||||||
if ($days < 1) { $days = 90; }
|
if ($days < 1) {
|
||||||
|
$days = 90;
|
||||||
|
}
|
||||||
$cutoff = Carbon::now()->subDays($days);
|
$cutoff = Carbon::now()->subDays($days);
|
||||||
|
|
||||||
$previewDisk = config('files.preview_disk', 'public');
|
$previewDisk = config('files.preview_disk', 'public');
|
||||||
@@ -27,6 +30,7 @@ public function handle(): int
|
|||||||
$count = $query->count();
|
$count = $query->count();
|
||||||
if ($count === 0) {
|
if ($count === 0) {
|
||||||
$this->info('No stale previews found.');
|
$this->info('No stale previews found.');
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,9 +40,12 @@ public function handle(): int
|
|||||||
$query->chunkById(200, function ($docs) use ($previewDisk, $dry) {
|
$query->chunkById(200, function ($docs) use ($previewDisk, $dry) {
|
||||||
foreach ($docs as $doc) {
|
foreach ($docs as $doc) {
|
||||||
$path = $doc->preview_path;
|
$path = $doc->preview_path;
|
||||||
if (!$path) { continue; }
|
if (! $path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ($dry) {
|
if ($dry) {
|
||||||
$this->line("Would delete: {$previewDisk}://{$path} (document #{$doc->id})");
|
$this->line("Would delete: {$previewDisk}://{$path} (document #{$doc->id})");
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ protected function schedule(Schedule $schedule): void
|
|||||||
// Optionally prune old previews daily
|
// Optionally prune old previews daily
|
||||||
if (config('files.enable_preview_prune', true)) {
|
if (config('files.enable_preview_prune', true)) {
|
||||||
$days = (int) config('files.preview_retention_days', 90);
|
$days = (int) config('files.preview_retention_days', 90);
|
||||||
if ($days < 1) { $days = 90; }
|
if ($days < 1) {
|
||||||
|
$days = 90;
|
||||||
|
}
|
||||||
$schedule->command('documents:prune-previews', [
|
$schedule->command('documents:prune-previews', [
|
||||||
'--days' => $days,
|
'--days' => $days,
|
||||||
])->dailyAt('02:00');
|
])->dailyAt('02:00');
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use App\Models\Contract;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromQuery;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithCustomValueBinder;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||||
|
|
||||||
|
class ClientContractsExport extends DefaultValueBinder implements FromQuery, ShouldAutoSize, WithColumnFormatting, WithCustomValueBinder, WithHeadings, WithMapping
|
||||||
|
{
|
||||||
|
public const DATE_EXCEL_FORMAT = 'dd"."mm"."yyyy';
|
||||||
|
|
||||||
|
public const TEXT_EXCEL_FORMAT = NumberFormat::FORMAT_TEXT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private array $columnLetterMap = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array{label: string}>
|
||||||
|
*/
|
||||||
|
public const COLUMN_METADATA = [
|
||||||
|
'reference' => ['label' => 'Referenca'],
|
||||||
|
'customer' => ['label' => 'Stranka'],
|
||||||
|
'address' => ['label' => 'Naslov'],
|
||||||
|
'start' => ['label' => 'Začetek'],
|
||||||
|
'segment' => ['label' => 'Segment'],
|
||||||
|
'balance' => ['label' => 'Stanje'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $columns
|
||||||
|
*/
|
||||||
|
public function __construct(private Builder $query, private array $columns) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function allowedColumns(): array
|
||||||
|
{
|
||||||
|
return array_keys(self::COLUMN_METADATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function columnLabel(string $column): string
|
||||||
|
{
|
||||||
|
return self::COLUMN_METADATA[$column]['label'] ?? $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function query(): Builder
|
||||||
|
{
|
||||||
|
return $this->query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, mixed>
|
||||||
|
*/
|
||||||
|
public function map($row): array
|
||||||
|
{
|
||||||
|
return array_map(fn (string $column) => $this->resolveValue($row, $column), $this->columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return array_map(fn (string $column) => self::columnLabel($column), $this->columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function columnFormats(): array
|
||||||
|
{
|
||||||
|
$formats = [];
|
||||||
|
|
||||||
|
foreach ($this->getColumnLetterMap() as $letter => $column) {
|
||||||
|
if ($column === 'reference') {
|
||||||
|
$formats[$letter] = self::TEXT_EXCEL_FORMAT;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($column === 'start') {
|
||||||
|
$formats[$letter] = self::DATE_EXCEL_FORMAT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $formats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveValue(Contract $contract, string $column): mixed
|
||||||
|
{
|
||||||
|
return match ($column) {
|
||||||
|
'reference' => $contract->reference,
|
||||||
|
'customer' => optional($contract->clientCase?->person)->full_name,
|
||||||
|
'address' => optional($contract->clientCase?->person?->address)->address,
|
||||||
|
'start' => $this->formatDate($contract->start_date),
|
||||||
|
'segment' => $contract->segments?->first()?->name,
|
||||||
|
'balance' => optional($contract->account)->balance_amount,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatDate(?string $date): mixed
|
||||||
|
{
|
||||||
|
if (empty($date)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$carbon = Carbon::parse($date);
|
||||||
|
|
||||||
|
return ExcelDate::dateTimeToExcel($carbon);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function getColumnLetterMap(): array
|
||||||
|
{
|
||||||
|
if ($this->columnLetterMap !== []) {
|
||||||
|
return $this->columnLetterMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
$letter = 'A';
|
||||||
|
foreach ($this->columns as $column) {
|
||||||
|
$this->columnLetterMap[$letter] = $column;
|
||||||
|
$letter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->columnLetterMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bindValue(Cell $cell, $value): bool
|
||||||
|
{
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
$cell->setValueExplicit($value, DataType::TYPE_NUMERIC);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::bindValue($cell, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use App\Models\Contract;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromQuery;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithCustomValueBinder;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||||
|
|
||||||
|
class SegmentContractsExport extends DefaultValueBinder implements FromQuery, ShouldAutoSize, WithColumnFormatting, WithCustomValueBinder, WithHeadings, WithMapping
|
||||||
|
{
|
||||||
|
public const DATE_EXCEL_FORMAT = 'dd"."mm"."yyyy';
|
||||||
|
|
||||||
|
public const TEXT_EXCEL_FORMAT = NumberFormat::FORMAT_TEXT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private array $columnLetterMap = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array{label: string}>
|
||||||
|
*/
|
||||||
|
public const COLUMN_METADATA = [
|
||||||
|
'reference' => ['label' => 'Pogodba'],
|
||||||
|
'client_case' => ['label' => 'Primer'],
|
||||||
|
'address' => ['label' => 'Naslov'],
|
||||||
|
'client' => ['label' => 'Stranka'],
|
||||||
|
'type' => ['label' => 'Vrsta'],
|
||||||
|
'start_date' => ['label' => 'Začetek'],
|
||||||
|
'end_date' => ['label' => 'Konec'],
|
||||||
|
'account' => ['label' => 'Stanje'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $columns
|
||||||
|
*/
|
||||||
|
public function __construct(private Builder $query, private array $columns) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function allowedColumns(): array
|
||||||
|
{
|
||||||
|
return array_keys(self::COLUMN_METADATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function columnLabel(string $column): string
|
||||||
|
{
|
||||||
|
return self::COLUMN_METADATA[$column]['label'] ?? $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function query(): Builder
|
||||||
|
{
|
||||||
|
return $this->query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, mixed>
|
||||||
|
*/
|
||||||
|
public function map($row): array
|
||||||
|
{
|
||||||
|
return array_map(fn (string $column) => $this->resolveValue($row, $column), $this->columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return array_map(fn (string $column) => self::columnLabel($column), $this->columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function columnFormats(): array
|
||||||
|
{
|
||||||
|
$formats = [];
|
||||||
|
|
||||||
|
foreach ($this->getColumnLetterMap() as $letter => $column) {
|
||||||
|
if ($column === 'reference') {
|
||||||
|
$formats[$letter] = self::TEXT_EXCEL_FORMAT;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($column, ['start_date', 'end_date'], true)) {
|
||||||
|
$formats[$letter] = self::DATE_EXCEL_FORMAT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $formats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveValue(Contract $contract, string $column): mixed
|
||||||
|
{
|
||||||
|
return match ($column) {
|
||||||
|
'reference' => $contract->reference,
|
||||||
|
'client_case' => optional($contract->clientCase?->person)->full_name,
|
||||||
|
'address' => optional($contract->clientCase?->person?->address)->address,
|
||||||
|
'client' => optional($contract->clientCase?->client?->person)->full_name,
|
||||||
|
'type' => optional($contract->type)->name,
|
||||||
|
'start_date' => $this->formatDate($contract->start_date),
|
||||||
|
'end_date' => $this->formatDate($contract->end_date),
|
||||||
|
'account' => optional($contract->account)->balance_amount,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatDate(mixed $value): ?float
|
||||||
|
{
|
||||||
|
$carbon = Carbon::make($value);
|
||||||
|
|
||||||
|
if (! $carbon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExcelDate::dateTimeToExcel($carbon->copy()->startOfDay());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function columnLetter(int $index): string
|
||||||
|
{
|
||||||
|
$index++;
|
||||||
|
$letter = '';
|
||||||
|
|
||||||
|
while ($index > 0) {
|
||||||
|
$remainder = ($index - 1) % 26;
|
||||||
|
$letter = chr(65 + $remainder).$letter;
|
||||||
|
$index = intdiv($index - 1, 26);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $letter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bindValue(Cell $cell, $value): bool
|
||||||
|
{
|
||||||
|
$columnKey = $this->getColumnLetterMap()[$cell->getColumn()] ?? null;
|
||||||
|
|
||||||
|
if ($columnKey === 'reference') {
|
||||||
|
$cell->setValueExplicit((string) $value, DataType::TYPE_STRING);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::bindValue($cell, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function getColumnLetterMap(): array
|
||||||
|
{
|
||||||
|
if ($this->columnLetterMap === []) {
|
||||||
|
foreach ($this->columns as $index => $column) {
|
||||||
|
$this->columnLetterMap[$this->columnLetter($index)] = $column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->columnLetterMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Account;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Inertia\Inertia;
|
|
||||||
|
|
||||||
class AccountController extends Controller
|
class AccountController extends Controller
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ class ActivityNotificationController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function __invoke(Request $request)
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$data = $request->validate([
|
||||||
'activity_id' => ['required', 'integer', 'exists:activities,id'],
|
'activity_id' => ['sometimes', 'integer', 'exists:activities,id'],
|
||||||
|
'activity_ids' => ['sometimes', 'array', 'min:1'],
|
||||||
|
'activity_ids.*' => ['integer', 'exists:activities,id'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$userId = optional($request->user())->id;
|
$userId = optional($request->user())->id;
|
||||||
@@ -22,9 +24,18 @@ public function __invoke(Request $request)
|
|||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$activity = Activity::query()->select(['id', 'due_date'])->findOrFail($request->integer('activity_id'));
|
$ids = [];
|
||||||
$due = optional($activity->due_date) ? date('Y-m-d', strtotime($activity->due_date)) : now()->toDateString();
|
if (!empty($data['activity_id'])) {
|
||||||
|
$ids[] = $data['activity_id'];
|
||||||
|
}
|
||||||
|
if (!empty($data['activity_ids'])) {
|
||||||
|
$ids = array_merge($ids, $data['activity_ids']);
|
||||||
|
}
|
||||||
|
$ids = array_unique($ids);
|
||||||
|
|
||||||
|
$activities = Activity::query()->select(['id', 'due_date'])->whereIn('id', $ids)->get();
|
||||||
|
foreach ($activities as $activity) {
|
||||||
|
$due = optional($activity->due_date) ? date('Y-m-d', strtotime($activity->due_date)) : now()->toDateString();
|
||||||
ActivityNotificationRead::query()->updateOrCreate(
|
ActivityNotificationRead::query()->updateOrCreate(
|
||||||
[
|
[
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
@@ -35,7 +46,8 @@ public function __invoke(Request $request)
|
|||||||
'read_at' => now(),
|
'read_at' => now(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json(['status' => 'ok']);
|
return back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public function index(Request $request): Response
|
|||||||
->get(['id', 'profile_id', 'sname', 'phone_number']);
|
->get(['id', 'profile_id', 'sname', 'phone_number']);
|
||||||
$templates = \App\Models\SmsTemplate::query()
|
$templates = \App\Models\SmsTemplate::query()
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(['id', 'name']);
|
->get(['id', 'name', 'content']);
|
||||||
$segments = \App\Models\Segment::query()
|
$segments = \App\Models\Segment::query()
|
||||||
->where('active', true)
|
->where('active', true)
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
@@ -98,6 +98,10 @@ public function show(Package $package, SmsService $sms): Response
|
|||||||
'start_date' => (string) ($c->start_date ?? ''),
|
'start_date' => (string) ($c->start_date ?? ''),
|
||||||
'end_date' => (string) ($c->end_date ?? ''),
|
'end_date' => (string) ($c->end_date ?? ''),
|
||||||
];
|
];
|
||||||
|
// Include contract.meta as flattened key-value pairs
|
||||||
|
if (is_array($c->meta) && ! empty($c->meta)) {
|
||||||
|
$vars['contract']['meta'] = $this->flattenMeta($c->meta);
|
||||||
|
}
|
||||||
if ($c->account) {
|
if ($c->account) {
|
||||||
$initialRaw = (string) $c->account->initial_amount;
|
$initialRaw = (string) $c->account->initial_amount;
|
||||||
$balanceRaw = (string) $c->account->balance_amount;
|
$balanceRaw = (string) $c->account->balance_amount;
|
||||||
@@ -121,7 +125,7 @@ public function show(Package $package, SmsService $sms): Response
|
|||||||
if (! $rendered) {
|
if (! $rendered) {
|
||||||
$body = isset($payload['body']) ? trim((string) $payload['body']) : '';
|
$body = isset($payload['body']) ? trim((string) $payload['body']) : '';
|
||||||
if ($body !== '') {
|
if ($body !== '') {
|
||||||
$rendered = $body;
|
$rendered = $sms->renderContent($body, $vars);
|
||||||
} elseif (! empty($payload['template_id'])) {
|
} elseif (! empty($payload['template_id'])) {
|
||||||
$tpl = \App\Models\SmsTemplate::find((int) $payload['template_id']);
|
$tpl = \App\Models\SmsTemplate::find((int) $payload['template_id']);
|
||||||
if ($tpl) {
|
if ($tpl) {
|
||||||
@@ -157,6 +161,10 @@ public function show(Package $package, SmsService $sms): Response
|
|||||||
'start_date' => (string) ($c->start_date ?? ''),
|
'start_date' => (string) ($c->start_date ?? ''),
|
||||||
'end_date' => (string) ($c->end_date ?? ''),
|
'end_date' => (string) ($c->end_date ?? ''),
|
||||||
];
|
];
|
||||||
|
// Include contract.meta as flattened key-value pairs
|
||||||
|
if (is_array($c->meta) && ! empty($c->meta)) {
|
||||||
|
$vars['contract']['meta'] = $this->flattenMeta($c->meta);
|
||||||
|
}
|
||||||
if ($c->account) {
|
if ($c->account) {
|
||||||
$initialRaw = (string) $c->account->initial_amount;
|
$initialRaw = (string) $c->account->initial_amount;
|
||||||
$balanceRaw = (string) $c->account->balance_amount;
|
$balanceRaw = (string) $c->account->balance_amount;
|
||||||
@@ -175,7 +183,7 @@ public function show(Package $package, SmsService $sms): Response
|
|||||||
if ($body !== '') {
|
if ($body !== '') {
|
||||||
$preview = [
|
$preview = [
|
||||||
'source' => 'body',
|
'source' => 'body',
|
||||||
'content' => $body,
|
'content' => $sms->renderContent($body, $vars),
|
||||||
];
|
];
|
||||||
} elseif (! empty($payload['template_id'])) {
|
} elseif (! empty($payload['template_id'])) {
|
||||||
/** @var SmsTemplate|null $tpl */
|
/** @var SmsTemplate|null $tpl */
|
||||||
@@ -215,6 +223,8 @@ public function store(StorePackageRequest $request): RedirectResponse
|
|||||||
'created_by' => optional($request->user())->id,
|
'created_by' => optional($request->user())->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
dd($data['items']);
|
||||||
|
|
||||||
$items = collect($data['items'])
|
$items = collect($data['items'])
|
||||||
->map(function (array $row) {
|
->map(function (array $row) {
|
||||||
return new PackageItem([
|
return new PackageItem([
|
||||||
@@ -286,30 +296,39 @@ public function cancel(Package $package): RedirectResponse
|
|||||||
public function contracts(Request $request, PhoneSelector $selector): \Illuminate\Http\JsonResponse
|
public function contracts(Request $request, PhoneSelector $selector): \Illuminate\Http\JsonResponse
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'segment_id' => ['required', 'integer', 'exists:segments,id'],
|
'segment_id' => ['nullable', 'integer', 'exists:segments,id'],
|
||||||
'q' => ['nullable', 'string'],
|
'q' => ['nullable', 'string'],
|
||||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||||
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
|
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
|
||||||
'only_mobile' => ['nullable', 'boolean'],
|
'only_mobile' => ['nullable', 'boolean'],
|
||||||
'only_validated' => ['nullable', 'boolean'],
|
'only_validated' => ['nullable', 'boolean'],
|
||||||
|
'start_date_from' => ['nullable', 'date'],
|
||||||
|
'start_date_to' => ['nullable', 'date'],
|
||||||
|
'promise_date_from' => ['nullable', 'date'],
|
||||||
|
'promise_date_to' => ['nullable', 'date'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$segmentId = (int) $request->input('segment_id');
|
$segmentId = $request->input('segment_id') ? (int) $request->input('segment_id') : null;
|
||||||
$perPage = (int) ($request->input('per_page') ?? 25);
|
$perPage = (int) ($request->input('per_page') ?? 25);
|
||||||
|
|
||||||
$query = Contract::query()
|
$query = Contract::query()
|
||||||
->join('contract_segment', function ($j) use ($segmentId) {
|
|
||||||
$j->on('contract_segment.contract_id', '=', 'contracts.id')
|
|
||||||
->where('contract_segment.segment_id', '=', $segmentId)
|
|
||||||
->where('contract_segment.active', true);
|
|
||||||
})
|
|
||||||
->with([
|
->with([
|
||||||
'clientCase.person.phones',
|
'clientCase.person.phones',
|
||||||
'clientCase.client.person',
|
'clientCase.client.person',
|
||||||
|
'account',
|
||||||
])
|
])
|
||||||
->select('contracts.*')
|
->select('contracts.*')
|
||||||
->latest('contracts.id');
|
->latest('contracts.id');
|
||||||
|
|
||||||
|
// Optional segment filter
|
||||||
|
if ($segmentId) {
|
||||||
|
$query->join('contract_segment', function ($j) use ($segmentId) {
|
||||||
|
$j->on('contract_segment.contract_id', '=', 'contracts.id')
|
||||||
|
->where('contract_segment.segment_id', '=', $segmentId)
|
||||||
|
->where('contract_segment.active', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ($q = trim((string) $request->input('q'))) {
|
if ($q = trim((string) $request->input('q'))) {
|
||||||
$query->where(function ($w) use ($q) {
|
$query->where(function ($w) use ($q) {
|
||||||
$w->where('contracts.reference', 'ILIKE', "%{$q}%");
|
$w->where('contracts.reference', 'ILIKE', "%{$q}%");
|
||||||
@@ -321,6 +340,30 @@ public function contracts(Request $request, PhoneSelector $selector): \Illuminat
|
|||||||
->where('client_cases.client_id', $clientId);
|
->where('client_cases.client_id', $clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Date range filters for start_date
|
||||||
|
if ($startDateFrom = $request->input('start_date_from')) {
|
||||||
|
$query->where('contracts.start_date', '>=', $startDateFrom);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startDateTo = $request->input('start_date_to')) {
|
||||||
|
$query->where('contracts.start_date', '<=', $startDateTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date range filters for account.promise_date
|
||||||
|
$promiseDateFrom = $request->input('promise_date_from');
|
||||||
|
$promiseDateTo = $request->input('promise_date_to');
|
||||||
|
|
||||||
|
if ($promiseDateFrom || $promiseDateTo) {
|
||||||
|
$query->whereHas('account', function ($q) use ($promiseDateFrom, $promiseDateTo) {
|
||||||
|
if ($promiseDateFrom) {
|
||||||
|
$q->where('promise_date', '>=', $promiseDateFrom);
|
||||||
|
}
|
||||||
|
if ($promiseDateTo) {
|
||||||
|
$q->where('promise_date', '<=', $promiseDateTo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Optional phone filters
|
// Optional phone filters
|
||||||
if ($request->boolean('only_mobile') || $request->boolean('only_validated')) {
|
if ($request->boolean('only_mobile') || $request->boolean('only_validated')) {
|
||||||
$query->whereHas('clientCase.person.phones', function ($q) use ($request) {
|
$query->whereHas('clientCase.person.phones', function ($q) use ($request) {
|
||||||
@@ -345,6 +388,8 @@ public function contracts(Request $request, PhoneSelector $selector): \Illuminat
|
|||||||
'id' => $contract->id,
|
'id' => $contract->id,
|
||||||
'uuid' => $contract->uuid,
|
'uuid' => $contract->uuid,
|
||||||
'reference' => $contract->reference,
|
'reference' => $contract->reference,
|
||||||
|
'start_date' => $contract->start_date,
|
||||||
|
'promise_date' => $contract->account?->promise_date,
|
||||||
'case' => [
|
'case' => [
|
||||||
'id' => $contract->clientCase?->id,
|
'id' => $contract->clientCase?->id,
|
||||||
'uuid' => $contract->clientCase?->uuid,
|
'uuid' => $contract->clientCase?->uuid,
|
||||||
@@ -414,12 +459,12 @@ public function storeFromContracts(StorePackageFromContractsRequest $request, Ph
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$key = $phone->id ? 'id:'.$phone->id : 'num:'.$phone->nu;
|
$key = $phone->id ? 'id:'.$phone->id : 'num:'.$phone->nu;
|
||||||
if ($seen->contains($key)) {
|
/*if ($seen->contains($key)) {
|
||||||
// skip duplicates across multiple contracts/persons
|
// skip duplicates across multiple contracts/persons
|
||||||
$skipped++;
|
$skipped++;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}*/
|
||||||
$seen->push($key);
|
$seen->push($key);
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'number' => (string) $phone->nu,
|
'number' => (string) $phone->nu,
|
||||||
@@ -467,4 +512,47 @@ public function storeFromContracts(StorePackageFromContractsRequest $request, Ph
|
|||||||
|
|
||||||
return back()->with('success', 'Package created from contracts');
|
return back()->with('success', 'Package created from contracts');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flatten nested meta structure into dot-notation key-value pairs.
|
||||||
|
* Extracts 'value' from objects with {title, value, type} structure.
|
||||||
|
* Also creates direct access aliases for nested fields (skipping numeric keys).
|
||||||
|
*/
|
||||||
|
private function flattenMeta(array $meta, string $prefix = ''): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
foreach ($meta as $key => $value) {
|
||||||
|
$newKey = $prefix === '' ? $key : "{$prefix}.{$key}";
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
// Check if it's a structured meta entry with 'value' field
|
||||||
|
if (isset($value['value'])) {
|
||||||
|
$result[$newKey] = $value['value'];
|
||||||
|
// If parent key is numeric, also create direct alias without the number
|
||||||
|
if ($prefix !== '' && is_numeric($key)) {
|
||||||
|
$result[$key] = $value['value'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Recursively flatten nested arrays
|
||||||
|
$nested = $this->flattenMeta($value, $newKey);
|
||||||
|
$result = array_merge($result, $nested);
|
||||||
|
|
||||||
|
// If current key is numeric, also flatten without it for easier access
|
||||||
|
if (is_numeric($key)) {
|
||||||
|
$directNested = $this->flattenMeta($value, $prefix);
|
||||||
|
foreach ($directNested as $dk => $dv) {
|
||||||
|
// Only add if not already set (prefer first occurrence)
|
||||||
|
if (! isset($result[$dk])) {
|
||||||
|
$result[$dk] = $dv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$result[$newKey] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\StoreUserRequest;
|
||||||
use App\Models\Permission;
|
use App\Models\Permission;
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ public function index(Request $request): Response
|
|||||||
{
|
{
|
||||||
Gate::authorize('manage-settings');
|
Gate::authorize('manage-settings');
|
||||||
|
|
||||||
$users = User::with('roles:id,slug,name')->orderBy('name')->get(['id', 'name', 'email']);
|
$users = User::with('roles:id,slug,name')->orderBy('name')->get(['id', 'name', 'email', 'active']);
|
||||||
$roles = Role::with('permissions:id,slug,name')->orderBy('name')->get(['id', 'name', 'slug']);
|
$roles = Role::with('permissions:id,slug,name')->orderBy('name')->get(['id', 'name', 'slug']);
|
||||||
$permissions = Permission::orderBy('slug')->get(['id', 'name', 'slug']);
|
$permissions = Permission::orderBy('slug')->get(['id', 'name', 'slug']);
|
||||||
|
|
||||||
@@ -29,6 +31,23 @@ public function index(Request $request): Response
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function store(StoreUserRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'email' => $validated['email'],
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! empty($validated['roles'])) {
|
||||||
|
$user->roles()->sync($validated['roles']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', 'Uporabnik uspešno ustvarjen');
|
||||||
|
}
|
||||||
|
|
||||||
public function update(Request $request, User $user): RedirectResponse
|
public function update(Request $request, User $user): RedirectResponse
|
||||||
{
|
{
|
||||||
Gate::authorize('manage-settings');
|
Gate::authorize('manage-settings');
|
||||||
@@ -42,4 +61,16 @@ public function update(Request $request, User $user): RedirectResponse
|
|||||||
|
|
||||||
return back()->with('success', 'Roles updated');
|
return back()->with('success', 'Roles updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function toggleActive(User $user): RedirectResponse
|
||||||
|
{
|
||||||
|
Gate::authorize('manage-settings');
|
||||||
|
|
||||||
|
$user->active = ! $user->active;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$status = $user->active ? 'aktiviran' : 'deaktiviran';
|
||||||
|
|
||||||
|
return back()->with('success', "Uporabnik {$status}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
use App\Models\CaseObject;
|
use App\Models\CaseObject;
|
||||||
use App\Models\ClientCase;
|
use App\Models\ClientCase;
|
||||||
use App\Models\Contract;
|
use App\Models\Contract;
|
||||||
use Illuminate\Database\QueryException;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class CaseObjectController extends Controller
|
class CaseObjectController extends Controller
|
||||||
|
|||||||
@@ -252,11 +252,14 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
|||||||
'action_id' => 'exists:\App\Models\Action,id',
|
'action_id' => 'exists:\App\Models\Action,id',
|
||||||
'decision_id' => 'exists:\App\Models\Decision,id',
|
'decision_id' => 'exists:\App\Models\Decision,id',
|
||||||
'contract_uuid' => 'nullable|uuid',
|
'contract_uuid' => 'nullable|uuid',
|
||||||
|
'phone_view' => 'nullable|boolean',
|
||||||
'send_auto_mail' => 'sometimes|boolean',
|
'send_auto_mail' => 'sometimes|boolean',
|
||||||
'attachment_document_ids' => 'sometimes|array',
|
'attachment_document_ids' => 'sometimes|array',
|
||||||
'attachment_document_ids.*' => 'integer',
|
'attachment_document_ids.*' => 'integer',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$isPhoneView = $attributes['phone_view'] ?? false;
|
||||||
|
|
||||||
// Map contract_uuid to contract_id within the same client case, if provided
|
// Map contract_uuid to contract_id within the same client case, if provided
|
||||||
$contractId = null;
|
$contractId = null;
|
||||||
if (! empty($attributes['contract_uuid'])) {
|
if (! empty($attributes['contract_uuid'])) {
|
||||||
@@ -279,10 +282,23 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
|||||||
'decision_id' => $attributes['decision_id'],
|
'decision_id' => $attributes['decision_id'],
|
||||||
'contract_id' => $contractId,
|
'contract_id' => $contractId,
|
||||||
]);
|
]);
|
||||||
/*foreach ($activity->decision->events as $e) {
|
|
||||||
$class = '\\App\\Events\\' . $e->name;
|
if ($isPhoneView && $contractId) {
|
||||||
event(new $class($clientCase));
|
$fieldJob = $contract->fieldJobs()
|
||||||
}*/
|
->whereNull('completed_at')
|
||||||
|
->whereNull('cancelled_at')
|
||||||
|
->where('assigned_user_id', \Auth::id())
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($fieldJob) {
|
||||||
|
$fieldJob->update([
|
||||||
|
'added_activity' => true,
|
||||||
|
'last_activity' => $row->created_at,
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger()->info('Activity successfully inserted', $attributes);
|
logger()->info('Activity successfully inserted', $attributes);
|
||||||
|
|
||||||
@@ -297,8 +313,8 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
|||||||
->values();
|
->values();
|
||||||
$validAttachmentIds = collect();
|
$validAttachmentIds = collect();
|
||||||
if ($attachmentIds->isNotEmpty() && $contractId) {
|
if ($attachmentIds->isNotEmpty() && $contractId) {
|
||||||
$validAttachmentIds = \App\Models\Document::query()
|
$validAttachmentIds = Document::query()
|
||||||
->where('documentable_type', \App\Models\Contract::class)
|
->where('documentable_type', Contract::class)
|
||||||
->where('documentable_id', $contractId)
|
->where('documentable_id', $contractId)
|
||||||
->whereIn('id', $attachmentIds)
|
->whereIn('id', $attachmentIds)
|
||||||
->pluck('id');
|
->pluck('id');
|
||||||
@@ -396,6 +412,21 @@ public function updateContractSegment(ClientCase $clientCase, string $uuid, Requ
|
|||||||
return back()->with('success', 'Contract segment updated.');
|
return back()->with('success', 'Contract segment updated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function patchContractMeta(ClientCase $clientCase, string $uuid, Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'meta' => ['required', 'array'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$contract = $clientCase->contracts()->where('uuid', $uuid)->firstOrFail();
|
||||||
|
|
||||||
|
$contract->update([
|
||||||
|
'meta' => $validated['meta'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', __('Meta podatki so bili posodobljeni.'));
|
||||||
|
}
|
||||||
|
|
||||||
public function attachSegment(ClientCase $clientCase, Request $request)
|
public function attachSegment(ClientCase $clientCase, Request $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
@@ -1443,178 +1474,265 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r
|
|||||||
{
|
{
|
||||||
$contract = Contract::query()->where('uuid', $uuid)->firstOrFail();
|
$contract = Contract::query()->where('uuid', $uuid)->firstOrFail();
|
||||||
if ($contract->client_case_id !== $clientCase->id) {
|
if ($contract->client_case_id !== $clientCase->id) {
|
||||||
|
\Log::warning('Contract not found uuid: {uuid}', ['uuid' => $uuid]);
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
$reactivateRequested = (bool) $request->boolean('reactivate');
|
|
||||||
// Determine applicable settings based on intent (archive vs reactivate)
|
$attr = $request->validate([
|
||||||
if ($reactivateRequested) {
|
'reactivate' => 'boolean',
|
||||||
$latestReactivate = \App\Models\ArchiveSetting::query()
|
]);
|
||||||
|
|
||||||
|
$reactivate = $attr['reactivate'] ?? false;
|
||||||
|
|
||||||
|
$setting = \App\Models\ArchiveSetting::query()
|
||||||
->where('enabled', true)
|
->where('enabled', true)
|
||||||
->where('reactivate', true)
|
|
||||||
->whereIn('strategy', ['immediate', 'manual'])
|
->whereIn('strategy', ['immediate', 'manual'])
|
||||||
|
->where('reactivate', $reactivate)
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->first();
|
->first();
|
||||||
if (! $latestReactivate) {
|
|
||||||
return back()->with('warning', __('contracts.reactivate_not_allowed'));
|
if (! $setting->exists()) {
|
||||||
}
|
\Log::warning('No archive settings found!');
|
||||||
$settings = collect([$latestReactivate]);
|
|
||||||
$hasReactivateRule = true;
|
return back()->with('warning', 'No settings found');
|
||||||
} else {
|
|
||||||
$settings = \App\Models\ArchiveSetting::query()
|
|
||||||
->where('enabled', true)
|
|
||||||
->whereIn('strategy', ['immediate', 'manual'])
|
|
||||||
->where(function ($q) { // exclude reactivate-only rules from archive run
|
|
||||||
$q->whereNull('reactivate')->orWhere('reactivate', false);
|
|
||||||
})
|
|
||||||
->get();
|
|
||||||
if ($settings->isEmpty()) {
|
|
||||||
return back()->with('warning', __('contracts.no_archive_settings'));
|
|
||||||
}
|
|
||||||
$hasReactivateRule = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Service archive executor
|
||||||
$executor = app(\App\Services\Archiving\ArchiveExecutor::class);
|
$executor = app(\App\Services\Archiving\ArchiveExecutor::class);
|
||||||
|
$result = null;
|
||||||
|
|
||||||
$context = [
|
$context = [
|
||||||
'contract_id' => $contract->id,
|
'contract_id' => $contract->id,
|
||||||
'client_case_id' => $clientCase->id,
|
'client_case_id' => $clientCase->id,
|
||||||
|
'account_id' => $contract->account->id ?? null,
|
||||||
];
|
];
|
||||||
if ($contract->account) {
|
|
||||||
$context['account_id'] = $contract->account->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
$overall = [];
|
|
||||||
$hadAnyEffect = false;
|
|
||||||
foreach ($settings as $setting) {
|
|
||||||
|
|
||||||
$res = $executor->executeSetting($setting, $context, optional($request->user())->id);
|
|
||||||
foreach ($res as $table => $count) {
|
|
||||||
$overall[$table] = ($overall[$table] ?? 0) + $count;
|
|
||||||
if ($count > 0) {
|
|
||||||
$hadAnyEffect = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($reactivateRequested && $hasReactivateRule) {
|
|
||||||
// Reactivation path: ensure contract becomes active and soft-delete cleared.
|
|
||||||
if ($contract->active == 0 || $contract->deleted_at) {
|
|
||||||
$contract->forceFill(['active' => 1, 'deleted_at' => null])->save();
|
|
||||||
$overall['contracts_reactivated'] = ($overall['contracts_reactivated'] ?? 0) + 1;
|
|
||||||
$hadAnyEffect = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Ensure the contract itself is archived even if rule conditions would have excluded it
|
|
||||||
if (! empty($contract->getAttributes()) && $contract->active) {
|
|
||||||
if (! array_key_exists('contracts', $overall)) {
|
|
||||||
$contract->update(['active' => 0]);
|
|
||||||
$overall['contracts'] = ($overall['contracts'] ?? 0) + 1;
|
|
||||||
} else {
|
|
||||||
$contract->refresh();
|
|
||||||
}
|
|
||||||
$hadAnyEffect = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an Activity record logging this archive if an action or decision is tied to any setting
|
|
||||||
if ($hadAnyEffect) {
|
|
||||||
$activitySetting = $settings->first(fn ($s) => ! is_null($s->action_id) || ! is_null($s->decision_id));
|
|
||||||
if ($activitySetting) {
|
|
||||||
try {
|
try {
|
||||||
if ($reactivateRequested) {
|
$result = $executor->executeSetting($setting, $context, \Auth::id());
|
||||||
$note = 'Ponovna aktivacija pogodba '.$contract->reference;
|
} catch (Exception $e) {
|
||||||
} else {
|
\Log::error('There was an error executing ArchiveExecutor::executeSetting {msg}', ['msg' => $e->getMessage()]);
|
||||||
$noteKey = 'contracts.archived_activity_note';
|
|
||||||
$note = __($noteKey, ['reference' => $contract->reference]);
|
return back()->with('warning', 'Something went wrong!');
|
||||||
if ($note === $noteKey) {
|
|
||||||
$note = \Illuminate\Support\Facades\Lang::get($noteKey, ['reference' => $contract->reference], 'sl');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
\DB::transaction(function () use ($contract, $clientCase, $setting, $reactivate) {
|
||||||
|
// Create an Activity record logging this archive if an action or decision is tied to any setting
|
||||||
|
if ($setting->action_id && $setting->decision_id) {
|
||||||
$activityData = [
|
$activityData = [
|
||||||
'client_case_id' => $clientCase->id,
|
'client_case_id' => $clientCase->id,
|
||||||
'action_id' => $activitySetting->action_id,
|
'action_id' => $setting->action_id,
|
||||||
'decision_id' => $activitySetting->decision_id,
|
'decision_id' => $setting->decision_id,
|
||||||
'note' => $note,
|
'note' => ($reactivate)
|
||||||
'active' => 1,
|
? "Ponovno aktivirana pogodba $contract->reference"
|
||||||
'user_id' => optional($request->user())->id,
|
: "Arhivirana pogodba $contract->reference",
|
||||||
];
|
];
|
||||||
if ($reactivateRequested) {
|
|
||||||
// Attach the contract_id when reactivated as per requirement
|
|
||||||
$activityData['contract_id'] = $contract->id;
|
|
||||||
}
|
|
||||||
\App\Models\Activity::create($activityData);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
logger()->warning('Failed to create archive/reactivate activity', [
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'contract_id' => $contract->id,
|
|
||||||
'setting_id' => optional($activitySetting)->id,
|
|
||||||
'reactivate' => $reactivateRequested,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If any archive setting specifies a segment_id, move the contract to that segment (archive bucket)
|
|
||||||
$segmentSetting = $settings->first(fn ($s) => ! is_null($s->segment_id)); // for reactivation this is the single reactivation setting if segment specified
|
|
||||||
if ($segmentSetting && $segmentSetting->segment_id) {
|
|
||||||
try {
|
try {
|
||||||
$segmentId = $segmentSetting->segment_id;
|
\App\Models\Activity::create($activityData);
|
||||||
\DB::transaction(function () use ($contract, $segmentId, $clientCase) {
|
} catch (Exception $e) {
|
||||||
// Ensure the segment is attached to the client case (activate if previously inactive)
|
\Log::warning('Activity could not be created!');
|
||||||
$casePivot = \DB::table('client_case_segment')
|
|
||||||
->where('client_case_id', $clientCase->id)
|
|
||||||
->where('segment_id', $segmentId)
|
|
||||||
->first();
|
|
||||||
if (! $casePivot) {
|
|
||||||
\DB::table('client_case_segment')->insert([
|
|
||||||
'client_case_id' => $clientCase->id,
|
|
||||||
'segment_id' => $segmentId,
|
|
||||||
'active' => true,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
} elseif (! $casePivot->active) {
|
|
||||||
\DB::table('client_case_segment')
|
|
||||||
->where('id', $casePivot->id)
|
|
||||||
->update(['active' => true, 'updated_at' => now()]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deactivate all current active contract segments
|
}
|
||||||
\DB::table('contract_segment')
|
|
||||||
->where('contract_id', $contract->id)
|
|
||||||
->where('active', true)
|
|
||||||
->update(['active' => false, 'updated_at' => now()]);
|
|
||||||
|
|
||||||
// Attach or activate the archive segment for this contract
|
// If any archive setting specifies a segment_id, move the contract to that segment (archive bucket)
|
||||||
$existing = \DB::table('contract_segment')
|
if ($setting->segment_id) {
|
||||||
->where('contract_id', $contract->id)
|
$segmentId = $setting->segment_id;
|
||||||
->where('segment_id', $segmentId)
|
|
||||||
->first();
|
$contract->segments()
|
||||||
if ($existing) {
|
->allRelatedIds()
|
||||||
\DB::table('contract_segment')
|
->map(fn (int $val, int|string $key) => $contract->segments()->updateExistingPivot($val, [
|
||||||
->where('id', $existing->id)
|
'active' => false,
|
||||||
->update(['active' => true, 'updated_at' => now()]);
|
'updated_at' => now(),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($contract->attachedSegments()->find($segmentId)->pluck('id')->isNotEmpty()) {
|
||||||
|
$contract->attachedSegments()->updateExistingPivot($segmentId, [
|
||||||
|
'active' => true,
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
\DB::table('contract_segment')->insert([
|
$contract->segments()->attach(
|
||||||
|
$segmentId,
|
||||||
|
[
|
||||||
|
'active' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$contract->fieldJobs()
|
||||||
|
->whereNull('completed_at')
|
||||||
|
->whereNull('cancelled_at')
|
||||||
|
->update([
|
||||||
|
'cancelled_at' => date('Y-m-d'),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
} catch (Exception $e) {
|
||||||
|
\Log::warning('Something went wrong with inserting / updating archive setting partials!');
|
||||||
|
|
||||||
|
return back()->with('warning', 'Something went wrong!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', $reactivate
|
||||||
|
? __('contracts.reactivated')
|
||||||
|
: __('contracts.archived')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive multiple contracts in a batch operation
|
||||||
|
*/
|
||||||
|
public function archiveBatch(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'contracts' => 'required|array',
|
||||||
|
'contracts.*' => 'required|uuid|exists:contracts,uuid',
|
||||||
|
'reactivate' => 'boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$reactivate = $validated['reactivate'] ?? false;
|
||||||
|
|
||||||
|
// Get archive setting
|
||||||
|
$setting = \App\Models\ArchiveSetting::query()
|
||||||
|
->where('enabled', true)
|
||||||
|
->whereIn('strategy', ['immediate', 'manual'])
|
||||||
|
->where('reactivate', $reactivate)
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $setting) {
|
||||||
|
\Log::warning('No archive settings found for batch archive');
|
||||||
|
return back()->with('flash', [
|
||||||
|
'error' => 'No archive settings found',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$executor = app(\App\Services\Archiving\ArchiveExecutor::class);
|
||||||
|
$successCount = 0;
|
||||||
|
$skippedCount = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($validated['contracts'] as $contractUuid) {
|
||||||
|
try {
|
||||||
|
$contract = Contract::where('uuid', $contractUuid)->firstOrFail();
|
||||||
|
|
||||||
|
// Skip if contract is already archived (active = 0)
|
||||||
|
if (!$contract->active) {
|
||||||
|
$skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientCase = $contract->clientCase;
|
||||||
|
|
||||||
|
$context = [
|
||||||
'contract_id' => $contract->id,
|
'contract_id' => $contract->id,
|
||||||
'segment_id' => $segmentId,
|
'client_case_id' => $clientCase->id,
|
||||||
|
'account_id' => $contract->account->id ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Execute archive setting
|
||||||
|
$executor->executeSetting($setting, $context, \Auth::id());
|
||||||
|
|
||||||
|
// Transaction for segment updates and activity logging
|
||||||
|
\DB::transaction(function () use ($contract, $clientCase, $setting, $reactivate) {
|
||||||
|
// Create activity log
|
||||||
|
if ($setting->action_id && $setting->decision_id) {
|
||||||
|
$activityData = [
|
||||||
|
'client_case_id' => $clientCase->id,
|
||||||
|
'action_id' => $setting->action_id,
|
||||||
|
'decision_id' => $setting->decision_id,
|
||||||
|
'note' => ($reactivate)
|
||||||
|
? "Ponovno aktivirana pogodba $contract->reference"
|
||||||
|
: "Arhivirana pogodba $contract->reference",
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
\App\Models\Activity::create($activityData);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
\Log::warning('Activity could not be created during batch archive');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to archive segment if specified
|
||||||
|
if ($setting->segment_id) {
|
||||||
|
$segmentId = $setting->segment_id;
|
||||||
|
|
||||||
|
// Deactivate all current segments
|
||||||
|
$contract->segments()
|
||||||
|
->allRelatedIds()
|
||||||
|
->map(fn (int $val) => $contract->segments()->updateExistingPivot($val, [
|
||||||
|
'active' => false,
|
||||||
|
'updated_at' => now(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Activate archive segment
|
||||||
|
if ($contract->attachedSegments()->find($segmentId)->pluck('id')->isNotEmpty()) {
|
||||||
|
$contract->attachedSegments()->updateExistingPivot($segmentId, [
|
||||||
|
'active' => true,
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$contract->segments()->attach($segmentId, [
|
||||||
'active' => true,
|
'active' => true,
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel pending field jobs
|
||||||
|
$contract->fieldJobs()
|
||||||
|
->whereNull('completed_at')
|
||||||
|
->whereNull('cancelled_at')
|
||||||
|
->update([
|
||||||
|
'cancelled_at' => date('Y-m-d'),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
} catch (\Throwable $e) {
|
|
||||||
logger()->warning('Failed to move contract to archive segment', [
|
$successCount++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
\Log::error('Error archiving contract in batch', [
|
||||||
|
'uuid' => $contractUuid,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
'contract_id' => $contract->id,
|
]);
|
||||||
'segment_id' => $segmentSetting->segment_id,
|
$errors[] = [
|
||||||
'setting_id' => $segmentSetting->id,
|
'uuid' => $contractUuid,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($errors) > 0) {
|
||||||
|
$message = "Archived $successCount contracts";
|
||||||
|
if ($skippedCount > 0) {
|
||||||
|
$message .= ", skipped $skippedCount already archived";
|
||||||
|
}
|
||||||
|
$message .= ", " . count($errors) . " failed";
|
||||||
|
|
||||||
|
return back()->with('flash', [
|
||||||
|
'error' => $message,
|
||||||
|
'details' => $errors,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$message = $reactivate
|
||||||
|
? "Successfully reactivated $successCount contracts"
|
||||||
|
: "Successfully archived $successCount contracts";
|
||||||
|
|
||||||
|
if ($skippedCount > 0) {
|
||||||
|
$message .= " ($skippedCount already archived)";
|
||||||
}
|
}
|
||||||
|
|
||||||
$message = $reactivateRequested ? __('contracts.reactivated') : __('contracts.archived');
|
return back()->with('flash', [
|
||||||
|
'success' => $message,
|
||||||
return back()->with('success', $message);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1836,7 +1954,7 @@ public function listContracts(ClientCase $clientCase)
|
|||||||
{
|
{
|
||||||
$contracts = $clientCase->contracts()
|
$contracts = $clientCase->contracts()
|
||||||
->with('account.type')
|
->with('account.type')
|
||||||
->select('id', 'uuid', 'reference', 'active', 'start_date', 'end_date')
|
->select('id', 'uuid', 'reference', 'active', 'start_date', 'end_date', 'meta')
|
||||||
->latest('id')
|
->latest('id')
|
||||||
->get()
|
->get()
|
||||||
->map(function ($c) {
|
->map(function ($c) {
|
||||||
@@ -1852,6 +1970,7 @@ public function listContracts(ClientCase $clientCase)
|
|||||||
'active' => (bool) $c->active,
|
'active' => (bool) $c->active,
|
||||||
'start_date' => (string) ($c->start_date ?? ''),
|
'start_date' => (string) ($c->start_date ?? ''),
|
||||||
'end_date' => (string) ($c->end_date ?? ''),
|
'end_date' => (string) ($c->end_date ?? ''),
|
||||||
|
'meta' => is_array($c->meta) && ! empty($c->meta) ? $this->flattenMeta($c->meta) : null,
|
||||||
'account' => $acc ? [
|
'account' => $acc ? [
|
||||||
'reference' => $acc->reference,
|
'reference' => $acc->reference,
|
||||||
'type' => $acc->type?->name,
|
'type' => $acc->type?->name,
|
||||||
@@ -1894,6 +2013,10 @@ public function previewSms(ClientCase $clientCase, Request $request, SmsService
|
|||||||
'start_date' => (string) ($contract->start_date ?? ''),
|
'start_date' => (string) ($contract->start_date ?? ''),
|
||||||
'end_date' => (string) ($contract->end_date ?? ''),
|
'end_date' => (string) ($contract->end_date ?? ''),
|
||||||
];
|
];
|
||||||
|
// Include contract.meta as flattened key-value pairs
|
||||||
|
if (is_array($contract->meta) && ! empty($contract->meta)) {
|
||||||
|
$vars['contract']['meta'] = $this->flattenMeta($contract->meta);
|
||||||
|
}
|
||||||
if ($contract->account) {
|
if ($contract->account) {
|
||||||
$initialRaw = (string) $contract->account->initial_amount;
|
$initialRaw = (string) $contract->account->initial_amount;
|
||||||
$balanceRaw = (string) $contract->account->balance_amount;
|
$balanceRaw = (string) $contract->account->balance_amount;
|
||||||
@@ -1917,4 +2040,47 @@ public function previewSms(ClientCase $clientCase, Request $request, SmsService
|
|||||||
'variables' => $vars,
|
'variables' => $vars,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flatten nested meta structure into dot-notation key-value pairs.
|
||||||
|
* Extracts 'value' from objects with {title, value, type} structure.
|
||||||
|
* Also creates direct access aliases for nested fields (skipping numeric keys).
|
||||||
|
*/
|
||||||
|
private function flattenMeta(array $meta, string $prefix = ''): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
foreach ($meta as $key => $value) {
|
||||||
|
$newKey = $prefix === '' ? $key : "{$prefix}.{$key}";
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
// Check if it's a structured meta entry with 'value' field
|
||||||
|
if (isset($value['value'])) {
|
||||||
|
$result[$newKey] = $value['value'];
|
||||||
|
// If parent key is numeric, also create direct alias without the number
|
||||||
|
if ($prefix !== '' && is_numeric($key)) {
|
||||||
|
$result[$key] = $value['value'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Recursively flatten nested arrays
|
||||||
|
$nested = $this->flattenMeta($value, $newKey);
|
||||||
|
$result = array_merge($result, $nested);
|
||||||
|
|
||||||
|
// If current key is numeric, also flatten without it for easier access
|
||||||
|
if (is_numeric($key)) {
|
||||||
|
$directNested = $this->flattenMeta($value, $prefix);
|
||||||
|
foreach ($directNested as $dk => $dv) {
|
||||||
|
// Only add if not already set (prefer first occurrence)
|
||||||
|
if (! isset($result[$dk])) {
|
||||||
|
$result[$dk] = $dv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$result[$newKey] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Exports\ClientContractsExport;
|
||||||
|
use App\Http\Requests\ExportClientContractsRequest;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use DB;
|
use DB;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
class ClientController extends Controller
|
class ClientController extends Controller
|
||||||
{
|
{
|
||||||
@@ -118,7 +122,8 @@ public function contracts(Client $client, Request $request)
|
|||||||
$from = $request->input('from');
|
$from = $request->input('from');
|
||||||
$to = $request->input('to');
|
$to = $request->input('to');
|
||||||
$search = $request->input('search');
|
$search = $request->input('search');
|
||||||
$segmentId = $request->input('segment');
|
$segmentsParam = $request->input('segments');
|
||||||
|
$segmentIds = $segmentsParam ? array_filter(explode(',', $segmentsParam)) : [];
|
||||||
|
|
||||||
$contractsQuery = \App\Models\Contract::query()
|
$contractsQuery = \App\Models\Contract::query()
|
||||||
->whereHas('clientCase', function ($q) use ($client) {
|
->whereHas('clientCase', function ($q) use ($client) {
|
||||||
@@ -127,6 +132,7 @@ public function contracts(Client $client, Request $request)
|
|||||||
->with([
|
->with([
|
||||||
'clientCase:id,uuid,person_id',
|
'clientCase:id,uuid,person_id',
|
||||||
'clientCase.person:id,full_name',
|
'clientCase.person:id,full_name',
|
||||||
|
'clientCase.person.address',
|
||||||
'segments' => function ($q) {
|
'segments' => function ($q) {
|
||||||
$q->wherePivot('active', true)->select('segments.id', 'segments.name');
|
$q->wherePivot('active', true)->select('segments.id', 'segments.name');
|
||||||
},
|
},
|
||||||
@@ -150,9 +156,9 @@ public function contracts(Client $client, Request $request)
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
->when($segmentId, function ($q) use ($segmentId) {
|
->when($segmentIds, function ($q) use ($segmentIds) {
|
||||||
$q->whereHas('segments', function ($s) use ($segmentId) {
|
$q->whereHas('segments', function ($s) use ($segmentIds) {
|
||||||
$s->where('segments.id', $segmentId)
|
$s->whereIn('segments.id', $segmentIds)
|
||||||
->where('contract_segment.active', true);
|
->where('contract_segment.active', true);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -168,12 +174,90 @@ public function contracts(Client $client, Request $request)
|
|||||||
return Inertia::render('Client/Contracts', [
|
return Inertia::render('Client/Contracts', [
|
||||||
'client' => $data,
|
'client' => $data,
|
||||||
'contracts' => $contractsQuery->paginate($request->integer('perPage', 20))->withQueryString(),
|
'contracts' => $contractsQuery->paginate($request->integer('perPage', 20))->withQueryString(),
|
||||||
'filters' => $request->only(['from', 'to', 'search', 'segment']),
|
'filters' => $request->only(['from', 'to', 'search', 'segments']),
|
||||||
'segments' => $segments,
|
'segments' => $segments,
|
||||||
'types' => $types,
|
'types' => $types,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function exportContracts(ExportClientContractsRequest $request, Client $client)
|
||||||
|
{
|
||||||
|
$data = $request->validated();
|
||||||
|
$columns = array_values(array_unique($data['columns']));
|
||||||
|
|
||||||
|
$from = $data['from'] ?? null;
|
||||||
|
$to = $data['to'] ?? null;
|
||||||
|
$search = $data['search'] ?? null;
|
||||||
|
$segmentsParam = $data['segments'] ?? null;
|
||||||
|
$segmentIds = $segmentsParam ? array_filter(explode(',', $segmentsParam)) : [];
|
||||||
|
|
||||||
|
$query = \App\Models\Contract::query()
|
||||||
|
->whereHas('clientCase', function ($q) use ($client) {
|
||||||
|
$q->where('client_id', $client->id);
|
||||||
|
})
|
||||||
|
->with([
|
||||||
|
'clientCase:id,uuid,person_id',
|
||||||
|
'clientCase.person:id,full_name',
|
||||||
|
'clientCase.person.address',
|
||||||
|
'segments' => function ($q) {
|
||||||
|
$q->wherePivot('active', true)->select('segments.id', 'segments.name');
|
||||||
|
},
|
||||||
|
'account:id,accounts.contract_id,balance_amount',
|
||||||
|
])
|
||||||
|
->select(['id', 'uuid', 'reference', 'start_date', 'client_case_id'])
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->when($from || $to, function ($q) use ($from, $to) {
|
||||||
|
if (! empty($from)) {
|
||||||
|
$q->whereDate('start_date', '>=', $from);
|
||||||
|
}
|
||||||
|
if (! empty($to)) {
|
||||||
|
$q->whereDate('start_date', '<=', $to);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->when($search, function ($q) use ($search) {
|
||||||
|
$q->where(function ($inner) use ($search) {
|
||||||
|
$inner->where('reference', 'ilike', '%'.$search.'%')
|
||||||
|
->orWhereHas('clientCase.person', function ($p) use ($search) {
|
||||||
|
$p->where('full_name', 'ilike', '%'.$search.'%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($segmentIds, function ($q) use ($segmentIds) {
|
||||||
|
$q->whereHas('segments', function ($s) use ($segmentIds) {
|
||||||
|
$s->whereIn('segments.id', $segmentIds)
|
||||||
|
->where('contract_segment.active', true);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orderByDesc('start_date');
|
||||||
|
|
||||||
|
if (($data['scope'] ?? ExportClientContractsRequest::SCOPE_ALL) === ExportClientContractsRequest::SCOPE_CURRENT) {
|
||||||
|
$page = max(1, (int) ($data['page'] ?? 1));
|
||||||
|
$perPage = max(1, min(200, (int) ($data['per_page'] ?? 15)));
|
||||||
|
$query->forPage($page, $perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = $this->buildExportFilename($client);
|
||||||
|
|
||||||
|
return Excel::download(new ClientContractsExport($query, $columns), $filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildExportFilename(Client $client): string
|
||||||
|
{
|
||||||
|
$datePrefix = now()->format('dmy');
|
||||||
|
$clientName = $this->slugify($client->person?->full_name ?? 'stranka');
|
||||||
|
|
||||||
|
return sprintf('%s_%s-Pogodbe.xlsx', $datePrefix, $clientName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function slugify(?string $value): string
|
||||||
|
{
|
||||||
|
if (empty($value)) {
|
||||||
|
return 'data';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Str::slug($value, '-') ?: 'data';
|
||||||
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ public function update(ContractConfig $config, Request $request)
|
|||||||
public function destroy(ContractConfig $config)
|
public function destroy(ContractConfig $config)
|
||||||
{
|
{
|
||||||
$config->delete();
|
$config->delete();
|
||||||
|
|
||||||
return back()->with('success', 'Configuration deleted');
|
return back()->with('success', 'Configuration deleted');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,28 @@
|
|||||||
|
|
||||||
use App\Models\Contract;
|
use App\Models\Contract;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
|
||||||
class ContractController extends Controller
|
class ContractController extends Controller
|
||||||
{
|
{
|
||||||
|
public function index(Contract $contract)
|
||||||
public function index(Contract $contract) {
|
{
|
||||||
return Inertia::render('Contract/Index', [
|
return Inertia::render('Contract/Index', [
|
||||||
'contracts' => $contract::with(['type', 'debtor'])
|
'contracts' => $contract::with(['type', 'debtor'])
|
||||||
->where('active', 1)
|
->where('active', 1)
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->paginate(10),
|
->paginate(10),
|
||||||
'person_types' => \App\Models\Person\PersonType::all(['id', 'name', 'description'])
|
'person_types' => \App\Models\Person\PersonType::all(['id', 'name', 'description'])
|
||||||
->where('deleted', 0)
|
->where('deleted', 0),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(Contract $contract){
|
public function show(Contract $contract)
|
||||||
|
{
|
||||||
return inertia('Contract/Show', [
|
return inertia('Contract/Show', [
|
||||||
'contract' => $contract::with(['type', 'client', 'debtor'])->findOrFail($contract->id)
|
'contract' => $contract::with(['type', 'client', 'debtor'])->findOrFail($contract->id),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ public function store(Request $request)
|
|||||||
$clientCase->contracts()->create([
|
$clientCase->contracts()->create([
|
||||||
'reference' => $request->input('reference'),
|
'reference' => $request->input('reference'),
|
||||||
'start_date' => date('Y-m-d', strtotime($request->input('start_date'))),
|
'start_date' => date('Y-m-d', strtotime($request->input('start_date'))),
|
||||||
'type_id' => $request->input('type_id')
|
'type_id' => $request->input('type_id'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -50,12 +52,79 @@ public function store(Request $request)
|
|||||||
return to_route('clientCase.show', $clientCase);
|
return to_route('clientCase.show', $clientCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Contract $contract, Request $request){
|
public function update(Contract $contract, Request $request)
|
||||||
|
{
|
||||||
$contract->update([
|
$contract->update([
|
||||||
'referenca' => $request->input('referenca'),
|
'referenca' => $request->input('referenca'),
|
||||||
'type_id' => $request->input('type_id')
|
'type_id' => $request->input('type_id'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function segment(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'segment_id' => ['required', 'integer', Rule::exists('segments', 'id')->where('active', true)],
|
||||||
|
'contracts' => ['required', 'array', 'min:1'],
|
||||||
|
'contracts.*' => ['string', Rule::exists('contracts', 'uuid')],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$segmentId = (int) $data['segment_id'];
|
||||||
|
$uuids = array_values($data['contracts']);
|
||||||
|
|
||||||
|
$contracts = Contract::query()
|
||||||
|
->whereIn('uuid', $uuids)
|
||||||
|
->get(['id', 'client_case_id']);
|
||||||
|
|
||||||
|
DB::transaction(function () use ($contracts, $segmentId) {
|
||||||
|
foreach ($contracts as $contract) {
|
||||||
|
// Ensure the segment is attached to the client case and active
|
||||||
|
$attached = DB::table('client_case_segment')
|
||||||
|
->where('client_case_id', $contract->client_case_id)
|
||||||
|
->where('segment_id', $segmentId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $attached) {
|
||||||
|
DB::table('client_case_segment')->insert([
|
||||||
|
'client_case_id' => $contract->client_case_id,
|
||||||
|
'segment_id' => $segmentId,
|
||||||
|
'active' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
} elseif (! $attached->active) {
|
||||||
|
DB::table('client_case_segment')
|
||||||
|
->where('id', $attached->id)
|
||||||
|
->update(['active' => true, 'updated_at' => now()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate all current contract segments
|
||||||
|
DB::table('contract_segment')
|
||||||
|
->where('contract_id', $contract->id)
|
||||||
|
->update(['active' => false, 'updated_at' => now()]);
|
||||||
|
|
||||||
|
// Activate or attach the target segment
|
||||||
|
$pivot = DB::table('contract_segment')
|
||||||
|
->where('contract_id', $contract->id)
|
||||||
|
->where('segment_id', $segmentId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($pivot) {
|
||||||
|
DB::table('contract_segment')
|
||||||
|
->where('id', $pivot->id)
|
||||||
|
->update(['active' => true, 'updated_at' => now()]);
|
||||||
|
} else {
|
||||||
|
DB::table('contract_segment')->insert([
|
||||||
|
'contract_id' => $contract->id,
|
||||||
|
'segment_id' => $segmentId,
|
||||||
|
'active' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return back()->with('success', __('Pogodbe so bile preusmerjene v izbrani segment.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class DebtController extends Controller
|
class DebtController extends Controller
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ public function index()
|
|||||||
'fields' => ['reference', 'balance_amount', 'contract_id', 'contract_reference', 'type_id', 'active', 'description'],
|
'fields' => ['reference', 'balance_amount', 'contract_id', 'contract_reference', 'type_id', 'active', 'description'],
|
||||||
'ui' => ['order' => 6],
|
'ui' => ['order' => 6],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => 'activities',
|
||||||
|
'canonical_root' => 'activity',
|
||||||
|
'label' => 'Activities',
|
||||||
|
'fields' => ['note', 'due_date', 'amount', 'action_id', 'decision_id', 'contract_id', 'client_case_id', 'user_id'],
|
||||||
|
'ui' => ['order' => 7],
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Ensure fields are arrays for frontend consumption
|
// Ensure fields are arrays for frontend consumption
|
||||||
|
|||||||
@@ -111,10 +111,10 @@ public function store(Request $request)
|
|||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
'reactivate' => 'boolean',
|
'reactivate' => 'boolean',
|
||||||
'entities' => 'nullable|array',
|
'entities' => 'nullable|array',
|
||||||
'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
|
'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments,activities',
|
||||||
'mappings' => 'array',
|
'mappings' => 'array',
|
||||||
'mappings.*.source_column' => 'required|string',
|
'mappings.*.source_column' => 'required|string',
|
||||||
'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
|
'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments,activities',
|
||||||
'mappings.*.target_field' => 'nullable|string',
|
'mappings.*.target_field' => 'nullable|string',
|
||||||
'mappings.*.transform' => 'nullable|string|max:50',
|
'mappings.*.transform' => 'nullable|string|max:50',
|
||||||
'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||||
@@ -124,7 +124,11 @@ public function store(Request $request)
|
|||||||
'meta.segment_id' => 'nullable|integer|exists:segments,id',
|
'meta.segment_id' => 'nullable|integer|exists:segments,id',
|
||||||
'meta.decision_id' => 'nullable|integer|exists:decisions,id',
|
'meta.decision_id' => 'nullable|integer|exists:decisions,id',
|
||||||
'meta.action_id' => 'nullable|integer|exists:actions,id',
|
'meta.action_id' => 'nullable|integer|exists:actions,id',
|
||||||
|
'meta.activity_action_id' => 'nullable|integer|exists:actions,id',
|
||||||
|
'meta.activity_decision_id' => 'nullable|integer|exists:decisions,id',
|
||||||
|
'meta.activity_created_at' => 'nullable|date',
|
||||||
'meta.payments_import' => 'nullable|boolean',
|
'meta.payments_import' => 'nullable|boolean',
|
||||||
|
'meta.history_import' => 'nullable|boolean',
|
||||||
'meta.contract_key_mode' => 'nullable|string|in:reference',
|
'meta.contract_key_mode' => 'nullable|string|in:reference',
|
||||||
])->validate();
|
])->validate();
|
||||||
|
|
||||||
@@ -142,7 +146,28 @@ public function store(Request $request)
|
|||||||
$template = null;
|
$template = null;
|
||||||
DB::transaction(function () use (&$template, $request, $data) {
|
DB::transaction(function () use (&$template, $request, $data) {
|
||||||
$paymentsImport = (bool) (data_get($data, 'meta.payments_import') ?? false);
|
$paymentsImport = (bool) (data_get($data, 'meta.payments_import') ?? false);
|
||||||
|
$historyImport = (bool) (data_get($data, 'meta.history_import') ?? false);
|
||||||
$entities = $data['entities'] ?? [];
|
$entities = $data['entities'] ?? [];
|
||||||
|
if ($historyImport) {
|
||||||
|
$paymentsImport = false; // history import cannot be combined with payments mode
|
||||||
|
$allowedHistoryEntities = ['person', 'person_addresses', 'person_phones', 'contracts', 'activities', 'client_cases'];
|
||||||
|
$entities = array_values(array_intersect($entities, $allowedHistoryEntities));
|
||||||
|
// If contracts are present, ensure accounts are included implicitly for reference consistency
|
||||||
|
if (in_array('contracts', $entities, true) && ! in_array('accounts', $entities, true)) {
|
||||||
|
$entities[] = 'accounts';
|
||||||
|
}
|
||||||
|
// Reject mappings that target disallowed entities for history import
|
||||||
|
$disallowedMappings = collect($data['mappings'] ?? [])->filter(function ($m) use ($allowedHistoryEntities) {
|
||||||
|
if (empty($m['entity'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ! in_array($m['entity'], $allowedHistoryEntities, true);
|
||||||
|
});
|
||||||
|
if ($disallowedMappings->isNotEmpty()) {
|
||||||
|
abort(422, 'History import only allows entities: person, person_addresses, person_phones, contracts, activities, client_cases. Remove other mapping entities.');
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($paymentsImport) {
|
if ($paymentsImport) {
|
||||||
$entities = ['contracts', 'accounts', 'payments'];
|
$entities = ['contracts', 'accounts', 'payments'];
|
||||||
}
|
}
|
||||||
@@ -162,7 +187,11 @@ public function store(Request $request)
|
|||||||
'segment_id' => data_get($data, 'meta.segment_id'),
|
'segment_id' => data_get($data, 'meta.segment_id'),
|
||||||
'decision_id' => data_get($data, 'meta.decision_id'),
|
'decision_id' => data_get($data, 'meta.decision_id'),
|
||||||
'action_id' => data_get($data, 'meta.action_id'),
|
'action_id' => data_get($data, 'meta.action_id'),
|
||||||
|
'activity_action_id' => data_get($data, 'meta.activity_action_id'),
|
||||||
|
'activity_decision_id' => data_get($data, 'meta.activity_decision_id'),
|
||||||
|
'activity_created_at' => data_get($data, 'meta.activity_created_at'),
|
||||||
'payments_import' => $paymentsImport ?: null,
|
'payments_import' => $paymentsImport ?: null,
|
||||||
|
'history_import' => $historyImport ?: null,
|
||||||
'contract_key_mode' => data_get($data, 'meta.contract_key_mode'),
|
'contract_key_mode' => data_get($data, 'meta.contract_key_mode'),
|
||||||
], fn ($v) => ! is_null($v) && $v !== ''),
|
], fn ($v) => ! is_null($v) && $v !== ''),
|
||||||
]);
|
]);
|
||||||
@@ -244,7 +273,7 @@ public function addMapping(Request $request, ImportTemplate $template)
|
|||||||
}
|
}
|
||||||
$data = validator($raw, [
|
$data = validator($raw, [
|
||||||
'source_column' => 'required|string',
|
'source_column' => 'required|string',
|
||||||
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
|
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments,activities',
|
||||||
'target_field' => 'nullable|string',
|
'target_field' => 'nullable|string',
|
||||||
'transform' => 'nullable|string|in:trim,upper,lower',
|
'transform' => 'nullable|string|in:trim,upper,lower',
|
||||||
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||||
@@ -314,7 +343,11 @@ public function update(Request $request, ImportTemplate $template)
|
|||||||
'meta.segment_id' => 'nullable|integer|exists:segments,id',
|
'meta.segment_id' => 'nullable|integer|exists:segments,id',
|
||||||
'meta.decision_id' => 'nullable|integer|exists:decisions,id',
|
'meta.decision_id' => 'nullable|integer|exists:decisions,id',
|
||||||
'meta.action_id' => 'nullable|integer|exists:actions,id',
|
'meta.action_id' => 'nullable|integer|exists:actions,id',
|
||||||
|
'meta.activity_action_id' => 'nullable|integer|exists:actions,id',
|
||||||
|
'meta.activity_decision_id' => 'nullable|integer|exists:decisions,id',
|
||||||
|
'meta.activity_created_at' => 'nullable|date',
|
||||||
'meta.payments_import' => 'nullable|boolean',
|
'meta.payments_import' => 'nullable|boolean',
|
||||||
|
'meta.history_import' => 'nullable|boolean',
|
||||||
'meta.contract_key_mode' => 'nullable|string|in:reference',
|
'meta.contract_key_mode' => 'nullable|string|in:reference',
|
||||||
])->validate();
|
])->validate();
|
||||||
|
|
||||||
@@ -342,6 +375,11 @@ public function update(Request $request, ImportTemplate $template)
|
|||||||
unset($newMeta[$k]);
|
unset($newMeta[$k]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
foreach (['activity_action_id', 'activity_decision_id', 'activity_created_at'] as $k) {
|
||||||
|
if (array_key_exists($k, $newMeta) && ($newMeta[$k] === '' || is_null($newMeta[$k]))) {
|
||||||
|
unset($newMeta[$k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finalize meta (ensure payments entities forced if enabled)
|
// Finalize meta (ensure payments entities forced if enabled)
|
||||||
@@ -349,6 +387,20 @@ public function update(Request $request, ImportTemplate $template)
|
|||||||
if (! empty($finalMeta['payments_import'])) {
|
if (! empty($finalMeta['payments_import'])) {
|
||||||
$finalMeta['entities'] = ['contracts', 'accounts', 'payments'];
|
$finalMeta['entities'] = ['contracts', 'accounts', 'payments'];
|
||||||
}
|
}
|
||||||
|
if (! empty($finalMeta['history_import'])) {
|
||||||
|
$finalMeta['payments_import'] = false;
|
||||||
|
$allowedHistoryEntities = ['person', 'person_addresses', 'person_phones', 'contracts', 'activities', 'client_cases'];
|
||||||
|
$finalMeta['entities'] = array_values(array_intersect($finalMeta['entities'] ?? [], $allowedHistoryEntities));
|
||||||
|
if (in_array('contracts', $finalMeta['entities'] ?? [], true) && ! in_array('accounts', $finalMeta['entities'] ?? [], true)) {
|
||||||
|
$finalMeta['entities'][] = 'accounts';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('activities', $finalMeta['entities'] ?? [], true)) {
|
||||||
|
if (empty($finalMeta['activity_action_id']) || empty($finalMeta['activity_decision_id'])) {
|
||||||
|
return back()->withErrors(['meta.activity_action_id' => 'Activities import requires selecting both a default action and decision.'])->withInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$update = [
|
$update = [
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
@@ -381,7 +433,7 @@ public function bulkAddMappings(Request $request, ImportTemplate $template)
|
|||||||
}
|
}
|
||||||
$data = validator($raw, [
|
$data = validator($raw, [
|
||||||
'sources' => 'required|string', // comma and/or newline separated
|
'sources' => 'required|string', // comma and/or newline separated
|
||||||
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
|
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments,activities',
|
||||||
'default_field' => 'nullable|string', // if provided, used as the field name for all entries
|
'default_field' => 'nullable|string', // if provided, used as the field name for all entries
|
||||||
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||||
'transform' => 'nullable|string|in:trim,upper,lower',
|
'transform' => 'nullable|string|in:trim,upper,lower',
|
||||||
@@ -488,7 +540,7 @@ public function updateMapping(Request $request, ImportTemplate $template, Import
|
|||||||
}
|
}
|
||||||
$data = validator($raw, [
|
$data = validator($raw, [
|
||||||
'source_column' => 'required|string',
|
'source_column' => 'required|string',
|
||||||
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
|
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments',
|
||||||
'target_field' => 'nullable|string',
|
'target_field' => 'nullable|string',
|
||||||
'transform' => 'nullable|string|in:trim,upper,lower',
|
'transform' => 'nullable|string|in:trim,upper,lower',
|
||||||
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||||
@@ -583,6 +635,9 @@ public function applyToImport(Request $request, ImportTemplate $template, Import
|
|||||||
'segment_id' => $tplMeta['segment_id'] ?? null,
|
'segment_id' => $tplMeta['segment_id'] ?? null,
|
||||||
'decision_id' => $tplMeta['decision_id'] ?? null,
|
'decision_id' => $tplMeta['decision_id'] ?? null,
|
||||||
'action_id' => $tplMeta['action_id'] ?? null,
|
'action_id' => $tplMeta['action_id'] ?? null,
|
||||||
|
'activity_action_id' => $tplMeta['activity_action_id'] ?? null,
|
||||||
|
'activity_decision_id' => $tplMeta['activity_decision_id'] ?? null,
|
||||||
|
'activity_created_at' => $tplMeta['activity_created_at'] ?? null,
|
||||||
'template_name' => $template->name,
|
'template_name' => $template->name,
|
||||||
], fn ($v) => ! is_null($v) && $v !== ''));
|
], fn ($v) => ! is_null($v) && $v !== ''));
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,15 @@ public function unread(Request $request)
|
|||||||
->select(['id', 'due_date', 'amount', 'contract_id', 'client_case_id', 'created_at'])
|
->select(['id', 'due_date', 'amount', 'contract_id', 'client_case_id', 'created_at'])
|
||||||
->whereNotNull('due_date')
|
->whereNotNull('due_date')
|
||||||
->whereDate('due_date', '<=', $today)
|
->whereDate('due_date', '<=', $today)
|
||||||
// Removed per-user unread filter: show notifications regardless of individual reads
|
// Exclude activities that have been marked as read by this user
|
||||||
|
->whereNotExists(function ($q) use ($user, $today) {
|
||||||
|
$q->select(\DB::raw(1))
|
||||||
|
->from('activity_notification_reads')
|
||||||
|
->whereColumn('activity_notification_reads.activity_id', 'activities.id')
|
||||||
|
->where('activity_notification_reads.user_id', $user->id)
|
||||||
|
->whereDate('activity_notification_reads.due_date', '<=', $today)
|
||||||
|
->whereNotNull('activity_notification_reads.read_at');
|
||||||
|
})
|
||||||
->when($clientCaseIdsForFilter->isNotEmpty(), function ($q) use ($clientCaseIdsForFilter) {
|
->when($clientCaseIdsForFilter->isNotEmpty(), function ($q) use ($clientCaseIdsForFilter) {
|
||||||
// Filter by clients: activities directly on any of the client's cases OR via contracts under those cases
|
// Filter by clients: activities directly on any of the client's cases OR via contracts under those cases
|
||||||
$q->where(function ($qq) use ($clientCaseIdsForFilter) {
|
$q->where(function ($qq) use ($clientCaseIdsForFilter) {
|
||||||
@@ -108,7 +116,15 @@ public function unread(Request $request)
|
|||||||
->select(['contract_id', 'client_case_id'])
|
->select(['contract_id', 'client_case_id'])
|
||||||
->whereNotNull('due_date')
|
->whereNotNull('due_date')
|
||||||
->whereDate('due_date', '<=', $today)
|
->whereDate('due_date', '<=', $today)
|
||||||
// Removed per-user unread filter for client list base
|
// Exclude activities that have been marked as read by this user
|
||||||
|
->whereNotExists(function ($q) use ($user, $today) {
|
||||||
|
$q->select(\DB::raw(1))
|
||||||
|
->from('activity_notification_reads')
|
||||||
|
->whereColumn('activity_notification_reads.activity_id', 'activities.id')
|
||||||
|
->where('activity_notification_reads.user_id', $user->id)
|
||||||
|
->whereDate('activity_notification_reads.due_date', '<=', $today)
|
||||||
|
->whereNotNull('activity_notification_reads.read_at');
|
||||||
|
})
|
||||||
->when($clientCaseIdsForFilter->isNotEmpty(), function ($q) use ($clientCaseIdsForFilter) {
|
->when($clientCaseIdsForFilter->isNotEmpty(), function ($q) use ($clientCaseIdsForFilter) {
|
||||||
$q->where(function ($qq) use ($clientCaseIdsForFilter) {
|
$q->where(function ($qq) use ($clientCaseIdsForFilter) {
|
||||||
$qq->whereIn('activities.client_case_id', $clientCaseIdsForFilter)
|
$qq->whereIn('activities.client_case_id', $clientCaseIdsForFilter)
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class PaymentController extends Controller
|
class PaymentController extends Controller
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -76,169 +76,81 @@ public function completedToday(Request $request)
|
|||||||
public function showCase(\App\Models\ClientCase $clientCase, Request $request)
|
public function showCase(\App\Models\ClientCase $clientCase, Request $request)
|
||||||
{
|
{
|
||||||
$userId = $request->user()->id;
|
$userId = $request->user()->id;
|
||||||
$completedMode = (bool) $request->boolean('completed');
|
$completedMode = $request->boolean('completed');
|
||||||
|
|
||||||
// Eager load client case with person details
|
// Eager load case with person details
|
||||||
$case = \App\Models\ClientCase::query()
|
$case = $clientCase->load('person.addresses', 'person.phones', 'person.emails', 'person.bankAccounts');
|
||||||
->with(['person' => fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts'])])
|
|
||||||
->findOrFail($clientCase->id);
|
|
||||||
|
|
||||||
// Determine contracts of this case relevant to the current user
|
// Query contracts based on field jobs
|
||||||
// - Normal mode: contracts assigned to me and still active (not completed/cancelled)
|
$contractsQuery = FieldJob::query()
|
||||||
// - Completed mode (?completed=1): contracts where my field job was completed today
|
|
||||||
if ($completedMode) {
|
|
||||||
$start = now()->startOfDay();
|
|
||||||
$end = now()->endOfDay();
|
|
||||||
$contractIds = FieldJob::query()
|
|
||||||
->where('assigned_user_id', $userId)
|
->where('assigned_user_id', $userId)
|
||||||
->whereNull('cancelled_at')
|
|
||||||
->whereBetween('completed_at', [$start, $end])
|
|
||||||
->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id))
|
->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id))
|
||||||
->pluck('contract_id')
|
->when($completedMode,
|
||||||
->unique()
|
fn ($q) => $q->whereNull('cancelled_at')->whereBetween('completed_at', [now()->startOfDay(), now()->endOfDay()]),
|
||||||
->values();
|
fn ($q) => $q->whereNull('completed_at')->whereNull('cancelled_at')
|
||||||
} else {
|
);
|
||||||
$contractIds = FieldJob::query()
|
|
||||||
->where('assigned_user_id', $userId)
|
|
||||||
->whereNull('completed_at')
|
|
||||||
->whereNull('cancelled_at')
|
|
||||||
->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id))
|
|
||||||
->pluck('contract_id')
|
|
||||||
->unique()
|
|
||||||
->values();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Get contracts with relationships
|
||||||
$contracts = \App\Models\Contract::query()
|
$contracts = \App\Models\Contract::query()
|
||||||
->where('client_case_id', $case->id)
|
->where('client_case_id', $case->id)
|
||||||
->whereIn('id', $contractIds)
|
->whereIn('id', $contractsQuery->pluck('contract_id')->unique())
|
||||||
->with(['type:id,name', 'account'])
|
->with(['type:id,name', 'account', 'latestObject'])
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Attach latest object (if any) to each contract as last_object for display
|
// Build merged documents
|
||||||
if ($contracts->isNotEmpty()) {
|
$documents = $case->documents()
|
||||||
$byId = $contracts->keyBy('id');
|
|
||||||
$latestObjects = \App\Models\CaseObject::query()
|
|
||||||
->whereIn('contract_id', $byId->keys())
|
|
||||||
->whereNull('deleted_at')
|
|
||||||
->select('id', 'reference', 'name', 'description', 'type', 'contract_id', 'created_at')
|
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->get()
|
->get()
|
||||||
->groupBy('contract_id')
|
->map(fn ($d) => array_merge($d->toArray(), [
|
||||||
->map(function ($group) {
|
'documentable_type' => \App\Models\ClientCase::class,
|
||||||
return $group->first();
|
'client_case_uuid' => $case->uuid,
|
||||||
});
|
]))
|
||||||
|
->concat(
|
||||||
foreach ($latestObjects as $cid => $obj) {
|
\App\Models\Document::query()
|
||||||
if (isset($byId[$cid])) {
|
|
||||||
$byId[$cid]->setAttribute('last_object', $obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build merged documents: case documents + documents of assigned contracts
|
|
||||||
$contractRefMap = [];
|
|
||||||
foreach ($contracts as $c) {
|
|
||||||
$contractRefMap[$c->id] = $c->reference;
|
|
||||||
}
|
|
||||||
|
|
||||||
$contractDocs = \App\Models\Document::query()
|
|
||||||
->where('documentable_type', \App\Models\Contract::class)
|
->where('documentable_type', \App\Models\Contract::class)
|
||||||
->whereIn('documentable_id', $contractIds)
|
->whereIn('documentable_id', $contracts->pluck('id'))
|
||||||
|
->with('documentable:id,uuid,reference')
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->get()
|
->get()
|
||||||
->map(function ($d) use ($contractRefMap) {
|
->map(fn ($d) => array_merge($d->toArray(), [
|
||||||
$arr = $d->toArray();
|
'contract_reference' => $d->documentable?->reference,
|
||||||
$arr['contract_reference'] = $contractRefMap[$d->documentable_id] ?? null;
|
'contract_uuid' => $d->documentable?->uuid,
|
||||||
$arr['documentable_type'] = \App\Models\Contract::class;
|
]))
|
||||||
$arr['contract_uuid'] = optional(\App\Models\Contract::withTrashed()->find($d->documentable_id))->uuid;
|
)
|
||||||
|
->sortByDesc('created_at')
|
||||||
|
->values();
|
||||||
|
|
||||||
return $arr;
|
// Get segment IDs for filtering actions
|
||||||
});
|
$segmentIds = \App\Models\FieldJobSetting::query()
|
||||||
|
->whereIn('id', $contractsQuery->pluck('field_job_setting_id')->filter()->unique())
|
||||||
|
->pluck('segment_id')
|
||||||
|
->filter()
|
||||||
|
->unique();
|
||||||
|
|
||||||
$caseDocs = $case->documents()->orderByDesc('created_at')->get()->map(function ($d) use ($case) {
|
return Inertia::render('Phone/Case/Index', [
|
||||||
$arr = $d->toArray();
|
'client' => $case->client->load('person.addresses', 'person.phones', 'person.emails', 'person.bankAccounts'),
|
||||||
$arr['documentable_type'] = \App\Models\ClientCase::class;
|
'client_case' => $case,
|
||||||
$arr['client_case_uuid'] = $case->uuid;
|
'contracts' => $contracts,
|
||||||
|
'documents' => $documents,
|
||||||
return $arr;
|
'types' => [
|
||||||
});
|
|
||||||
|
|
||||||
$documents = $caseDocs->concat($contractDocs)->sortByDesc('created_at')->values();
|
|
||||||
|
|
||||||
// Provide minimal types for PersonInfoGrid
|
|
||||||
$types = [
|
|
||||||
'address_types' => \App\Models\Person\AddressType::all(),
|
'address_types' => \App\Models\Person\AddressType::all(),
|
||||||
'phone_types' => \App\Models\Person\PhoneType::all(),
|
'phone_types' => \App\Models\Person\PhoneType::all(),
|
||||||
];
|
],
|
||||||
|
'account_types' => \App\Models\AccountType::all(),
|
||||||
// Case activities (compact for phone): latest 20 with relations
|
'actions' => \App\Models\Action::query()
|
||||||
$activities = $case->activities()
|
->when($segmentIds->isNotEmpty(), fn ($q) => $q->whereIn('segment_id', $segmentIds))
|
||||||
|
->with([
|
||||||
|
'decisions:id,name,color_tag,auto_mail,email_template_id',
|
||||||
|
'decisions.emailTemplate:id,name,entity_types,allow_attachments',
|
||||||
|
])
|
||||||
|
->get(['id', 'name', 'color_tag', 'segment_id']),
|
||||||
|
'activities' => $case->activities()
|
||||||
->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name'])
|
->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name'])
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->limit(20)
|
->limit(20)
|
||||||
->get()
|
->get()
|
||||||
->map(function ($a) {
|
->map(fn ($a) => $a->setAttribute('user_name', $a->user?->name)),
|
||||||
$a->setAttribute('user_name', optional($a->user)->name);
|
|
||||||
|
|
||||||
return $a;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Determine segment filters from FieldJobSettings for this case/user context
|
|
||||||
$settingIds = FieldJob::query()
|
|
||||||
->where('assigned_user_id', $userId)
|
|
||||||
->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id))
|
|
||||||
->when(
|
|
||||||
$completedMode,
|
|
||||||
function ($q) {
|
|
||||||
$q->whereNull('cancelled_at')
|
|
||||||
->whereBetween('completed_at', [now()->startOfDay(), now()->endOfDay()]);
|
|
||||||
},
|
|
||||||
function ($q) {
|
|
||||||
$q->whereNull('completed_at')->whereNull('cancelled_at');
|
|
||||||
}
|
|
||||||
)
|
|
||||||
->pluck('field_job_setting_id')
|
|
||||||
->filter()
|
|
||||||
->unique()
|
|
||||||
->values();
|
|
||||||
|
|
||||||
$segmentIds = collect();
|
|
||||||
if ($settingIds->isNotEmpty()) {
|
|
||||||
$segmentIds = \App\Models\FieldJobSetting::query()
|
|
||||||
->whereIn('id', $settingIds)
|
|
||||||
->pluck('segment_id')
|
|
||||||
->filter()
|
|
||||||
->unique()
|
|
||||||
->values();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter actions and their decisions by the derived segment ids (decisions.segment_id)
|
|
||||||
$actions = \App\Models\Action::query()
|
|
||||||
->when($segmentIds->isNotEmpty(), function ($q) use ($segmentIds) {
|
|
||||||
// Filter actions by their segment_id matching the FieldJobSetting segment(s)
|
|
||||||
$q->whereIn('segment_id', $segmentIds);
|
|
||||||
})
|
|
||||||
->with([
|
|
||||||
'decisions' => function ($q) {
|
|
||||||
$q->select('decisions.id', 'decisions.name', 'decisions.color_tag', 'decisions.auto_mail', 'decisions.email_template_id');
|
|
||||||
},
|
|
||||||
'decisions.emailTemplate' => function ($q) {
|
|
||||||
$q->select('id', 'name', 'entity_types', 'allow_attachments');
|
|
||||||
},
|
|
||||||
])
|
|
||||||
->get(['id', 'name', 'color_tag', 'segment_id']);
|
|
||||||
|
|
||||||
return Inertia::render('Phone/Case/Index', [
|
|
||||||
'client' => $case->client()->with('person', fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts']))->firstOrFail(),
|
|
||||||
'client_case' => $case,
|
|
||||||
'contracts' => $contracts,
|
|
||||||
'documents' => $documents,
|
|
||||||
'types' => $types,
|
|
||||||
'account_types' => \App\Models\AccountType::all(),
|
|
||||||
// Provide decisions (filtered by segment) with linked email template metadata (entity_types, allow_attachments)
|
|
||||||
'actions' => $actions,
|
|
||||||
'activities' => $activities,
|
|
||||||
'completed_mode' => $completedMode,
|
'completed_mode' => $completedMode,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Post;
|
|
||||||
use App\Http\Requests\StorePostRequest;
|
use App\Http\Requests\StorePostRequest;
|
||||||
use App\Http\Requests\UpdatePostRequest;
|
use App\Http\Requests\UpdatePostRequest;
|
||||||
|
use App\Models\Post;
|
||||||
|
|
||||||
class PostController extends Controller
|
class PostController extends Controller
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,12 +2,20 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Exports\SegmentContractsExport;
|
||||||
|
use App\Http\Requests\ExportSegmentContractsRequest;
|
||||||
use App\Http\Requests\StoreSegmentRequest;
|
use App\Http\Requests\StoreSegmentRequest;
|
||||||
use App\Http\Requests\UpdateSegmentRequest;
|
use App\Http\Requests\UpdateSegmentRequest;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Contract;
|
||||||
use App\Models\Segment;
|
use App\Models\Segment;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
class SegmentController extends Controller
|
class SegmentController extends Controller
|
||||||
{
|
{
|
||||||
@@ -44,64 +52,26 @@ public function index()
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(\App\Models\Segment $segment)
|
public function show(Segment $segment)
|
||||||
{
|
{
|
||||||
// Retrieve contracts that are active in this segment, eager-loading required relations
|
|
||||||
$search = request('search');
|
$search = request('search');
|
||||||
$clientFilter = request('client') ?? request('client_id'); // support either ?client=<uuid|id> or ?client_id=<id>
|
$clientFilter = request('client') ?? request('client_id');
|
||||||
$contractsQuery = \App\Models\Contract::query()
|
$perPage = request()->integer('perPage', request()->integer('per_page', 15));
|
||||||
->whereHas('segments', function ($q) use ($segment) {
|
$perPage = max(1, min(200, $perPage));
|
||||||
$q->where('segments.id', $segment->id)
|
|
||||||
->where('contract_segment.active', '=', 1);
|
|
||||||
})
|
|
||||||
->with([
|
|
||||||
'clientCase.person',
|
|
||||||
'clientCase.client.person',
|
|
||||||
'type',
|
|
||||||
'account',
|
|
||||||
])
|
|
||||||
->latest('id');
|
|
||||||
|
|
||||||
// Optional filter by client (accepts numeric id or client uuid)
|
$contracts = $this->buildContractsQuery($segment, $search, $clientFilter)
|
||||||
if (! empty($clientFilter)) {
|
->paginate($perPage)
|
||||||
$contractsQuery->whereHas('clientCase.client', function ($q) use ($clientFilter) {
|
|
||||||
if (is_numeric($clientFilter)) {
|
|
||||||
$q->where('clients.id', (int) $clientFilter);
|
|
||||||
} else {
|
|
||||||
$q->where('clients.uuid', $clientFilter);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($search)) {
|
|
||||||
$contractsQuery->where(function ($qq) use ($search) {
|
|
||||||
$qq->where('contracts.reference', 'ilike', '%'.$search.'%')
|
|
||||||
->orWhereHas('clientCase.person', function ($p) use ($search) {
|
|
||||||
$p->where('full_name', 'ilike', '%'.$search.'%');
|
|
||||||
})
|
|
||||||
->orWhereHas('clientCase.client.person', function ($p) use ($search) {
|
|
||||||
$p->where('full_name', 'ilike', '%'.$search.'%');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$contracts = $contractsQuery
|
|
||||||
->paginate(15)
|
|
||||||
->withQueryString();
|
->withQueryString();
|
||||||
|
|
||||||
// Mirror client onto the contract to simplify frontend access (c.client.person.full_name)
|
$contracts = $this->hydrateClientShortcut($contracts);
|
||||||
$items = collect($contracts->items());
|
|
||||||
$items->each(function ($contract) {
|
|
||||||
if ($contract->relationLoaded('clientCase') && $contract->clientCase) {
|
|
||||||
$contract->setRelation('client', $contract->clientCase->client);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (method_exists($contracts, 'setCollection')) {
|
|
||||||
$contracts->setCollection($items);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a full client list for this segment (not limited to current page) for the dropdown
|
// Hide addresses array since we're using the singular address relationship
|
||||||
$clients = \App\Models\Client::query()
|
$contracts->getCollection()->each(function ($contract) {
|
||||||
|
$contract->clientCase?->person?->makeHidden('addresses');
|
||||||
|
$contract->clientCase?->client?->person?->makeHidden('addresses');
|
||||||
|
});
|
||||||
|
|
||||||
|
$clients = Client::query()
|
||||||
->whereHas('clientCases.contracts.segments', function ($q) use ($segment) {
|
->whereHas('clientCases.contracts.segments', function ($q) use ($segment) {
|
||||||
$q->where('segments.id', $segment->id)
|
$q->where('segments.id', $segment->id)
|
||||||
->where('contract_segment.active', '=', 1);
|
->where('contract_segment.active', '=', 1);
|
||||||
@@ -124,6 +94,69 @@ public function show(\App\Models\Segment $segment)
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function export(ExportSegmentContractsRequest $request, Segment $segment)
|
||||||
|
{
|
||||||
|
$data = $request->validated();
|
||||||
|
$client = $this->resolveClient($data['client'] ?? null);
|
||||||
|
$columns = array_values(array_unique($data['columns']));
|
||||||
|
$query = $this->buildContractsQuery(
|
||||||
|
$segment,
|
||||||
|
$data['search'] ?? null,
|
||||||
|
$data['client'] ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (($data['scope'] ?? ExportSegmentContractsRequest::SCOPE_ALL) === ExportSegmentContractsRequest::SCOPE_CURRENT) {
|
||||||
|
$page = max(1, (int) ($data['page'] ?? 1));
|
||||||
|
$perPage = max(1, min(200, (int) ($data['per_page'] ?? 15)));
|
||||||
|
$query->forPage($page, $perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = $this->buildExportFilename($segment, $client);
|
||||||
|
|
||||||
|
return Excel::download(new SegmentContractsExport($query, $columns), $filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveClient(?string $identifier): ?Client
|
||||||
|
{
|
||||||
|
if (empty($identifier)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = Client::query()->with(['person:id,full_name']);
|
||||||
|
|
||||||
|
if (Str::isUuid($identifier)) {
|
||||||
|
$query->where('uuid', $identifier);
|
||||||
|
} elseif (is_numeric($identifier)) {
|
||||||
|
$query->where('id', (int) $identifier);
|
||||||
|
} else {
|
||||||
|
$query->where('uuid', $identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildExportFilename(Segment $segment, ?Client $client): string
|
||||||
|
{
|
||||||
|
$datePrefix = now()->format('dmy');
|
||||||
|
$segmentName = $this->slugify($segment->name ?? 'segment');
|
||||||
|
$base = sprintf('%s_%s-Pogodbe', $datePrefix, $segmentName);
|
||||||
|
|
||||||
|
if ($client && $client->person?->full_name) {
|
||||||
|
$clientName = $this->slugify($client->person->full_name);
|
||||||
|
|
||||||
|
return sprintf('%s_%s.xlsx', $base, $clientName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('%s.xlsx', $base);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function slugify(string $value): string
|
||||||
|
{
|
||||||
|
$slug = trim(preg_replace('/[^a-zA-Z0-9]+/', '-', $value), '-');
|
||||||
|
|
||||||
|
return $slug !== '' ? $slug : 'data';
|
||||||
|
}
|
||||||
|
|
||||||
public function settings(Request $request)
|
public function settings(Request $request)
|
||||||
{
|
{
|
||||||
return Inertia::render('Settings/Segments/Index', [
|
return Inertia::render('Settings/Segments/Index', [
|
||||||
@@ -155,4 +188,59 @@ public function update(UpdateSegmentRequest $request, Segment $segment)
|
|||||||
|
|
||||||
return to_route('settings.segments')->with('success', 'Segment updated');
|
return to_route('settings.segments')->with('success', 'Segment updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildContractsQuery(Segment $segment, ?string $search, ?string $clientFilter): Builder
|
||||||
|
{
|
||||||
|
$query = Contract::query()
|
||||||
|
->whereHas('segments', function ($q) use ($segment) {
|
||||||
|
$q->where('segments.id', $segment->id)
|
||||||
|
->where('contract_segment.active', '=', 1);
|
||||||
|
})
|
||||||
|
->with([
|
||||||
|
'clientCase.person.address',
|
||||||
|
'type',
|
||||||
|
'account',
|
||||||
|
])
|
||||||
|
->latest('id');
|
||||||
|
|
||||||
|
if (! empty($clientFilter)) {
|
||||||
|
$query->whereHas('clientCase.client', function ($q) use ($clientFilter) {
|
||||||
|
if (is_numeric($clientFilter)) {
|
||||||
|
$q->where('clients.id', (int) $clientFilter);
|
||||||
|
} else {
|
||||||
|
$q->where('clients.uuid', $clientFilter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($search)) {
|
||||||
|
$query->where(function ($qq) use ($search) {
|
||||||
|
$qq->where('contracts.reference', 'ilike', '%'.$search.'%')
|
||||||
|
->orWhereHas('clientCase.person', function ($p) use ($search) {
|
||||||
|
$p->where('full_name', 'ilike', '%'.$search.'%');
|
||||||
|
})
|
||||||
|
->orWhereHas('clientCase.client.person', function ($p) use ($search) {
|
||||||
|
$p->where('full_name', 'ilike', '%'.$search.'%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hydrateClientShortcut(LengthAwarePaginator $contracts): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
$items = collect($contracts->items());
|
||||||
|
$items->each(function (Contract $contract) {
|
||||||
|
if ($contract->relationLoaded('clientCase') && $contract->clientCase) {
|
||||||
|
$contract->setRelation('client', $contract->clientCase->client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (method_exists($contracts, 'setCollection')) {
|
||||||
|
$contracts->setCollection($items);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $contracts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ class SettingController extends Controller
|
|||||||
{
|
{
|
||||||
//
|
//
|
||||||
|
|
||||||
public function index(Request $request){
|
public function index(Request $request)
|
||||||
|
{
|
||||||
return Inertia::render('Settings/Index');
|
return Inertia::render('Settings/Index');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class EnsureUserIsActive
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if ($user && ! $user->active) {
|
||||||
|
// Revoke all tokens for Sanctum
|
||||||
|
if (method_exists($user, 'tokens')) {
|
||||||
|
$user->tokens()->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout from web guard
|
||||||
|
Auth::guard('web')->logout();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
if ($request->expectsJson()) {
|
||||||
|
return response()->json(['message' => 'Vaš račun je bil onemogočen.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('login')->with('error', 'Vaš račun je bil onemogočen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,7 +71,15 @@ public function share(Request $request): array
|
|||||||
$activities = \App\Models\Activity::query()
|
$activities = \App\Models\Activity::query()
|
||||||
->select(['id', 'due_date', 'amount', 'contract_id', 'client_case_id', 'created_at'])
|
->select(['id', 'due_date', 'amount', 'contract_id', 'client_case_id', 'created_at'])
|
||||||
->whereDate('due_date', $today)
|
->whereDate('due_date', $today)
|
||||||
// Removed per-user unread filter: show notifications regardless of individual reads
|
// Exclude activities that have been marked as read by this user
|
||||||
|
->whereNotExists(function ($q) use ($user, $today) {
|
||||||
|
$q->select(\DB::raw(1))
|
||||||
|
->from('activity_notification_reads')
|
||||||
|
->whereColumn('activity_notification_reads.activity_id', 'activities.id')
|
||||||
|
->where('activity_notification_reads.user_id', $user->id)
|
||||||
|
->whereDate('activity_notification_reads.due_date', '<=', $today)
|
||||||
|
->whereNotNull('activity_notification_reads.read_at');
|
||||||
|
})
|
||||||
->orderBy('created_at')
|
->orderBy('created_at')
|
||||||
->limit(20)
|
->limit(20)
|
||||||
->get();
|
->get();
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class StoreUserRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return Gate::allows('manage-settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
|
||||||
|
'password' => ['required', 'string', Password::defaults(), 'confirmed'],
|
||||||
|
'roles' => ['array'],
|
||||||
|
'roles.*' => ['integer', 'exists:roles,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom error messages.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.required' => 'Ime uporabnika je obvezno.',
|
||||||
|
'email.required' => 'E-poštni naslov je obvezen.',
|
||||||
|
'email.email' => 'E-poštni naslov mora biti veljaven.',
|
||||||
|
'email.unique' => 'Ta e-poštni naslov je že v uporabi.',
|
||||||
|
'password.required' => 'Geslo je obvezno.',
|
||||||
|
'password.confirmed' => 'Gesli se ne ujemata.',
|
||||||
|
'roles.*.exists' => 'Izbrana vloga ni veljavna.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Exports\ClientContractsExport;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ExportClientContractsRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public const SCOPE_CURRENT = 'current';
|
||||||
|
|
||||||
|
public const SCOPE_ALL = 'all';
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$columnRule = Rule::in(ClientContractsExport::allowedColumns());
|
||||||
|
|
||||||
|
return [
|
||||||
|
'scope' => ['required', Rule::in([self::SCOPE_CURRENT, self::SCOPE_ALL])],
|
||||||
|
'columns' => ['required', 'array', 'min:1'],
|
||||||
|
'columns.*' => ['string', $columnRule],
|
||||||
|
'search' => ['nullable', 'string', 'max:255'],
|
||||||
|
'from' => ['nullable', 'date'],
|
||||||
|
'to' => ['nullable', 'date'],
|
||||||
|
'segments' => ['nullable', 'string'],
|
||||||
|
'page' => ['nullable', 'integer', 'min:1'],
|
||||||
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:200'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$this->merge([
|
||||||
|
'per_page' => $this->input('per_page') ?? $this->input('perPage'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Exports\SegmentContractsExport;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ExportSegmentContractsRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public const SCOPE_CURRENT = 'current';
|
||||||
|
|
||||||
|
public const SCOPE_ALL = 'all';
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$columnRule = Rule::in(SegmentContractsExport::allowedColumns());
|
||||||
|
|
||||||
|
return [
|
||||||
|
'scope' => ['required', Rule::in([self::SCOPE_CURRENT, self::SCOPE_ALL])],
|
||||||
|
'columns' => ['required', 'array', 'min:1'],
|
||||||
|
'columns.*' => ['string', $columnRule],
|
||||||
|
'search' => ['nullable', 'string', 'max:255'],
|
||||||
|
'client' => ['nullable', 'string', 'max:64'],
|
||||||
|
'page' => ['nullable', 'integer', 'min:1'],
|
||||||
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:200'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$this->merge([
|
||||||
|
'client' => $this->input('client') ?? $this->input('client_id'),
|
||||||
|
'per_page' => $this->input('per_page') ?? $this->input('perPage'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ public function rules(): array
|
|||||||
'name' => ['required', 'string', 'max:50'],
|
'name' => ['required', 'string', 'max:50'],
|
||||||
'description' => ['nullable', 'string', 'max:255'],
|
'description' => ['nullable', 'string', 'max:255'],
|
||||||
'active' => ['boolean'],
|
'active' => ['boolean'],
|
||||||
'exclude' => ['boolean']
|
'exclude' => ['boolean'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class PersonCollection extends ResourceCollection
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'data' => $this->collection
|
'data' => $this->collection,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
use App\Models\SmsSender;
|
use App\Models\SmsSender;
|
||||||
use App\Models\SmsTemplate;
|
use App\Models\SmsTemplate;
|
||||||
use App\Services\Sms\SmsService;
|
use App\Services\Sms\SmsService;
|
||||||
|
use Illuminate\Bus\Batchable;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
|
|
||||||
class PackageItemSmsJob implements ShouldQueue
|
class PackageItemSmsJob implements ShouldQueue
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
public function __construct(public int $packageItemId)
|
public function __construct(public int $packageItemId)
|
||||||
{
|
{
|
||||||
@@ -69,6 +70,10 @@ public function handle(SmsService $sms): void
|
|||||||
'start_date' => (string) ($contract->start_date ?? ''),
|
'start_date' => (string) ($contract->start_date ?? ''),
|
||||||
'end_date' => (string) ($contract->end_date ?? ''),
|
'end_date' => (string) ($contract->end_date ?? ''),
|
||||||
];
|
];
|
||||||
|
// Include contract.meta as flattened key-value pairs for template access
|
||||||
|
if (is_array($contract->meta) && ! empty($contract->meta)) {
|
||||||
|
$variables['contract']['meta'] = $this->flattenMeta($contract->meta);
|
||||||
|
}
|
||||||
if ($contract->account) {
|
if ($contract->account) {
|
||||||
// Preserve raw values and provide EU-formatted versions for SMS rendering
|
// Preserve raw values and provide EU-formatted versions for SMS rendering
|
||||||
$initialRaw = (string) $contract->account->initial_amount;
|
$initialRaw = (string) $contract->account->initial_amount;
|
||||||
@@ -97,7 +102,7 @@ public function handle(SmsService $sms): void
|
|||||||
/** @var SmsSender|null $sender */
|
/** @var SmsSender|null $sender */
|
||||||
$sender = $senderId ? SmsSender::find($senderId) : null;
|
$sender = $senderId ? SmsSender::find($senderId) : null;
|
||||||
/** @var SmsTemplate|null $template */
|
/** @var SmsTemplate|null $template */
|
||||||
$template = $templateId ? SmsTemplate::find($templateId) : null;
|
$template = $templateId ? SmsTemplate::with(['action', 'decision'])->find($templateId) : null;
|
||||||
|
|
||||||
$to = $target['number'] ?? null;
|
$to = $target['number'] ?? null;
|
||||||
if (! is_string($to) || $to === '') {
|
if (! is_string($to) || $to === '') {
|
||||||
@@ -117,7 +122,7 @@ public function handle(SmsService $sms): void
|
|||||||
$key = $scope === 'per_profile' && $profile ? "sms:{$provider}:{$profile->id}" : "sms:{$provider}";
|
$key = $scope === 'per_profile' && $profile ? "sms:{$provider}:{$profile->id}" : "sms:{$provider}";
|
||||||
|
|
||||||
// Throttle
|
// Throttle
|
||||||
$sendClosure = function () use ($sms, $item, $package, $profile, $sender, $template, $to, $variables, $deliveryReport, $bodyOverride) {
|
$sendClosure = function () use ($sms, $item, $package, $profile, $sender, $template, $to, $variables, $deliveryReport, $bodyOverride, $target) {
|
||||||
// Idempotency key (optional external use)
|
// Idempotency key (optional external use)
|
||||||
if (empty($item->idempotency_key)) {
|
if (empty($item->idempotency_key)) {
|
||||||
$hash = sha1(implode('|', [
|
$hash = sha1(implode('|', [
|
||||||
@@ -188,6 +193,25 @@ public function handle(SmsService $sms): void
|
|||||||
$item->last_error = $log->status === 'sent' ? null : ($log->meta['error_message'] ?? 'Failed');
|
$item->last_error = $log->status === 'sent' ? null : ($log->meta['error_message'] ?? 'Failed');
|
||||||
$item->save();
|
$item->save();
|
||||||
|
|
||||||
|
// Create activity if template has action_id and decision_id configured and SMS was sent successfully
|
||||||
|
if ($newStatus === 'sent' && $template && ($template->action_id || $template->decision_id)) {
|
||||||
|
if (! empty($target['contract_id'])) {
|
||||||
|
$contract = Contract::query()->with('clientCase')->find($target['contract_id']);
|
||||||
|
|
||||||
|
if ($contract && $contract->client_case_id) {
|
||||||
|
\App\Models\Activity::create(array_filter([
|
||||||
|
'client_case_id' => $contract->client_case_id,
|
||||||
|
'contract_id' => $contract->id,
|
||||||
|
'action_id' => $template->action_id,
|
||||||
|
'decision_id' => $template->decision_id,
|
||||||
|
'note' => "SMS poslan na {$to}: {$result['message']}",
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update package counters atomically
|
// Update package counters atomically
|
||||||
if ($newStatus === 'sent') {
|
if ($newStatus === 'sent') {
|
||||||
$package->increment('sent_count');
|
$package->increment('sent_count');
|
||||||
@@ -214,4 +238,47 @@ public function handle(SmsService $sms): void
|
|||||||
$sendClosure();
|
$sendClosure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flatten nested meta structure into dot-notation key-value pairs.
|
||||||
|
* Extracts 'value' from objects with {title, value, type} structure.
|
||||||
|
* Also creates direct access aliases for nested fields (skipping numeric keys).
|
||||||
|
*/
|
||||||
|
private function flattenMeta(array $meta, string $prefix = ''): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
foreach ($meta as $key => $value) {
|
||||||
|
$newKey = $prefix === '' ? $key : "{$prefix}.{$key}";
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
// Check if it's a structured meta entry with 'value' field
|
||||||
|
if (isset($value['value'])) {
|
||||||
|
$result[$newKey] = $value['value'];
|
||||||
|
// If parent key is numeric, also create direct alias without the number
|
||||||
|
if ($prefix !== '' && is_numeric($key)) {
|
||||||
|
$result[$key] = $value['value'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Recursively flatten nested arrays
|
||||||
|
$nested = $this->flattenMeta($value, $newKey);
|
||||||
|
$result = array_merge($result, $nested);
|
||||||
|
|
||||||
|
// If current key is numeric, also flatten without it for easier access
|
||||||
|
if (is_numeric($key)) {
|
||||||
|
$directNested = $this->flattenMeta($value, $prefix);
|
||||||
|
foreach ($directNested as $dk => $dv) {
|
||||||
|
// Only add if not already set (prefer first occurrence)
|
||||||
|
if (! isset($result[$dk])) {
|
||||||
|
$result[$dk] = $dv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$result[$newKey] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ protected function performSmtpAuthTest(MailProfile $profile): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
$remote = ($encryption === 'ssl') ? 'ssl://'.$host : $host;
|
$remote = ($encryption === 'ssl') ? 'ssl://'.$host : $host;
|
||||||
$errno = 0; $errstr = '';
|
$errno = 0;
|
||||||
|
$errstr = '';
|
||||||
$socket = @fsockopen($remote, $port, $errno, $errstr, 15);
|
$socket = @fsockopen($remote, $port, $errno, $errstr, 15);
|
||||||
if (! $socket) {
|
if (! $socket) {
|
||||||
throw new \RuntimeException("Connect failed: $errstr ($errno)");
|
throw new \RuntimeException("Connect failed: $errstr ($errno)");
|
||||||
@@ -104,7 +105,9 @@ protected function performSmtpAuthTest(MailProfile $profile): void
|
|||||||
// Cleanly quit
|
// Cleanly quit
|
||||||
$this->command($socket, "QUIT\r\n", [221], 'QUIT');
|
$this->command($socket, "QUIT\r\n", [221], 'QUIT');
|
||||||
} finally {
|
} finally {
|
||||||
try { fclose($socket); } catch (\Throwable) {
|
try {
|
||||||
|
fclose($socket);
|
||||||
|
} catch (\Throwable) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,6 +119,7 @@ protected function performSmtpAuthTest(MailProfile $profile): void
|
|||||||
protected function command($socket, string $cmd, array $expect, string $context): string
|
protected function command($socket, string $cmd, array $expect, string $context): string
|
||||||
{
|
{
|
||||||
fwrite($socket, $cmd);
|
fwrite($socket, $cmd);
|
||||||
|
|
||||||
return $this->expect($socket, $expect, $context);
|
return $this->expect($socket, $expect, $context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +142,7 @@ protected function expect($socket, array $expectedCodes, string $context): strin
|
|||||||
if (! in_array($code, $expectedCodes, true)) {
|
if (! in_array($code, $expectedCodes, true)) {
|
||||||
throw new \RuntimeException("Unexpected SMTP code $code during $context: ".implode(' | ', $lines));
|
throw new \RuntimeException("Unexpected SMTP code $code during $context: ".implode(' | ', $lines));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $line;
|
return $line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
class Account extends Model
|
class Account extends Model
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\Person/AccountFactory> */
|
/** @use HasFactory<\Database\Factories\Person/AccountFactory> */
|
||||||
|
use SoftDeletes;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class Action extends Model
|
|||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\ActionFactory> */
|
/** @use HasFactory<\Database\Factories\ActionFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
use Searchable;
|
use Searchable;
|
||||||
|
|
||||||
protected $fillable = ['name', 'color_tag', 'segment_id'];
|
protected $fillable = ['name', 'color_tag', 'segment_id'];
|
||||||
@@ -31,5 +32,4 @@ public function activities(): HasMany
|
|||||||
{
|
{
|
||||||
return $this->hasMany(\App\Models\Activity::class);
|
return $this->hasMany(\App\Models\Activity::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,23 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\Uuid;
|
use App\Traits\Uuid;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Laravel\Scout\Searchable;
|
use Laravel\Scout\Searchable;
|
||||||
|
|
||||||
class Client extends Model
|
class Client extends Model
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\ClientFactory> */
|
/** @use HasFactory<\Database\Factories\ClientFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use Uuid;
|
|
||||||
use Searchable;
|
use Searchable;
|
||||||
|
use Uuid;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'person_id'
|
'person_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
@@ -26,7 +27,6 @@ class Client extends Model
|
|||||||
'person_id',
|
'person_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
protected function makeAllSearchableUsing(Builder $query): Builder
|
protected function makeAllSearchableUsing(Builder $query): Builder
|
||||||
{
|
{
|
||||||
return $query->with('person');
|
return $query->with('person');
|
||||||
@@ -37,11 +37,10 @@ public function toSearchableArray(): array
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'person.full_name' => '',
|
'person.full_name' => '',
|
||||||
'person_addresses.address' => ''
|
'person_addresses.address' => '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function person(): BelongsTo
|
public function person(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(\App\Models\Person\Person::class);
|
return $this->belongsTo(\App\Models\Person\Person::class);
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ protected function startDate(): Attribute
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$str = is_string($value) ? $value : (string) $value;
|
$str = is_string($value) ? $value : (string) $value;
|
||||||
|
|
||||||
return \App\Services\DateNormalizer::toDate($str);
|
return \App\Services\DateNormalizer::toDate($str);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -71,6 +72,7 @@ protected function endDate(): Attribute
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$str = is_string($value) ? $value : (string) $value;
|
$str = is_string($value) ? $value : (string) $value;
|
||||||
|
|
||||||
return \App\Services\DateNormalizer::toDate($str);
|
return \App\Services\DateNormalizer::toDate($str);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -94,6 +96,11 @@ public function segments(): BelongsToMany
|
|||||||
->wherePivot('active', true);
|
->wherePivot('active', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function attachedSegments(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(\App\Models\Segment::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function account(): HasOne
|
public function account(): HasOne
|
||||||
{
|
{
|
||||||
// Use latestOfMany to always surface newest account snapshot if multiple exist.
|
// Use latestOfMany to always surface newest account snapshot if multiple exist.
|
||||||
@@ -112,6 +119,18 @@ public function documents(): MorphMany
|
|||||||
return $this->morphMany(\App\Models\Document::class, 'documentable');
|
return $this->morphMany(\App\Models\Document::class, 'documentable');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function fieldJobs(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\FieldJob::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function latestObject(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(\App\Models\CaseObject::class)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->latest();
|
||||||
|
}
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
{
|
{
|
||||||
static::created(function (Contract $contract): void {
|
static::created(function (Contract $contract): void {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class FieldJob extends Model
|
|||||||
'priority',
|
'priority',
|
||||||
'notes',
|
'notes',
|
||||||
'address_snapshot ',
|
'address_snapshot ',
|
||||||
|
'last_activity',
|
||||||
|
'added_activity'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@@ -31,6 +33,8 @@ class FieldJob extends Model
|
|||||||
'completed_at' => 'datetime',
|
'completed_at' => 'datetime',
|
||||||
'cancelled_at' => 'datetime',
|
'cancelled_at' => 'datetime',
|
||||||
'priority' => 'boolean',
|
'priority' => 'boolean',
|
||||||
|
'last_activity' => 'datetime',
|
||||||
|
'added_activity' => 'boolean',
|
||||||
'address_snapshot ' => 'array',
|
'address_snapshot ' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -90,7 +94,8 @@ public function user(): BelongsTo
|
|||||||
|
|
||||||
public function contract(): BelongsTo
|
public function contract(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Contract::class, 'contract_id');
|
return $this->belongsTo(Contract::class, 'contract_id')
|
||||||
|
->where('active', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class ImportEvent extends Model
|
|||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'import_id','user_id','event','level','message','context','import_row_id'
|
'import_id', 'user_id', 'event', 'level', 'message', 'context', 'import_row_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class ImportTemplateMapping extends Model
|
|||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'import_template_id', 'entity', 'source_column', 'target_field', 'transform', 'apply_mode', 'options', 'position'
|
'import_template_id', 'entity', 'source_column', 'target_field', 'transform', 'apply_mode', 'options', 'position',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
use Laravel\Scout\Attributes\SearchUsingFullText;
|
||||||
use Laravel\Scout\Searchable;
|
use Laravel\Scout\Searchable;
|
||||||
|
|
||||||
class Person extends Model
|
class Person extends Model
|
||||||
@@ -45,6 +46,7 @@ class Person extends Model
|
|||||||
'group_id',
|
'group_id',
|
||||||
'type_id',
|
'type_id',
|
||||||
'user_id',
|
'user_id',
|
||||||
|
'employer'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
@@ -64,6 +66,14 @@ protected static function booted()
|
|||||||
$person->nu = static::generateUniqueNu();
|
$person->nu = static::generateUniqueNu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static::saving(function (Person $person) {
|
||||||
|
$person->full_name_search = static::buildFullNameSearchPayload(
|
||||||
|
$person->first_name,
|
||||||
|
$person->last_name,
|
||||||
|
$person->full_name
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function makeAllSearchableUsing(Builder $query): Builder
|
protected function makeAllSearchableUsing(Builder $query): Builder
|
||||||
@@ -71,16 +81,20 @@ protected function makeAllSearchableUsing(Builder $query): Builder
|
|||||||
return $query->with(['addresses', 'phones', 'emails']);
|
return $query->with(['addresses', 'phones', 'emails']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[SearchUsingFullText(['full_name_search'], ['config' => 'simple'])]
|
||||||
public function toSearchableArray(): array
|
public function toSearchableArray(): array
|
||||||
{
|
{
|
||||||
return [
|
$columns = [
|
||||||
'first_name' => '',
|
'first_name' => (string) $this->first_name,
|
||||||
'last_name' => '',
|
'last_name' => (string) $this->last_name,
|
||||||
'full_name' => '',
|
'full_name' => (string) $this->full_name,
|
||||||
'person_addresses.address' => '',
|
'person_addresses.address' => '',
|
||||||
'person_phones.nu' => '',
|
'person_phones.nu' => '',
|
||||||
'emails.value' => '',
|
'emails.value' => '',
|
||||||
|
'full_name_search' => (string) $this->full_name_search,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return $columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function phones(): HasMany
|
public function phones(): HasMany
|
||||||
@@ -99,6 +113,14 @@ public function addresses(): HasMany
|
|||||||
->orderBy('id');
|
->orderBy('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function address(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(\App\Models\Person\PersonAddress::class)
|
||||||
|
->with(['type'])
|
||||||
|
->where('active', '=', 1)
|
||||||
|
->oldestOfMany('id');
|
||||||
|
}
|
||||||
|
|
||||||
public function emails(): HasMany
|
public function emails(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(\App\Models\Email::class, 'person_id')
|
return $this->hasMany(\App\Models\Email::class, 'person_id')
|
||||||
@@ -144,4 +166,43 @@ protected static function generateUniqueNu(): string
|
|||||||
|
|
||||||
return $nu;
|
return $nu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function buildFullNameSearchPayload(?string $firstName, ?string $lastName, ?string $fullName): string
|
||||||
|
{
|
||||||
|
$segments = collect([
|
||||||
|
static::joinNameParts($firstName, $lastName),
|
||||||
|
static::joinNameParts($lastName, $firstName),
|
||||||
|
$fullName,
|
||||||
|
])->filter();
|
||||||
|
|
||||||
|
if ($segments->isEmpty()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $segments
|
||||||
|
->map(fn (string $segment): string => static::normalizeSegment($segment))
|
||||||
|
->filter()
|
||||||
|
->unique()
|
||||||
|
->implode(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function joinNameParts(?string $first, ?string $second): ?string
|
||||||
|
{
|
||||||
|
$parts = collect([$first, $second])->filter(fn ($value) => filled($value));
|
||||||
|
|
||||||
|
if ($parts->isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parts->implode(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function normalizeSegment(?string $value): ?string
|
||||||
|
{
|
||||||
|
if (empty($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) Str::of($value)->squish()->lower();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,4 @@ public function persons(): HasMany
|
|||||||
{
|
{
|
||||||
return $this->hasMany(\App\Models\Person\Person::class);
|
return $this->hasMany(\App\Models\Person\Person::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class PersonType extends Model
|
class PersonType extends Model
|
||||||
@@ -14,12 +13,11 @@ class PersonType extends Model
|
|||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
'description'
|
'description',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function persons(): HasMany
|
public function persons(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(\App\Models\Person\Person::class);
|
return $this->hasMany(\App\Models\Person\Person::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class Post extends Model
|
|||||||
public function toSearchableArray()
|
public function toSearchableArray()
|
||||||
{
|
{
|
||||||
$array = $this->toArray();
|
$array = $this->toArray();
|
||||||
|
|
||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,22 +15,24 @@ class Segment extends Model
|
|||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
'active',
|
'active',
|
||||||
'exclude'
|
'exclude',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'active' => 'boolean',
|
'active' => 'boolean',
|
||||||
'exclude' => 'boolean'
|
'exclude' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function contracts(): BelongsToMany {
|
public function contracts(): BelongsToMany
|
||||||
|
{
|
||||||
return $this->belongsToMany(\App\Models\Contract::class);
|
return $this->belongsToMany(\App\Models\Contract::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clientCase(): BelongsToMany {
|
public function clientCase(): BelongsToMany
|
||||||
|
{
|
||||||
return $this->belongsToMany(\App\Models\ClientCase::class)->withTimestamps();
|
return $this->belongsToMany(\App\Models\ClientCase::class)->withTimestamps();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class User extends Authenticatable
|
|||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
|
'active',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +64,7 @@ protected function casts(): array
|
|||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
|
'active' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ protected function isAdmin(User $user): bool
|
|||||||
if (app()->environment('testing')) {
|
if (app()->environment('testing')) {
|
||||||
return true; // simplify for tests
|
return true; // simplify for tests
|
||||||
}
|
}
|
||||||
|
|
||||||
return method_exists($user, 'isAdmin') ? $user->isAdmin() : $user->id === 1; // fallback heuristic
|
return method_exists($user, 'isAdmin') ? $user->isAdmin() : $user->id === 1; // fallback heuristic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
use App\Models\Post;
|
use App\Models\Post;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Auth\Access\Response;
|
|
||||||
|
|
||||||
class PostPolicy
|
class PostPolicy
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,11 +6,14 @@
|
|||||||
use App\Actions\Fortify\ResetUserPassword;
|
use App\Actions\Fortify\ResetUserPassword;
|
||||||
use App\Actions\Fortify\UpdateUserPassword;
|
use App\Actions\Fortify\UpdateUserPassword;
|
||||||
use App\Actions\Fortify\UpdateUserProfileInformation;
|
use App\Actions\Fortify\UpdateUserProfileInformation;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Cache\RateLimiting\Limit;
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Laravel\Fortify\Fortify;
|
use Laravel\Fortify\Fortify;
|
||||||
|
|
||||||
class FortifyServiceProvider extends ServiceProvider
|
class FortifyServiceProvider extends ServiceProvider
|
||||||
@@ -33,6 +36,22 @@ public function boot(): void
|
|||||||
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
|
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
|
||||||
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||||
|
|
||||||
|
Fortify::authenticateUsing(function (Request $request) {
|
||||||
|
$user = User::where('email', $request->email)->first();
|
||||||
|
|
||||||
|
if ($user && Hash::check($request->password, $user->password)) {
|
||||||
|
if (! $user->active) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
Fortify::username() => ['Uporabnik je onemogočen.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
RateLimiter::for('login', function (Request $request) {
|
RateLimiter::for('login', function (Request $request) {
|
||||||
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ public function executeSetting(ArchiveSetting $setting, ?array $context = null,
|
|||||||
$entities = $flat;
|
$entities = $flat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dd($entities);
|
||||||
|
|
||||||
foreach ($entities as $entityDef) {
|
foreach ($entities as $entityDef) {
|
||||||
$rawTable = $entityDef['table'] ?? null;
|
$rawTable = $entityDef['table'] ?? null;
|
||||||
if (! $rawTable) {
|
if (! $rawTable) {
|
||||||
@@ -97,7 +99,7 @@ public function executeSetting(ArchiveSetting $setting, ?array $context = null,
|
|||||||
// Process in batches to avoid locking large tables
|
// Process in batches to avoid locking large tables
|
||||||
while (true) {
|
while (true) {
|
||||||
$query = DB::table($table)->whereNull('deleted_at');
|
$query = DB::table($table)->whereNull('deleted_at');
|
||||||
if (Schema::hasColumn($table, 'active')) {
|
if (Schema::hasColumn($table, 'active') && ! $reactivate) {
|
||||||
$query->where('active', 1);
|
$query->where('active', 1);
|
||||||
}
|
}
|
||||||
// Apply context filters or chain derived filters
|
// Apply context filters or chain derived filters
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ public static function toDate(?string $raw): ?string
|
|||||||
// Rebuild date with corrected year
|
// Rebuild date with corrected year
|
||||||
$month = (int) $dt->format('m');
|
$month = (int) $dt->format('m');
|
||||||
$day = (int) $dt->format('d');
|
$day = (int) $dt->format('d');
|
||||||
|
|
||||||
return sprintf('%04d-%02d-%02d', $year, $month, $day);
|
return sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $dt->format('Y-m-d');
|
return $dt->format('Y-m-d');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,11 @@ class ImportSimulationService
|
|||||||
*/
|
*/
|
||||||
private ?int $clientId = null;
|
private ?int $clientId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History import mode flag (from template meta).
|
||||||
|
*/
|
||||||
|
private bool $historyImport = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public entry: simulate import applying mappings to first $limit rows.
|
* Public entry: simulate import applying mappings to first $limit rows.
|
||||||
* Keeps existing machine keys for backward compatibility, but adds Slovenian
|
* Keeps existing machine keys for backward compatibility, but adds Slovenian
|
||||||
@@ -79,6 +84,7 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
|
|||||||
$simRows = [];
|
$simRows = [];
|
||||||
// Determine keyref behavior for contract.reference from mappings/template
|
// Determine keyref behavior for contract.reference from mappings/template
|
||||||
$tplMeta = optional($import->template)->meta ?? [];
|
$tplMeta = optional($import->template)->meta ?? [];
|
||||||
|
$this->historyImport = (bool) ($tplMeta['history_import'] ?? false);
|
||||||
$contractKeyModeTpl = $tplMeta['contract_key_mode'] ?? null; // e.g. 'reference'
|
$contractKeyModeTpl = $tplMeta['contract_key_mode'] ?? null; // e.g. 'reference'
|
||||||
$contractRefMode = $this->mappingModeForImport($import, 'contract.reference'); // e.g. 'keyref'
|
$contractRefMode = $this->mappingModeForImport($import, 'contract.reference'); // e.g. 'keyref'
|
||||||
foreach ($rows as $idx => $rawValues) {
|
foreach ($rows as $idx => $rawValues) {
|
||||||
@@ -489,6 +495,38 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// History import: auto-ensure account placeholder when contract exists but no account mapping
|
||||||
|
if ($this->historyImport && $existingContract && isset($rowEntities['contract']['id']) && ! isset($rowEntities['account'])) {
|
||||||
|
if (! isset($summaries['account'])) {
|
||||||
|
$summaries['account'] = [
|
||||||
|
'root' => 'account',
|
||||||
|
'total_rows' => 0,
|
||||||
|
'create' => 0,
|
||||||
|
'update' => 0,
|
||||||
|
'missing_ref' => 0,
|
||||||
|
'invalid' => 0,
|
||||||
|
'duplicate' => 0,
|
||||||
|
'duplicate_db' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$summaries['account']['total_rows']++;
|
||||||
|
$summaries['account']['update']++;
|
||||||
|
$ref = $rowEntities['contract']['reference'] ?? null;
|
||||||
|
if ($ref === null || $ref === '') {
|
||||||
|
$ref = 'HIST-'.$rowEntities['contract']['id'];
|
||||||
|
}
|
||||||
|
$rowEntities['account'] = [
|
||||||
|
'reference' => $ref,
|
||||||
|
'exists' => true,
|
||||||
|
'id' => null,
|
||||||
|
'balance_before' => 0,
|
||||||
|
'balance_after' => 0,
|
||||||
|
'action' => 'implicit_history',
|
||||||
|
'action_label' => $translatedActions['implicit'] ?? 'posredno',
|
||||||
|
'history_zeroed' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Payment (affects account balance; may create implicit account)
|
// Payment (affects account balance; may create implicit account)
|
||||||
if (isset($entityRoots['payment'])) {
|
if (isset($entityRoots['payment'])) {
|
||||||
// Inject inferred account if none mapped explicitly
|
// Inject inferred account if none mapped explicitly
|
||||||
@@ -891,7 +929,7 @@ private function simulateContract(callable $val, array $summaries, array $cache,
|
|||||||
'client_case_id' => $contract?->client_case_id,
|
'client_case_id' => $contract?->client_case_id,
|
||||||
'active' => $contract?->active,
|
'active' => $contract?->active,
|
||||||
'deleted_at' => $contract?->deleted_at,
|
'deleted_at' => $contract?->deleted_at,
|
||||||
'action' => $contract ? 'update' : ($reference ? 'create' : 'skip'),
|
'action' => $contract ? ($this->historyImport ? 'skipped_history' : 'update') : ($reference ? 'create' : 'skip'),
|
||||||
];
|
];
|
||||||
$summaries['contract']['total_rows']++;
|
$summaries['contract']['total_rows']++;
|
||||||
if (! $reference) {
|
if (! $reference) {
|
||||||
@@ -902,6 +940,11 @@ private function simulateContract(callable $val, array $summaries, array $cache,
|
|||||||
$summaries['contract']['create']++;
|
$summaries['contract']['create']++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->historyImport && $contract) {
|
||||||
|
$entity['history_reuse'] = true;
|
||||||
|
$entity['message'] = 'Existing contract reused (history import)';
|
||||||
|
}
|
||||||
|
|
||||||
return [$entity, $summaries, $cache];
|
return [$entity, $summaries, $cache];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -931,7 +974,7 @@ private function simulateAccount(callable $val, array $summaries, array $cache,
|
|||||||
'exists' => (bool) $account,
|
'exists' => (bool) $account,
|
||||||
'balance_before' => $account?->balance_amount,
|
'balance_before' => $account?->balance_amount,
|
||||||
'balance_after' => $account?->balance_amount,
|
'balance_after' => $account?->balance_amount,
|
||||||
'action' => $account ? 'update' : ($reference ? 'create' : 'skip'),
|
'action' => $account ? ($this->historyImport ? 'skipped_history' : 'update') : ($reference ? 'create' : 'skip'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Direct balance override support.
|
// Direct balance override support.
|
||||||
@@ -940,7 +983,7 @@ private function simulateAccount(callable $val, array $summaries, array $cache,
|
|||||||
$rawIncoming = $val('account.balance_amount')
|
$rawIncoming = $val('account.balance_amount')
|
||||||
?? $val('accounts.balance_amount')
|
?? $val('accounts.balance_amount')
|
||||||
?? $val('account.balance');
|
?? $val('account.balance');
|
||||||
if ($rawIncoming !== null && $rawIncoming !== '') {
|
if (! $this->historyImport && $rawIncoming !== null && $rawIncoming !== '') {
|
||||||
$rawStr = (string) $rawIncoming;
|
$rawStr = (string) $rawIncoming;
|
||||||
// Remove currency symbols and non numeric punctuation except , . -
|
// Remove currency symbols and non numeric punctuation except , . -
|
||||||
$clean = preg_replace('/[^0-9,\.\-]+/', '', $rawStr) ?? '';
|
$clean = preg_replace('/[^0-9,\.\-]+/', '', $rawStr) ?? '';
|
||||||
@@ -974,6 +1017,19 @@ private function simulateAccount(callable $val, array $summaries, array $cache,
|
|||||||
$summaries['account']['create']++;
|
$summaries['account']['create']++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->historyImport) {
|
||||||
|
// History imports keep balances unchanged and do not update accounts
|
||||||
|
$entity['balance_after'] = $account?->balance_amount ?? 0;
|
||||||
|
$entity['balance_before'] = $account?->balance_amount ?? 0;
|
||||||
|
if ($account) {
|
||||||
|
$entity['message'] = 'Existing account left unchanged (history import)';
|
||||||
|
} else {
|
||||||
|
$entity['balance_after'] = 0;
|
||||||
|
$entity['balance_before'] = 0;
|
||||||
|
$entity['history_zeroed'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [$entity, $summaries, $cache];
|
return [$entity, $summaries, $cache];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1210,6 +1266,10 @@ private function simulateGenericRoot(
|
|||||||
$reference = $val('phone.nu');
|
$reference = $val('phone.nu');
|
||||||
} elseif ($root === 'email') {
|
} elseif ($root === 'email') {
|
||||||
$reference = $val('email.value');
|
$reference = $val('email.value');
|
||||||
|
} elseif ($root === 'activity') {
|
||||||
|
$noteRef = $val('activity.note');
|
||||||
|
$dueRef = $val('activity.due_date');
|
||||||
|
$reference = $noteRef || $dueRef ? trim((string) ($dueRef ?? '')).($noteRef ? ' | '.$noteRef : '') : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1237,7 +1297,9 @@ private function simulateGenericRoot(
|
|||||||
$entity['country'] = $val('address.country') ?? null;
|
$entity['country'] = $val('address.country') ?? null;
|
||||||
break;
|
break;
|
||||||
case 'phone':
|
case 'phone':
|
||||||
$entity['nu'] = $val('phone.nu') ?? null;
|
$rawNu = $val('phone.nu') ?? null;
|
||||||
|
// Strip all non-numeric characters from phone number
|
||||||
|
$entity['nu'] = $rawNu !== null ? preg_replace('/[^0-9]/', '', (string) $rawNu) : null;
|
||||||
break;
|
break;
|
||||||
case 'email':
|
case 'email':
|
||||||
$entity['value'] = $val('email.value') ?? null;
|
$entity['value'] = $val('email.value') ?? null;
|
||||||
@@ -1246,6 +1308,18 @@ private function simulateGenericRoot(
|
|||||||
$entity['title'] = $val('client_case.title') ?? null;
|
$entity['title'] = $val('client_case.title') ?? null;
|
||||||
$entity['status'] = $val('client_case.status') ?? null;
|
$entity['status'] = $val('client_case.status') ?? null;
|
||||||
break;
|
break;
|
||||||
|
case 'case_object':
|
||||||
|
$entity['name'] = $val('case_object.name') ?? null;
|
||||||
|
$entity['description'] = $val('case_object.description') ?? null;
|
||||||
|
$entity['type'] = $val('case_object.type') ?? null;
|
||||||
|
break;
|
||||||
|
case 'activity':
|
||||||
|
$entity['note'] = $val('activity.note') ?? null;
|
||||||
|
$entity['due_date'] = $val('activity.due_date') ?? null;
|
||||||
|
$entity['amount'] = $val('activity.amount') ?? null;
|
||||||
|
$entity['action_id'] = $val('activity.action_id') ?? null;
|
||||||
|
$entity['decision_id'] = $val('activity.decision_id') ?? null;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($verbose) {
|
if ($verbose) {
|
||||||
@@ -1313,7 +1387,8 @@ private function genericIdentityCandidates(string $root, callable $val): array
|
|||||||
case 'phone':
|
case 'phone':
|
||||||
$nu = $val('phone.nu');
|
$nu = $val('phone.nu');
|
||||||
if ($nu) {
|
if ($nu) {
|
||||||
$norm = preg_replace('/\D+/', '', (string) $nu) ?? '';
|
// Strip all non-numeric characters from phone number
|
||||||
|
$norm = preg_replace('/[^0-9]/', '', (string) $nu) ?? '';
|
||||||
|
|
||||||
return $norm ? ['nu:'.$norm] : [];
|
return $norm ? ['nu:'.$norm] : [];
|
||||||
}
|
}
|
||||||
@@ -1346,6 +1421,30 @@ private function genericIdentityCandidates(string $root, callable $val): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
case 'case_object':
|
||||||
|
$ref = $val('case_object.reference');
|
||||||
|
$name = $val('case_object.name');
|
||||||
|
$ids = [];
|
||||||
|
if ($ref) {
|
||||||
|
// Normalize reference (remove spaces)
|
||||||
|
$normRef = preg_replace('/\s+/', '', trim((string) $ref));
|
||||||
|
$ids[] = 'ref:'.$normRef;
|
||||||
|
}
|
||||||
|
if ($name) {
|
||||||
|
$ids[] = 'name:'.mb_strtolower(trim((string) $name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ids;
|
||||||
|
case 'activity':
|
||||||
|
$note = $val('activity.note');
|
||||||
|
$due = $val('activity.due_date');
|
||||||
|
$contractRef = $val('contract.reference');
|
||||||
|
$ids = [];
|
||||||
|
if ($note || $due) {
|
||||||
|
$ids[] = 'activity:'.mb_strtolower(trim((string) ($note ?? ''))).'|'.mb_strtolower(trim((string) ($due ?? ''))).'|'.mb_strtolower(trim((string) ($contractRef ?? '')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ids;
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -1366,7 +1465,8 @@ private function loadExistingGenericIdentities(string $root): array
|
|||||||
case 'phone':
|
case 'phone':
|
||||||
foreach (\App\Models\Person\PersonPhone::query()->pluck('nu') as $p) {
|
foreach (\App\Models\Person\PersonPhone::query()->pluck('nu') as $p) {
|
||||||
if ($p) {
|
if ($p) {
|
||||||
$set['nu:'.preg_replace('/\D+/', '', (string) $p)] = true;
|
// Strip all non-numeric characters from phone number
|
||||||
|
$set['nu:'.preg_replace('/[^0-9]/', '', (string) $p)] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1391,6 +1491,32 @@ private function loadExistingGenericIdentities(string $root): array
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'case_object':
|
||||||
|
foreach (\App\Models\CaseObject::query()->get(['reference', 'name']) as $rec) {
|
||||||
|
if ($rec->reference) {
|
||||||
|
// Normalize reference (remove spaces)
|
||||||
|
$normRef = preg_replace('/\s+/', '', trim((string) $rec->reference));
|
||||||
|
$set['ref:'.$normRef] = true;
|
||||||
|
}
|
||||||
|
if ($rec->name) {
|
||||||
|
$set['name:'.mb_strtolower(trim((string) $rec->name))] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'activity':
|
||||||
|
foreach (\App\Models\Activity::query()->get(['note', 'due_date', 'contract_id', 'client_case_id']) as $rec) {
|
||||||
|
$note = mb_strtolower(trim((string) ($rec->note ?? '')));
|
||||||
|
$due = $rec->due_date ? mb_strtolower(trim((string) $rec->due_date)) : '';
|
||||||
|
$contractRef = null;
|
||||||
|
if ($rec->contract_id) {
|
||||||
|
$contractRef = Contract::where('id', $rec->contract_id)->value('reference');
|
||||||
|
}
|
||||||
|
$key = 'activity:'.$note.'|'.$due.'|'.mb_strtolower(trim((string) ($contractRef ?? '')));
|
||||||
|
if (trim($key, 'activity:|') !== '') {
|
||||||
|
$set[$key] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
// swallow and return what we have
|
// swallow and return what we have
|
||||||
@@ -1411,6 +1537,7 @@ private function modelClassForGeneric(string $root): ?string
|
|||||||
'activity' => \App\Models\Activity::class,
|
'activity' => \App\Models\Activity::class,
|
||||||
'client' => \App\Models\Client::class,
|
'client' => \App\Models\Client::class,
|
||||||
'client_case' => \App\Models\ClientCase::class,
|
'client_case' => \App\Models\ClientCase::class,
|
||||||
|
'case_object' => \App\Models\CaseObject::class,
|
||||||
][$root] ?? null;
|
][$root] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1563,7 +1690,8 @@ private function simulateGenericRootMulti(
|
|||||||
} elseif ($root === 'phone') {
|
} elseif ($root === 'phone') {
|
||||||
$nu = $groupVals('phone', 'nu')[$g] ?? null;
|
$nu = $groupVals('phone', 'nu')[$g] ?? null;
|
||||||
if ($nu) {
|
if ($nu) {
|
||||||
$norm = preg_replace('/\D+/', '', (string) $nu) ?? '';
|
// Strip all non-numeric characters from phone number
|
||||||
|
$norm = preg_replace('/[^0-9]/', '', (string) $nu) ?? '';
|
||||||
if ($norm) {
|
if ($norm) {
|
||||||
$identityCandidates = ['nu:'.$norm];
|
$identityCandidates = ['nu:'.$norm];
|
||||||
}
|
}
|
||||||
@@ -1615,7 +1743,9 @@ private function simulateGenericRootMulti(
|
|||||||
if ($root === 'email') {
|
if ($root === 'email') {
|
||||||
$entity['value'] = $groupVals('email', 'value')[$g] ?? null;
|
$entity['value'] = $groupVals('email', 'value')[$g] ?? null;
|
||||||
} elseif ($root === 'phone') {
|
} elseif ($root === 'phone') {
|
||||||
$entity['nu'] = $groupVals('phone', 'nu')[$g] ?? null;
|
$rawNu = $groupVals('phone', 'nu')[$g] ?? null;
|
||||||
|
// Strip all non-numeric characters from phone number
|
||||||
|
$entity['nu'] = $rawNu !== null ? preg_replace('/[^0-9]/', '', (string) $rawNu) : null;
|
||||||
} elseif ($root === 'address') {
|
} elseif ($root === 'address') {
|
||||||
$entity['address'] = $groupVals('address', 'address')[$g] ?? null;
|
$entity['address'] = $groupVals('address', 'address')[$g] ?? null;
|
||||||
$entity['country'] = $groupVals('address', 'country')[$g] ?? null;
|
$entity['country'] = $groupVals('address', 'country')[$g] ?? null;
|
||||||
@@ -1691,6 +1821,8 @@ private function actionTranslations(): array
|
|||||||
'skip' => 'preskoči',
|
'skip' => 'preskoči',
|
||||||
'implicit' => 'posredno',
|
'implicit' => 'posredno',
|
||||||
'reactivate' => 'reaktiviraj',
|
'reactivate' => 'reaktiviraj',
|
||||||
|
'skipped_history' => 'preskoči (zgodovina)',
|
||||||
|
'implicit_history' => 'posredno (zgodovina)',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ protected function normalizeForSms(string $text): string
|
|||||||
{
|
{
|
||||||
// Replace NBSP (\xC2\xA0 in UTF-8) and tabs with regular space
|
// Replace NBSP (\xC2\xA0 in UTF-8) and tabs with regular space
|
||||||
$text = str_replace(["\u{00A0}", "\t"], ' ', $text);
|
$text = str_replace(["\u{00A0}", "\t"], ' ', $text);
|
||||||
|
|
||||||
// Optionally collapse CRLF to LF (providers typically accept both); keep as-is otherwise
|
// Optionally collapse CRLF to LF (providers typically accept both); keep as-is otherwise
|
||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -3,9 +3,11 @@
|
|||||||
namespace App\Traits;
|
namespace App\Traits;
|
||||||
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
trait Uuid
|
trait Uuid
|
||||||
{
|
{
|
||||||
protected static function boot(){
|
protected static function boot()
|
||||||
|
{
|
||||||
parent::boot();
|
parent::boot();
|
||||||
static::creating(function ($model) {
|
static::creating(function ($model) {
|
||||||
$model->uuid = (string) Str::uuid();
|
$model->uuid = (string) Str::uuid();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
$middleware->web(append: [
|
$middleware->web(append: [
|
||||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||||
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
||||||
|
\App\Http\Middleware\EnsureUserIsActive::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
|
|||||||
+3
-2
@@ -5,7 +5,6 @@
|
|||||||
"keywords": ["laravel", "framework"],
|
"keywords": ["laravel", "framework"],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"tijsverkoyen/css-to-inline-styles": "^2.2",
|
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"arielmejiadev/larapex-charts": "^2.1",
|
"arielmejiadev/larapex-charts": "^2.1",
|
||||||
"diglactic/laravel-breadcrumbs": "^10.0",
|
"diglactic/laravel-breadcrumbs": "^10.0",
|
||||||
@@ -16,9 +15,11 @@
|
|||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/scout": "^10.11",
|
"laravel/scout": "^10.11",
|
||||||
"laravel/tinker": "^2.9",
|
"laravel/tinker": "^2.9",
|
||||||
|
"maatwebsite/excel": "^3.1",
|
||||||
"meilisearch/meilisearch-php": "^1.11",
|
"meilisearch/meilisearch-php": "^1.11",
|
||||||
"robertboes/inertia-breadcrumbs": "dev-laravel-12",
|
"robertboes/inertia-breadcrumbs": "dev-laravel-12",
|
||||||
"tightenco/ziggy": "^2.0"
|
"tightenco/ziggy": "^2.0",
|
||||||
|
"tijsverkoyen/css-to-inline-styles": "^2.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
Generated
+591
-2
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "51fd57123c1b9f51c24f28e04a692ec4",
|
"content-hash": "d29c47a4d6813ee8e80a7c8112c2f17e",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "arielmejiadev/larapex-charts",
|
"name": "arielmejiadev/larapex-charts",
|
||||||
@@ -242,6 +242,162 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-02-09T16:56:22+00:00"
|
"time": "2024-02-09T16:56:22+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "composer/pcre",
|
||||||
|
"version": "3.3.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/composer/pcre.git",
|
||||||
|
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||||
|
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"phpstan/phpstan": "<1.11.10"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^1.12 || ^2",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1 || ^2",
|
||||||
|
"phpunit/phpunit": "^8 || ^9"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"phpstan": {
|
||||||
|
"includes": [
|
||||||
|
"extension.neon"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Composer\\Pcre\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jordi Boggiano",
|
||||||
|
"email": "j.boggiano@seld.be",
|
||||||
|
"homepage": "http://seld.be"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
|
||||||
|
"keywords": [
|
||||||
|
"PCRE",
|
||||||
|
"preg",
|
||||||
|
"regex",
|
||||||
|
"regular expression"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/composer/pcre/issues",
|
||||||
|
"source": "https://github.com/composer/pcre/tree/3.3.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://packagist.com",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/composer",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-11-12T16:29:46+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "composer/semver",
|
||||||
|
"version": "3.4.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/composer/semver.git",
|
||||||
|
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
|
||||||
|
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^5.3.2 || ^7.0 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^1.11",
|
||||||
|
"symfony/phpunit-bridge": "^3 || ^7"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Composer\\Semver\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nils Adermann",
|
||||||
|
"email": "naderman@naderman.de",
|
||||||
|
"homepage": "http://www.naderman.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jordi Boggiano",
|
||||||
|
"email": "j.boggiano@seld.be",
|
||||||
|
"homepage": "http://seld.be"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rob Bast",
|
||||||
|
"email": "rob.bast@gmail.com",
|
||||||
|
"homepage": "http://robbast.nl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Semver library that offers utilities, version constraint parsing and validation.",
|
||||||
|
"keywords": [
|
||||||
|
"semantic",
|
||||||
|
"semver",
|
||||||
|
"validation",
|
||||||
|
"versioning"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"irc": "ircs://irc.libera.chat:6697/composer",
|
||||||
|
"issues": "https://github.com/composer/semver/issues",
|
||||||
|
"source": "https://github.com/composer/semver/tree/3.4.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://packagist.com",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/composer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-20T19:15:30+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "dasprid/enum",
|
"name": "dasprid/enum",
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
@@ -737,6 +893,67 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-03-06T22:45:56+00:00"
|
"time": "2025-03-06T22:45:56+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ezyang/htmlpurifier",
|
||||||
|
"version": "v4.19.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ezyang/htmlpurifier.git",
|
||||||
|
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf",
|
||||||
|
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"cerdic/css-tidy": "^1.7 || ^2.0",
|
||||||
|
"simpletest/simpletest": "dev-master"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
|
||||||
|
"ext-bcmath": "Used for unit conversion and imagecrash protection",
|
||||||
|
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
|
||||||
|
"ext-tidy": "Used for pretty-printing HTML"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"library/HTMLPurifier.composer.php"
|
||||||
|
],
|
||||||
|
"psr-0": {
|
||||||
|
"HTMLPurifier": "library/"
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/library/HTMLPurifier/Language/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-2.1-or-later"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Edward Z. Yang",
|
||||||
|
"email": "admin@htmlpurifier.org",
|
||||||
|
"homepage": "http://ezyang.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Standards compliant HTML filter written in PHP",
|
||||||
|
"homepage": "http://htmlpurifier.org/",
|
||||||
|
"keywords": [
|
||||||
|
"html"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/ezyang/htmlpurifier/issues",
|
||||||
|
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0"
|
||||||
|
},
|
||||||
|
"time": "2025-10-17T16:34:55+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "facade/ignition-contracts",
|
"name": "facade/ignition-contracts",
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -2695,6 +2912,272 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-12-08T08:18:47+00:00"
|
"time": "2024-12-08T08:18:47+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "maatwebsite/excel",
|
||||||
|
"version": "3.1.67",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/SpartnerNL/Laravel-Excel.git",
|
||||||
|
"reference": "e508e34a502a3acc3329b464dad257378a7edb4d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e508e34a502a3acc3329b464dad257378a7edb4d",
|
||||||
|
"reference": "e508e34a502a3acc3329b464dad257378a7edb4d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer/semver": "^3.3",
|
||||||
|
"ext-json": "*",
|
||||||
|
"illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0",
|
||||||
|
"php": "^7.0||^8.0",
|
||||||
|
"phpoffice/phpspreadsheet": "^1.30.0",
|
||||||
|
"psr/simple-cache": "^1.0||^2.0||^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"laravel/scout": "^7.0||^8.0||^9.0||^10.0",
|
||||||
|
"orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0",
|
||||||
|
"predis/predis": "^1.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"aliases": {
|
||||||
|
"Excel": "Maatwebsite\\Excel\\Facades\\Excel"
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"Maatwebsite\\Excel\\ExcelServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Maatwebsite\\Excel\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Patrick Brouwers",
|
||||||
|
"email": "patrick@spartner.nl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Supercharged Excel exports and imports in Laravel",
|
||||||
|
"keywords": [
|
||||||
|
"PHPExcel",
|
||||||
|
"batch",
|
||||||
|
"csv",
|
||||||
|
"excel",
|
||||||
|
"export",
|
||||||
|
"import",
|
||||||
|
"laravel",
|
||||||
|
"php",
|
||||||
|
"phpspreadsheet"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
|
||||||
|
"source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.67"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://laravel-excel.com/commercial-support",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/patrickbrouwers",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-26T09:13:16+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maennchen/zipstream-php",
|
||||||
|
"version": "3.2.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||||
|
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5",
|
||||||
|
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"php-64bit": "^8.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"brianium/paratest": "^7.7",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.86",
|
||||||
|
"guzzlehttp/guzzle": "^7.5",
|
||||||
|
"mikey179/vfsstream": "^1.6",
|
||||||
|
"php-coveralls/php-coveralls": "^2.5",
|
||||||
|
"phpunit/phpunit": "^12.0",
|
||||||
|
"vimeo/psalm": "^6.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"guzzlehttp/psr7": "^2.4",
|
||||||
|
"psr/http-message": "^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"ZipStream\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Paul Duncan",
|
||||||
|
"email": "pabs@pablotron.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jonatan Männchen",
|
||||||
|
"email": "jonatan@maennchen.ch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jesse Donat",
|
||||||
|
"email": "donatj@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "András Kolesár",
|
||||||
|
"email": "kolesar@kolesar.hu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
|
||||||
|
"keywords": [
|
||||||
|
"stream",
|
||||||
|
"zip"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||||
|
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/maennchen",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-12-10T09:58:31+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "markbaker/complex",
|
||||||
|
"version": "3.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MarkBaker/PHPComplex.git",
|
||||||
|
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||||
|
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||||
|
"phpcompatibility/php-compatibility": "^9.3",
|
||||||
|
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Complex\\": "classes/src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Baker",
|
||||||
|
"email": "mark@lange.demon.co.uk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP Class for working with complex numbers",
|
||||||
|
"homepage": "https://github.com/MarkBaker/PHPComplex",
|
||||||
|
"keywords": [
|
||||||
|
"complex",
|
||||||
|
"mathematics"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
|
||||||
|
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
|
||||||
|
},
|
||||||
|
"time": "2022-12-06T16:21:08+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "markbaker/matrix",
|
||||||
|
"version": "3.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MarkBaker/PHPMatrix.git",
|
||||||
|
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
|
||||||
|
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||||
|
"phpcompatibility/php-compatibility": "^9.3",
|
||||||
|
"phpdocumentor/phpdocumentor": "2.*",
|
||||||
|
"phploc/phploc": "^4.0",
|
||||||
|
"phpmd/phpmd": "2.*",
|
||||||
|
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||||
|
"sebastian/phpcpd": "^4.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Matrix\\": "classes/src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Baker",
|
||||||
|
"email": "mark@demon-angel.eu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP Class for working with matrices",
|
||||||
|
"homepage": "https://github.com/MarkBaker/PHPMatrix",
|
||||||
|
"keywords": [
|
||||||
|
"mathematics",
|
||||||
|
"matrix",
|
||||||
|
"vector"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
|
||||||
|
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
|
||||||
|
},
|
||||||
|
"time": "2022-12-02T22:17:43+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "meilisearch/meilisearch-php",
|
"name": "meilisearch/meilisearch-php",
|
||||||
"version": "v1.13.0",
|
"version": "v1.13.0",
|
||||||
@@ -3475,6 +3958,112 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-10-02T11:20:13+00:00"
|
"time": "2024-10-02T11:20:13+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "phpoffice/phpspreadsheet",
|
||||||
|
"version": "1.30.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||||
|
"reference": "fa8257a579ec623473eabfe49731de5967306c4c"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c",
|
||||||
|
"reference": "fa8257a579ec623473eabfe49731de5967306c4c",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer/pcre": "^1||^2||^3",
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-fileinfo": "*",
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"ext-libxml": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"ext-xml": "*",
|
||||||
|
"ext-xmlreader": "*",
|
||||||
|
"ext-xmlwriter": "*",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"ezyang/htmlpurifier": "^4.15",
|
||||||
|
"maennchen/zipstream-php": "^2.1 || ^3.0",
|
||||||
|
"markbaker/complex": "^3.0",
|
||||||
|
"markbaker/matrix": "^3.0",
|
||||||
|
"php": ">=7.4.0 <8.5.0",
|
||||||
|
"psr/http-client": "^1.0",
|
||||||
|
"psr/http-factory": "^1.0",
|
||||||
|
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||||
|
"dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.2",
|
||||||
|
"mitoteam/jpgraph": "^10.3",
|
||||||
|
"mpdf/mpdf": "^8.1.1",
|
||||||
|
"phpcompatibility/php-compatibility": "^9.3",
|
||||||
|
"phpstan/phpstan": "^1.1",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.0",
|
||||||
|
"phpunit/phpunit": "^8.5 || ^9.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7",
|
||||||
|
"tecnickcom/tcpdf": "^6.5"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
|
||||||
|
"ext-intl": "PHP Internationalization Functions",
|
||||||
|
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
|
||||||
|
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
|
||||||
|
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Maarten Balliauw",
|
||||||
|
"homepage": "https://blog.maartenballiauw.be"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mark Baker",
|
||||||
|
"homepage": "https://markbakeruk.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Franck Lefevre",
|
||||||
|
"homepage": "https://rootslabs.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Erik Tilt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Adrien Crivelli"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
|
||||||
|
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
|
||||||
|
"keywords": [
|
||||||
|
"OpenXML",
|
||||||
|
"excel",
|
||||||
|
"gnumeric",
|
||||||
|
"ods",
|
||||||
|
"php",
|
||||||
|
"spreadsheet",
|
||||||
|
"xls",
|
||||||
|
"xlsx"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||||
|
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1"
|
||||||
|
},
|
||||||
|
"time": "2025-10-26T16:01:04+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoption/phpoption",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
@@ -10335,6 +10924,6 @@
|
|||||||
"platform": {
|
"platform": {
|
||||||
"php": "^8.2"
|
"php": "^8.2"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -98,7 +98,7 @@
|
|||||||
'options' => [
|
'options' => [
|
||||||
'LC_COLLATE' => env('PGSQL_LC_COLLATE', 'en_US.UTF-8'),
|
'LC_COLLATE' => env('PGSQL_LC_COLLATE', 'en_US.UTF-8'),
|
||||||
'LC_CTYPE' => env('PGSQL_LC_CTYPE', 'en_US.UTF-8'),
|
'LC_CTYPE' => env('PGSQL_LC_CTYPE', 'en_US.UTF-8'),
|
||||||
]
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'sqlsrv' => [
|
'sqlsrv' => [
|
||||||
|
|||||||
@@ -14,6 +14,6 @@
|
|||||||
|
|
||||||
'colors' => [
|
'colors' => [
|
||||||
'#008FFB', '#00E396', '#feb019', '#ff455f', '#775dd0', '#80effe',
|
'#008FFB', '#00E396', '#feb019', '#ff455f', '#775dd0', '#80effe',
|
||||||
'#0077B5', '#ff6384', '#c9cbcf', '#0057ff', '00a9f4', '#2ccdc9', '#5e72e4'
|
'#0077B5', '#ff6384', '#c9cbcf', '#0057ff', '00a9f4', '#2ccdc9', '#5e72e4',
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
||||||
use App\Models\Segment;
|
use App\Models\Segment;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Action>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Action>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ public function up(): void
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
Schema::create('debts', function (Blueprint $table) {
|
Schema::create('debts', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('reference', 125)->nullable();
|
$table->string('reference', 125)->nullable();
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
/**
|
/**
|
||||||
* Run the migrations.
|
* Run the migrations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('payment_types', function (Blueprint $table) {
|
Schema::create('payment_types', function (Blueprint $table) {
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('accounts', function (Blueprint $table) {
|
Schema::table('accounts', function (Blueprint $table) {
|
||||||
$table->decimal("initial_amount", 20, 4)->default(0);
|
$table->decimal('initial_amount', 20, 4)->default(0);
|
||||||
$table->decimal("balance_amount", 20, 4)->default(0);
|
$table->decimal('balance_amount', 20, 4)->default(0);
|
||||||
$table->date("promise_date")->nullable();
|
$table->date('promise_date')->nullable();
|
||||||
$table->index('balance_amount');
|
$table->index('balance_amount');
|
||||||
$table->index('promise_date');
|
$table->index('promise_date');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration {
|
return new class extends Migration
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* Run the migrations.
|
* Run the migrations.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ private static function hasIndex(string $table, string $index): bool
|
|||||||
$connection = Schema::getConnection();
|
$connection = Schema::getConnection();
|
||||||
$schemaManager = $connection->getDoctrineSchemaManager();
|
$schemaManager = $connection->getDoctrineSchemaManager();
|
||||||
$doctrineTable = $schemaManager->listTableDetails($table);
|
$doctrineTable = $schemaManager->listTableDetails($table);
|
||||||
|
|
||||||
return $doctrineTable->hasIndex($index);
|
return $doctrineTable->hasIndex($index);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public function up(): void
|
|||||||
if (is_string($row->nu) && preg_match('/^[A-Za-z0-9]{6}$/', $row->nu)) {
|
if (is_string($row->nu) && preg_match('/^[A-Za-z0-9]{6}$/', $row->nu)) {
|
||||||
if (! isset($used[$row->nu])) {
|
if (! isset($used[$row->nu])) {
|
||||||
$used[$row->nu] = true;
|
$used[$row->nu] = true;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// duplicate will be regenerated below
|
// duplicate will be regenerated below
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ public function up(): void
|
|||||||
// Backfill entity from target_field's first segment where possible
|
// Backfill entity from target_field's first segment where possible
|
||||||
DB::table('import_mappings')->orderBy('id')->chunkById(1000, function ($rows) {
|
DB::table('import_mappings')->orderBy('id')->chunkById(1000, function ($rows) {
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
if (!empty($row->entity)) continue;
|
if (! empty($row->entity)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$entity = null;
|
$entity = null;
|
||||||
if (! empty($row->target_field)) {
|
if (! empty($row->target_field)) {
|
||||||
$parts = explode('.', $row->target_field);
|
$parts = explode('.', $row->target_field);
|
||||||
@@ -49,7 +51,10 @@ public function down(): void
|
|||||||
Schema::table('import_mappings', function (Blueprint $table) {
|
Schema::table('import_mappings', function (Blueprint $table) {
|
||||||
if (Schema::hasColumn('import_mappings', 'entity')) {
|
if (Schema::hasColumn('import_mappings', 'entity')) {
|
||||||
// drop composite index if exists
|
// drop composite index if exists
|
||||||
try { $table->dropIndex(['import_id', 'entity']); } catch (\Throwable $e) { /* ignore */ }
|
try {
|
||||||
|
$table->dropIndex(['import_id', 'entity']);
|
||||||
|
} catch (\Throwable $e) { /* ignore */
|
||||||
|
}
|
||||||
$table->dropColumn('entity');
|
$table->dropColumn('entity');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ public function up(): void
|
|||||||
// Backfill entity from target_field first segment
|
// Backfill entity from target_field first segment
|
||||||
DB::table('import_template_mappings')->orderBy('id')->chunkById(1000, function ($rows) {
|
DB::table('import_template_mappings')->orderBy('id')->chunkById(1000, function ($rows) {
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
if (!empty($row->entity)) continue;
|
if (! empty($row->entity)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$entity = null;
|
$entity = null;
|
||||||
if (! empty($row->target_field)) {
|
if (! empty($row->target_field)) {
|
||||||
$parts = explode('.', $row->target_field);
|
$parts = explode('.', $row->target_field);
|
||||||
@@ -47,7 +49,10 @@ public function down(): void
|
|||||||
{
|
{
|
||||||
Schema::table('import_template_mappings', function (Blueprint $table) {
|
Schema::table('import_template_mappings', function (Blueprint $table) {
|
||||||
if (Schema::hasColumn('import_template_mappings', 'entity')) {
|
if (Schema::hasColumn('import_template_mappings', 'entity')) {
|
||||||
try { $table->dropIndex(['import_template_id', 'entity']); } catch (\Throwable $e) { /* ignore */ }
|
try {
|
||||||
|
$table->dropIndex(['import_template_id', 'entity']);
|
||||||
|
} catch (\Throwable $e) { /* ignore */
|
||||||
|
}
|
||||||
$table->dropColumn('entity');
|
$table->dropColumn('entity');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration {
|
return new class extends Migration
|
||||||
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('field_job_settings', function (Blueprint $table) {
|
Schema::create('field_job_settings', function (Blueprint $table) {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration {
|
return new class extends Migration
|
||||||
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('field_jobs', function (Blueprint $table) {
|
Schema::create('field_jobs', function (Blueprint $table) {
|
||||||
|
|||||||
+5
-1
@@ -26,7 +26,11 @@ public function up(): void
|
|||||||
|
|
||||||
$keepFirst = true;
|
$keepFirst = true;
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
if ($keepFirst) { $keepFirst = false; continue; }
|
if ($keepFirst) {
|
||||||
|
$keepFirst = false;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$base = mb_substr($row->reference, 0, 120);
|
$base = mb_substr($row->reference, 0, 120);
|
||||||
$newRef = $base.'-'.$row->id;
|
$newRef = $base.'-'.$row->id;
|
||||||
DB::table('contracts')->where('id', $row->id)->update(['reference' => $newRef]);
|
DB::table('contracts')->where('id', $row->id)->update(['reference' => $newRef]);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->boolean('active')->default(true)->after('email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('person', function (Blueprint $table) {
|
||||||
|
$table->text('full_name_search')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->backfillSearchColumn();
|
||||||
|
|
||||||
|
if ($this->isPostgres()) {
|
||||||
|
DB::statement(<<<'SQL'
|
||||||
|
ALTER TABLE person
|
||||||
|
ADD COLUMN full_name_search_vector tsvector
|
||||||
|
GENERATED ALWAYS AS (to_tsvector('simple', coalesce(full_name_search, '')))
|
||||||
|
STORED
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
DB::statement('CREATE INDEX person_full_name_search_vector_idx ON person USING GIN (full_name_search_vector)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if ($this->isPostgres()) {
|
||||||
|
DB::statement('DROP INDEX IF EXISTS person_full_name_search_vector_idx');
|
||||||
|
DB::statement('ALTER TABLE person DROP COLUMN IF EXISTS full_name_search_vector');
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('person', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('full_name_search');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function backfillSearchColumn(): void
|
||||||
|
{
|
||||||
|
DB::table('person')
|
||||||
|
->select('id', 'first_name', 'last_name', 'full_name')
|
||||||
|
->lazyById()
|
||||||
|
->each(function ($row): void {
|
||||||
|
DB::table('person')
|
||||||
|
->where('id', $row->id)
|
||||||
|
->update(['full_name_search' => $this->buildSearchValue($row)]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildSearchValue(object $row): string
|
||||||
|
{
|
||||||
|
$segments = array_filter([
|
||||||
|
$this->joinParts($row->first_name ?? null, $row->last_name ?? null),
|
||||||
|
$this->joinParts($row->last_name ?? null, $row->first_name ?? null),
|
||||||
|
$row->full_name ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($segments)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = array_unique(array_map(function (string $value): string {
|
||||||
|
$collapsed = preg_replace('/\s+/u', ' ', trim($value)) ?: '';
|
||||||
|
|
||||||
|
return mb_strtolower($collapsed);
|
||||||
|
}, $segments));
|
||||||
|
|
||||||
|
return trim(implode(' ', array_filter($normalized)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function joinParts(?string $first, ?string $second): ?string
|
||||||
|
{
|
||||||
|
$parts = array_filter([$first, $second], fn ($part) => filled($part));
|
||||||
|
|
||||||
|
if (empty($parts)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim(implode(' ', $parts));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isPostgres(): bool
|
||||||
|
{
|
||||||
|
return DB::connection()->getDriverName() === 'pgsql';
|
||||||
|
}
|
||||||
|
};
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('field_jobs', function (Blueprint $table) {
|
||||||
|
$table->boolean('added_activity')->default(false);
|
||||||
|
$table->timestamp('last_activity')->nullable()->default(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('field_jobs', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('last_activity');
|
||||||
|
$table->dropColumn('added_activity');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('person', function (Blueprint $table){
|
||||||
|
$table->string('employer', 125)->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('person', function (Blueprint $table){
|
||||||
|
$table->dropColumn('employer');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Add a generated tsvector column for fulltext search
|
||||||
|
DB::statement("
|
||||||
|
ALTER TABLE person_addresses
|
||||||
|
ADD COLUMN search_vector tsvector
|
||||||
|
GENERATED ALWAYS AS (
|
||||||
|
to_tsvector('simple',
|
||||||
|
coalesce(address, '') || ' ' ||
|
||||||
|
coalesce(post_code, '') || ' ' ||
|
||||||
|
coalesce(city, '')
|
||||||
|
)
|
||||||
|
) STORED
|
||||||
|
");
|
||||||
|
|
||||||
|
// Create GIN index on the tsvector column for fast fulltext search
|
||||||
|
DB::statement('CREATE INDEX person_addresses_search_vector_idx ON person_addresses USING GIN(search_vector)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('person_addresses', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('person_addresses_search_vector_idx');
|
||||||
|
$table->dropColumn('search_vector');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class AccountSeeder extends Seeder
|
class AccountSeeder extends Seeder
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
use App\Models\Action;
|
use App\Models\Action;
|
||||||
use App\Models\Decision;
|
use App\Models\Decision;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class ActionSeeder extends Seeder
|
class ActionSeeder extends Seeder
|
||||||
@@ -17,31 +16,31 @@ public function run(): void
|
|||||||
Action::create([
|
Action::create([
|
||||||
'name' => 'KLIC - IZHODNI',
|
'name' => 'KLIC - IZHODNI',
|
||||||
'color_tag' => '',
|
'color_tag' => '',
|
||||||
'segment_id' => 1
|
'segment_id' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Action::create([
|
Action::create([
|
||||||
'name' => 'KLIC - VHODNI',
|
'name' => 'KLIC - VHODNI',
|
||||||
'color_tag' => '',
|
'color_tag' => '',
|
||||||
'segment_id' => 1
|
'segment_id' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Action::create([
|
Action::create([
|
||||||
'name' => 'ePOŠTA',
|
'name' => 'ePOŠTA',
|
||||||
'color_tag' => '',
|
'color_tag' => '',
|
||||||
'segment_id' => 1
|
'segment_id' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Action::create([
|
Action::create([
|
||||||
'name' => 'VROČANJE',
|
'name' => 'VROČANJE',
|
||||||
'color_tag' => '',
|
'color_tag' => '',
|
||||||
'segment_id' => 1
|
'segment_id' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Action::create([
|
Action::create([
|
||||||
'name' => 'SMS',
|
'name' => 'SMS',
|
||||||
'color_tag' => '',
|
'color_tag' => '',
|
||||||
'segment_id' => 1
|
'segment_id' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ------- 1
|
// ------- 1
|
||||||
@@ -52,12 +51,12 @@ public function run(): void
|
|||||||
|
|
||||||
\DB::table('action_decision')->insert([
|
\DB::table('action_decision')->insert([
|
||||||
'action_id' => 1,
|
'action_id' => 1,
|
||||||
'decision_id' => 1
|
'decision_id' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
\DB::table('action_decision')->insert([
|
\DB::table('action_decision')->insert([
|
||||||
'action_id' => 2,
|
'action_id' => 2,
|
||||||
'decision_id' => 1
|
'decision_id' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ------- 2
|
// ------- 2
|
||||||
@@ -68,7 +67,7 @@ public function run(): void
|
|||||||
|
|
||||||
\DB::table('action_decision')->insert([
|
\DB::table('action_decision')->insert([
|
||||||
'action_id' => 3,
|
'action_id' => 3,
|
||||||
'decision_id' => 2
|
'decision_id' => 2,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// -------- 3
|
// -------- 3
|
||||||
@@ -79,7 +78,7 @@ public function run(): void
|
|||||||
|
|
||||||
\DB::table('action_decision')->insert([
|
\DB::table('action_decision')->insert([
|
||||||
'action_id' => 3,
|
'action_id' => 3,
|
||||||
'decision_id' => 3
|
'decision_id' => 3,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --------- 4
|
// --------- 4
|
||||||
@@ -90,7 +89,7 @@ public function run(): void
|
|||||||
|
|
||||||
\DB::table('action_decision')->insert([
|
\DB::table('action_decision')->insert([
|
||||||
'action_id' => 4,
|
'action_id' => 4,
|
||||||
'decision_id' => 4
|
'decision_id' => 4,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --------- 5
|
// --------- 5
|
||||||
@@ -101,7 +100,7 @@ public function run(): void
|
|||||||
|
|
||||||
\DB::table('action_decision')->insert([
|
\DB::table('action_decision')->insert([
|
||||||
'action_id' => 4,
|
'action_id' => 4,
|
||||||
'decision_id' => 5
|
'decision_id' => 5,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --------- 6
|
// --------- 6
|
||||||
@@ -112,7 +111,7 @@ public function run(): void
|
|||||||
|
|
||||||
\DB::table('action_decision')->insert([
|
\DB::table('action_decision')->insert([
|
||||||
'action_id' => 5,
|
'action_id' => 5,
|
||||||
'decision_id' => 6
|
'decision_id' => 6,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --------- 7
|
// --------- 7
|
||||||
@@ -123,7 +122,7 @@ public function run(): void
|
|||||||
|
|
||||||
\DB::table('action_decision')->insert([
|
\DB::table('action_decision')->insert([
|
||||||
'action_id' => 5,
|
'action_id' => 5,
|
||||||
'decision_id' => 7
|
'decision_id' => 7,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class ActivitySeeder extends Seeder
|
class ActivitySeeder extends Seeder
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class ClientCaseSeeder extends Seeder
|
class ClientCaseSeeder extends Seeder
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class ClientSeeder extends Seeder
|
class ClientSeeder extends Seeder
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Contract;
|
|
||||||
use App\Models\ContractType;
|
use App\Models\ContractType;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class ContractSeeder extends Seeder
|
class ContractSeeder extends Seeder
|
||||||
@@ -12,13 +10,11 @@ class ContractSeeder extends Seeder
|
|||||||
/**
|
/**
|
||||||
* Run the database seeds.
|
* Run the database seeds.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$contractType = [
|
$contractType = [
|
||||||
['name' => 'delivery', 'description' => ''],
|
['name' => 'delivery', 'description' => ''],
|
||||||
[ 'name' => 'leasing', 'description' => '']
|
['name' => 'leasing', 'description' => ''],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($contractType as $ct) {
|
foreach ($contractType as $ct) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class DebtSeeder extends Seeder
|
class DebtSeeder extends Seeder
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class DecisionSeeder extends Seeder
|
class DecisionSeeder extends Seeder
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public function run(): void
|
|||||||
'key' => 'person',
|
'key' => 'person',
|
||||||
'canonical_root' => 'person',
|
'canonical_root' => 'person',
|
||||||
'label' => 'Person',
|
'label' => 'Person',
|
||||||
'fields' => ['first_name', 'last_name', 'full_name', 'gender', 'birthday', 'tax_number', 'social_security_number', 'description'],
|
'fields' => ['first_name', 'last_name', 'full_name', 'gender', 'birthday', 'tax_number', 'social_security_number', 'description', 'employer'],
|
||||||
'field_aliases' => [
|
'field_aliases' => [
|
||||||
'dob' => 'birthday',
|
'dob' => 'birthday',
|
||||||
'date_of_birth' => 'birthday',
|
'date_of_birth' => 'birthday',
|
||||||
@@ -30,6 +30,7 @@ public function run(): void
|
|||||||
['pattern' => '/^(spol|gender)\b/i', 'field' => 'gender'],
|
['pattern' => '/^(spol|gender)\b/i', 'field' => 'gender'],
|
||||||
['pattern' => '/^(rojstvo|datum\s*rojstva|dob|birth|birthday|date\s*of\s*birth)\b/i', 'field' => 'birthday'],
|
['pattern' => '/^(rojstvo|datum\s*rojstva|dob|birth|birthday|date\s*of\s*birth)\b/i', 'field' => 'birthday'],
|
||||||
['pattern' => '/^(komentar|opis|opomba|comment|description|note)\b/i', 'field' => 'description'],
|
['pattern' => '/^(komentar|opis|opomba|comment|description|note)\b/i', 'field' => 'description'],
|
||||||
|
['pattern' => '/^(delodajalec|služba)\b/i', 'field' => 'employer']
|
||||||
],
|
],
|
||||||
'ui' => ['order' => 1],
|
'ui' => ['order' => 1],
|
||||||
],
|
],
|
||||||
@@ -125,6 +126,21 @@ public function run(): void
|
|||||||
],
|
],
|
||||||
'ui' => ['order' => 7],
|
'ui' => ['order' => 7],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => 'case_objects',
|
||||||
|
'canonical_root' => 'case_object',
|
||||||
|
'label' => 'Case Objects',
|
||||||
|
'fields' => ['reference', 'name', 'description', 'type', 'contract_id'],
|
||||||
|
'aliases' => ['case_object', 'case_objects', 'object', 'objects', 'predmet', 'predmeti'],
|
||||||
|
'rules' => [
|
||||||
|
['pattern' => '/^(sklic|reference|ref)\b/i', 'field' => 'reference'],
|
||||||
|
['pattern' => '/^(ime|naziv|name|title)\b/i', 'field' => 'name'],
|
||||||
|
['pattern' => '/^(tip|vrsta|type|kind)\b/i', 'field' => 'type'],
|
||||||
|
['pattern' => '/^(komentar|opis|opomba|comment|description|note)\b/i', 'field' => 'description'],
|
||||||
|
['pattern' => '/^(contract\s*id|contract_id|pogodba\s*id|pogodba_id)\b/i', 'field' => 'contract_id'],
|
||||||
|
],
|
||||||
|
'ui' => ['order' => 8],
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'key' => 'payments',
|
'key' => 'payments',
|
||||||
'canonical_root' => 'payment',
|
'canonical_root' => 'payment',
|
||||||
@@ -158,7 +174,30 @@ public function run(): void
|
|||||||
['pattern' => '/^(datum|date|paid\s*at|payment\s*date)\b/i', 'field' => 'payment_date'],
|
['pattern' => '/^(datum|date|paid\s*at|payment\s*date)\b/i', 'field' => 'payment_date'],
|
||||||
['pattern' => '/^(znesek|amount|vplacilo|vplačilo|placilo|plačilo)\b/i', 'field' => 'amount'],
|
['pattern' => '/^(znesek|amount|vplacilo|vplačilo|placilo|plačilo)\b/i', 'field' => 'amount'],
|
||||||
],
|
],
|
||||||
'ui' => ['order' => 8],
|
'ui' => ['order' => 9],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'activities',
|
||||||
|
'canonical_root' => 'activity',
|
||||||
|
'label' => 'Activities',
|
||||||
|
'fields' => ['note', 'due_date', 'amount', 'action_id', 'decision_id', 'contract_id', 'client_case_id', 'user_id'],
|
||||||
|
'field_aliases' => [
|
||||||
|
'opis' => 'note',
|
||||||
|
'datum' => 'due_date',
|
||||||
|
'rok' => 'due_date',
|
||||||
|
'znesek' => 'amount',
|
||||||
|
],
|
||||||
|
'aliases' => ['activity', 'activities', 'opravilo', 'opravila'],
|
||||||
|
'rules' => [
|
||||||
|
['pattern' => '/^(aktivnost|activity|note|opis)\b/i', 'field' => 'note'],
|
||||||
|
['pattern' => '/^(rok|due|datum|date)\b/i', 'field' => 'due_date'],
|
||||||
|
['pattern' => '/^(znesek|amount|vrednost|value)\b/i', 'field' => 'amount'],
|
||||||
|
['pattern' => '/^(akcija|action)\b/i', 'field' => 'action_id'],
|
||||||
|
['pattern' => '/^(odlocitev|odločitev|decision)\b/i', 'field' => 'decision_id'],
|
||||||
|
['pattern' => '/^(pogodba|contract)\b/i', 'field' => 'contract_id'],
|
||||||
|
['pattern' => '/^(primer|case)\b/i', 'field' => 'client_case_id'],
|
||||||
|
],
|
||||||
|
'ui' => ['order' => 10],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -155,5 +155,42 @@ public function run(): void
|
|||||||
'options' => $map['options'] ?? null,
|
'options' => $map['options'] ?? null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Activities linked to contracts demo
|
||||||
|
$activities = ImportTemplate::query()->firstOrCreate([
|
||||||
|
'name' => 'Activities CSV (contract linked)',
|
||||||
|
], [
|
||||||
|
'uuid' => (string) Str::uuid(),
|
||||||
|
'description' => 'Activities import linked to existing contracts via reference.',
|
||||||
|
'source_type' => 'csv',
|
||||||
|
'default_record_type' => 'activity',
|
||||||
|
'sample_headers' => ['contract_reference', 'note', 'due_date', 'amount', 'action', 'decision', 'user_email'],
|
||||||
|
'is_active' => true,
|
||||||
|
'meta' => [
|
||||||
|
'delimiter' => ',',
|
||||||
|
'enclosure' => '"',
|
||||||
|
'escape' => '\\',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$activityMappings = [
|
||||||
|
['source_column' => 'contract_reference', 'target_field' => 'contract.reference', 'position' => 1],
|
||||||
|
['source_column' => 'note', 'target_field' => 'activity.note', 'position' => 2],
|
||||||
|
['source_column' => 'due_date', 'target_field' => 'activity.due_date', 'position' => 3],
|
||||||
|
['source_column' => 'amount', 'target_field' => 'activity.amount', 'position' => 4],
|
||||||
|
['source_column' => 'action', 'target_field' => 'activity.action_id', 'position' => 5],
|
||||||
|
['source_column' => 'decision', 'target_field' => 'activity.decision_id', 'position' => 6],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($activityMappings as $map) {
|
||||||
|
ImportTemplateMapping::firstOrCreate([
|
||||||
|
'import_template_id' => $activities->id,
|
||||||
|
'source_column' => $map['source_column'],
|
||||||
|
], [
|
||||||
|
'target_field' => $map['target_field'],
|
||||||
|
'position' => $map['position'],
|
||||||
|
'options' => $map['options'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class PaymentSeeder extends Seeder
|
class PaymentSeeder extends Seeder
|
||||||
|
|||||||
@@ -20,30 +20,29 @@ public function run(): void
|
|||||||
//
|
//
|
||||||
$personTypes = [
|
$personTypes = [
|
||||||
['name' => 'legal', 'description' => ''],
|
['name' => 'legal', 'description' => ''],
|
||||||
[ 'name' => 'natural', 'description' => '']
|
['name' => 'natural', 'description' => ''],
|
||||||
];
|
];
|
||||||
|
|
||||||
$personGroups = [
|
$personGroups = [
|
||||||
['name' => 'naročnik', 'description' => '', 'color_tag' => 'blue-500'],
|
['name' => 'naročnik', 'description' => '', 'color_tag' => 'blue-500'],
|
||||||
[ 'name' => 'primer naročnika', 'description' => '', 'color_tag' => 'red-400']
|
['name' => 'primer naročnika', 'description' => '', 'color_tag' => 'red-400'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$phoneTypes = [
|
$phoneTypes = [
|
||||||
['name' => 'mobile', 'description' => ''],
|
['name' => 'mobile', 'description' => ''],
|
||||||
[ 'name' => 'telephone', 'description' => '']
|
['name' => 'telephone', 'description' => ''],
|
||||||
];
|
];
|
||||||
|
|
||||||
$addressTypes = [
|
$addressTypes = [
|
||||||
['name' => 'permanent', 'description' => ''],
|
['name' => 'permanent', 'description' => ''],
|
||||||
[ 'name' => 'temporary', 'description' => '']
|
['name' => 'temporary', 'description' => ''],
|
||||||
];
|
];
|
||||||
|
|
||||||
$contractTypes = [
|
$contractTypes = [
|
||||||
['name' => 'early', 'description' => ''],
|
['name' => 'early', 'description' => ''],
|
||||||
['name' => 'hard', 'description' => '']
|
['name' => 'hard', 'description' => ''],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
foreach ($personTypes as $pt) {
|
foreach ($personTypes as $pt) {
|
||||||
PersonType::create($pt);
|
PersonType::create($pt);
|
||||||
}
|
}
|
||||||
@@ -77,7 +76,7 @@ public function run(): void
|
|||||||
'description' => 'sdwwf',
|
'description' => 'sdwwf',
|
||||||
'group_id' => 1,
|
'group_id' => 1,
|
||||||
'type_id' => 1,
|
'type_id' => 1,
|
||||||
'user_id' => 1
|
'user_id' => 1,
|
||||||
])->client()->create();
|
])->client()->create();
|
||||||
|
|
||||||
// debtors
|
// debtors
|
||||||
@@ -93,9 +92,9 @@ public function run(): void
|
|||||||
'description' => 'sdwwf',
|
'description' => 'sdwwf',
|
||||||
'group_id' => 2,
|
'group_id' => 2,
|
||||||
'type_id' => 2,
|
'type_id' => 2,
|
||||||
'user_id' => 1
|
'user_id' => 1,
|
||||||
])->clientCase()->create([
|
])->clientCase()->create([
|
||||||
'client_id' => 1
|
'client_id' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Person::create([
|
Person::create([
|
||||||
@@ -110,9 +109,9 @@ public function run(): void
|
|||||||
'description' => 'dw323',
|
'description' => 'dw323',
|
||||||
'group_id' => 2,
|
'group_id' => 2,
|
||||||
'type_id' => 2,
|
'type_id' => 2,
|
||||||
'user_id' => 1
|
'user_id' => 1,
|
||||||
])->clientCase()->create([
|
])->clientCase()->create([
|
||||||
'client_id' => 1
|
'client_id' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// client
|
// client
|
||||||
@@ -128,7 +127,7 @@ public function run(): void
|
|||||||
'description' => 'sdwwf',
|
'description' => 'sdwwf',
|
||||||
'group_id' => 1,
|
'group_id' => 1,
|
||||||
'type_id' => 1,
|
'type_id' => 1,
|
||||||
'user_id' => 1
|
'user_id' => 1,
|
||||||
])->client()->create();
|
])->client()->create();
|
||||||
|
|
||||||
// debtors
|
// debtors
|
||||||
@@ -144,10 +143,10 @@ public function run(): void
|
|||||||
'description' => 'sdwwf',
|
'description' => 'sdwwf',
|
||||||
'group_id' => 2,
|
'group_id' => 2,
|
||||||
'type_id' => 2,
|
'type_id' => 2,
|
||||||
'user_id' => 1
|
'user_id' => 1,
|
||||||
])->clientCase()->create(
|
])->clientCase()->create(
|
||||||
[
|
[
|
||||||
'client_id' => 2
|
'client_id' => 2,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -163,10 +162,10 @@ public function run(): void
|
|||||||
'description' => 'dw323',
|
'description' => 'dw323',
|
||||||
'group_id' => 2,
|
'group_id' => 2,
|
||||||
'type_id' => 1,
|
'type_id' => 1,
|
||||||
'user_id' => 1
|
'user_id' => 1,
|
||||||
])->clientCase()->create(
|
])->clientCase()->create(
|
||||||
[
|
[
|
||||||
'client_id' => 2
|
'client_id' => 2,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class PostSeeder extends Seeder
|
class PostSeeder extends Seeder
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
use App\Models\Permission;
|
use App\Models\Permission;
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class RolePermissionSeeder extends Seeder
|
class RolePermissionSeeder extends Seeder
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Segment;
|
use App\Models\Segment;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class SegmentSeeder extends Seeder
|
class SegmentSeeder extends Seeder
|
||||||
@@ -15,7 +14,7 @@ public function run(): void
|
|||||||
{
|
{
|
||||||
$sements = [
|
$sements = [
|
||||||
['name' => 'global', 'description' => ''],
|
['name' => 'global', 'description' => ''],
|
||||||
[ 'name' => 'terrain', 'description' => '']
|
['name' => 'terrain', 'description' => ''],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($sements as $st) {
|
foreach ($sements as $st) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public function run(): void
|
|||||||
|
|
||||||
if (! $user) {
|
if (! $user) {
|
||||||
$this->command?->warn("User {$email} not found – nothing updated.");
|
$this->command?->warn("User {$email} not found – nothing updated.");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ function goToPageInput() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div v-if="showToolbar" class="mb-3 flex items-center justify-between gap-3">
|
<div v-if="showToolbar" class="mb-3 flex items-center gap-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -180,7 +180,8 @@ function goToPageInput() {
|
|||||||
v-model="internalSearch"
|
v-model="internalSearch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<slot name="toolbar-extra" />
|
||||||
|
<div class="ml-auto flex items-center gap-2">
|
||||||
<label class="text-sm text-gray-600">Na stran</label>
|
<label class="text-sm text-gray-600">Na stran</label>
|
||||||
<select
|
<select
|
||||||
class="rounded border-gray-300 text-sm"
|
class="rounded border-gray-300 text-sm"
|
||||||
@@ -202,6 +203,10 @@ function goToPageInput() {
|
|||||||
class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur border-b border-gray-200 shadow-sm"
|
class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur border-b border-gray-200 shadow-sm"
|
||||||
>
|
>
|
||||||
<FwbTableHeadCell v-for="col in columns" :key="col.key" :class="col.class">
|
<FwbTableHeadCell v-for="col in columns" :key="col.key" :class="col.class">
|
||||||
|
<template v-if="$slots['header-' + col.key]">
|
||||||
|
<slot :name="'header-' + col.key" :column="col" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<button
|
<button
|
||||||
v-if="col.sortable"
|
v-if="col.sortable"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -216,6 +221,7 @@ function goToPageInput() {
|
|||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
<span v-else>{{ col.label }}</span>
|
<span v-else>{{ col.label }}</span>
|
||||||
|
</template>
|
||||||
</FwbTableHeadCell>
|
</FwbTableHeadCell>
|
||||||
<FwbTableHeadCell v-if="$slots.actions" class="w-px"> </FwbTableHeadCell>
|
<FwbTableHeadCell v-if="$slots.actions" class="w-px"> </FwbTableHeadCell>
|
||||||
</FwbTableHead>
|
</FwbTableHead>
|
||||||
|
|||||||
@@ -250,6 +250,41 @@ const formatEu = (value, decimals = 2) => {
|
|||||||
}).format(isNaN(num) ? 0 : num);
|
}).format(isNaN(num) ? 0 : num);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Flatten meta structure on the client side (mirrors backend logic)
|
||||||
|
const flattenMeta = (meta, prefix = "") => {
|
||||||
|
if (!meta || typeof meta !== "object") return {};
|
||||||
|
const result = {};
|
||||||
|
for (const [key, value] of Object.entries(meta)) {
|
||||||
|
const newKey = prefix === "" ? key : `${prefix}.${key}`;
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
// Check if it's a structured meta entry with 'value' field
|
||||||
|
if ("value" in value) {
|
||||||
|
result[newKey] = value.value;
|
||||||
|
// If parent key is numeric, also create direct alias
|
||||||
|
if (prefix !== "" && /^\d+$/.test(key)) {
|
||||||
|
result[key] = value.value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Recursively flatten nested objects
|
||||||
|
const nested = flattenMeta(value, newKey);
|
||||||
|
Object.assign(result, nested);
|
||||||
|
// If current key is numeric, also flatten without it
|
||||||
|
if (/^\d+$/.test(key)) {
|
||||||
|
const directNested = flattenMeta(value, prefix);
|
||||||
|
for (const [dk, dv] of Object.entries(directNested)) {
|
||||||
|
if (!(dk in result)) {
|
||||||
|
result[dk] = dv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[newKey] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const renderTokens = (text, vars) => {
|
const renderTokens = (text, vars) => {
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
const resolver = (obj, path) => {
|
const resolver = (obj, path) => {
|
||||||
@@ -355,6 +390,7 @@ const buildVarsFromSelectedContract = () => {
|
|||||||
if (!uuid) return {};
|
if (!uuid) return {};
|
||||||
const c = (contractsForCase.value || []).find((x) => x.uuid === uuid);
|
const c = (contractsForCase.value || []).find((x) => x.uuid === uuid);
|
||||||
if (!c) return {};
|
if (!c) return {};
|
||||||
|
|
||||||
const vars = {
|
const vars = {
|
||||||
contract: {
|
contract: {
|
||||||
uuid: c.uuid,
|
uuid: c.uuid,
|
||||||
@@ -363,6 +399,15 @@ const buildVarsFromSelectedContract = () => {
|
|||||||
end_date: c.end_date || "",
|
end_date: c.end_date || "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// Include contract.meta - flatten if needed (in case server returns nested structure)
|
||||||
|
if (c.meta && typeof c.meta === "object") {
|
||||||
|
// Check if already flattened (no nested objects with 'value' property)
|
||||||
|
const hasStructuredMeta = Object.values(c.meta).some(
|
||||||
|
(v) => v && typeof v === "object" && "value" in v
|
||||||
|
);
|
||||||
|
vars.contract.meta = hasStructuredMeta ? flattenMeta(c.meta) : c.meta;
|
||||||
|
}
|
||||||
|
|
||||||
if (c.account) {
|
if (c.account) {
|
||||||
vars.account = {
|
vars.account = {
|
||||||
reference: c.account.reference,
|
reference: c.account.reference,
|
||||||
@@ -535,6 +580,19 @@ const openSmsDialog = (phone) => {
|
|||||||
// Load contracts for this case (for contract/account placeholders)
|
// Load contracts for this case (for contract/account placeholders)
|
||||||
loadContractsForCase();
|
loadContractsForCase();
|
||||||
};
|
};
|
||||||
|
// Format YYYY-MM-DD (or ISO date) to dd.mm.yyyy
|
||||||
|
function formatDate(value) {
|
||||||
|
if (!value) return "-";
|
||||||
|
try {
|
||||||
|
const iso = String(value).split("T")[0];
|
||||||
|
const parts = iso.split("-");
|
||||||
|
if (parts.length !== 3) return value;
|
||||||
|
const [y, m, d] = parts;
|
||||||
|
return `${d.padStart(2, "0")}.${m.padStart(2, "0")}.${y}`;
|
||||||
|
} catch (e) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
const loadContractsForCase = async () => {
|
const loadContractsForCase = async () => {
|
||||||
try {
|
try {
|
||||||
const url = route("clientCase.contracts.list", { client_case: props.clientCaseUuid });
|
const url = route("clientCase.contracts.list", { client_case: props.clientCaseUuid });
|
||||||
@@ -595,43 +653,86 @@ const submitSms = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
|
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
|
||||||
<div class="rounded p-2 shadow">
|
<div class="rounded p-2 shadow">
|
||||||
<p class="text-xs leading-5 md:text-sm text-gray-500">Nu.</p>
|
<p class="text-xs leading-5 md:text-sm text-gray-500">Primer ref.</p>
|
||||||
<p class="text-sm md:text-base leading-7 text-gray-900">{{ person.nu }}</p>
|
<p class="text-sm md:text-base leading-7 text-gray-900">{{ person.nu }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded p-2 shadow">
|
<div class="rounded p-2 shadow">
|
||||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Name.</p>
|
<p class="text-sm leading-5 md:text-sm text-gray-500">Naziv</p>
|
||||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
{{ person.full_name }}
|
{{ person.full_name }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded p-2 shadow">
|
<div class="rounded p-2 shadow">
|
||||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Tax NU.</p>
|
<p class="text-sm leading-5 md:text-sm text-gray-500">Davčna</p>
|
||||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
{{ person.tax_number }}
|
{{ person.tax_number }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded p-2 shadow">
|
<div class="rounded p-2 shadow">
|
||||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Social security NU.</p>
|
<p class="text-sm leading-5 md:text-sm text-gray-500">Emšo</p>
|
||||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
{{ person.social_security_number }}
|
{{ person.social_security_number }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
|
<div
|
||||||
<div class="rounded p-2 shadow">
|
v-if="clientCaseUuid"
|
||||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Address</p>
|
class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-1"
|
||||||
|
>
|
||||||
|
<div class="col-span-full lg:col-span-1 rounded p-2 shadow">
|
||||||
|
<p class="text-sm leading-5 md:text-sm text-gray-500">Naslov</p>
|
||||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
{{ getMainAddress(person.addresses) }}
|
{{ getMainAddress(person.addresses) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded p-2 shadow">
|
<div class="rounded p-2 shadow">
|
||||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Phone</p>
|
<p class="text-sm leading-5 md:text-sm text-gray-500">Telefon</p>
|
||||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
{{ getMainPhone(person.phones) }}
|
{{ getMainPhone(person.phones) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-full lg:col-span-1 rounded p-2 shadow">
|
<div class="rounded p-2 shadow">
|
||||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Description</p>
|
<p class="text-sm leading-5 md:text-sm text-gray-500">Datum rojstva</p>
|
||||||
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
|
{{ formatDate(person.birthday) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-rows-* grid-cols-1 md:grid-cols-2 gap-2 mt-1">
|
||||||
|
<div class="rounded p-2 shadow">
|
||||||
|
<p class="text-sm leading-5 md:text-sm text-gray-500">Naslov</p>
|
||||||
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
|
{{ getMainAddress(person.addresses) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded p-2 shadow">
|
||||||
|
<p class="text-sm leading-5 md:text-sm text-gray-500">Telefon</p>
|
||||||
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
|
{{ getMainPhone(person.phones) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="clientCaseUuid"
|
||||||
|
class="grid grid-rows-* grid-cols-1 lg:grid-cols-2 gap-2 mt-1"
|
||||||
|
>
|
||||||
|
<div class="rounded p-2 shadow">
|
||||||
|
<p class="text-sm leading-5 md:text-sm text-gray-500">Delodajalec</p>
|
||||||
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
|
{{ person.employer }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded p-2 shadow">
|
||||||
|
<p class="text-sm leading-5 md:text-sm text-gray-500">Opis</p>
|
||||||
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
|
{{ person.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid grid-rows-* grid-cols-1 md:grid-cols-2 gap-2 mt-1">
|
||||||
|
<div class="col-span-full rounded p-2 shadow">
|
||||||
|
<p class="text-sm leading-5 md:text-sm text-gray-500">Opis</p>
|
||||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||||
{{ person.description }}
|
{{ person.description }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
import { usePage, Link } from "@inertiajs/vue3";
|
import { usePage, Link, router } from "@inertiajs/vue3";
|
||||||
import Dropdown from "@/Components/Dropdown.vue";
|
import Dropdown from "@/Components/Dropdown.vue";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
import { faBell } from "@fortawesome/free-solid-svg-icons";
|
import { faBell } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -53,7 +53,7 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
async function markRead(item) {
|
function markRead(item) {
|
||||||
const idx = items.value.findIndex((i) => i.id === item.id);
|
const idx = items.value.findIndex((i) => i.id === item.id);
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
return;
|
return;
|
||||||
@@ -62,14 +62,20 @@ async function markRead(item) {
|
|||||||
// Optimistically remove
|
// Optimistically remove
|
||||||
const removed = items.value.splice(idx, 1)[0];
|
const removed = items.value.splice(idx, 1)[0];
|
||||||
|
|
||||||
try {
|
router.patch(
|
||||||
await window.axios.post(route("notifications.activity.read"), {
|
route("notifications.activity.read"),
|
||||||
activity_id: item.id,
|
{ activity_id: item.id },
|
||||||
});
|
{
|
||||||
} catch (e) {
|
onSuccess: () => {
|
||||||
|
// Item successfully marked as read
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
// Rollback on failure
|
// Rollback on failure
|
||||||
items.value.splice(idx, 0, removed);
|
items.value.splice(idx, 0, removed);
|
||||||
|
},
|
||||||
|
preserveScroll: true
|
||||||
}
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,15 @@ const filteredSenders = computed(() => {
|
|||||||
return props.senders.filter(s => s.profile_id === form.profile_id)
|
return props.senders.filter(s => s.profile_id === form.profile_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function onTemplateChange() {
|
||||||
|
const template = props.templates.find(t => t.id === form.template_id)
|
||||||
|
if (template?.content) {
|
||||||
|
form.body = template.content
|
||||||
|
} else {
|
||||||
|
form.body = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function submitCreate() {
|
function submitCreate() {
|
||||||
const lines = (form.numbers || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)
|
const lines = (form.numbers || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)
|
||||||
if (!lines.length) return
|
if (!lines.length) return
|
||||||
@@ -78,19 +87,32 @@ const contracts = ref({ data: [], meta: { current_page: 1, last_page: 1, per_pag
|
|||||||
const segmentId = ref(null)
|
const segmentId = ref(null)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const clientId = ref(null)
|
const clientId = ref(null)
|
||||||
|
const startDateFrom = ref('')
|
||||||
|
const startDateTo = ref('')
|
||||||
|
const promiseDateFrom = ref('')
|
||||||
|
const promiseDateTo = ref('')
|
||||||
const onlyMobile = ref(false)
|
const onlyMobile = ref(false)
|
||||||
const onlyValidated = ref(false)
|
const onlyValidated = ref(false)
|
||||||
const loadingContracts = ref(false)
|
const loadingContracts = ref(false)
|
||||||
const selectedContractIds = ref(new Set())
|
const selectedContractIds = ref(new Set())
|
||||||
|
const perPage = ref(25)
|
||||||
|
|
||||||
async function loadContracts(url = null) {
|
async function loadContracts(url = null) {
|
||||||
if (!segmentId.value) {
|
|
||||||
contracts.value = { data: [], meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 } }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
loadingContracts.value = true
|
loadingContracts.value = true
|
||||||
try {
|
try {
|
||||||
const target = url || `${route('admin.packages.contracts')}?segment_id=${encodeURIComponent(segmentId.value)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ''}${clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ''}${onlyMobile.value ? `&only_mobile=1` : ''}${onlyValidated.value ? `&only_validated=1` : ''}`
|
const params = new URLSearchParams()
|
||||||
|
if (segmentId.value) params.append('segment_id', segmentId.value)
|
||||||
|
if (search.value) params.append('q', search.value)
|
||||||
|
if (clientId.value) params.append('client_id', clientId.value)
|
||||||
|
if (startDateFrom.value) params.append('start_date_from', startDateFrom.value)
|
||||||
|
if (startDateTo.value) params.append('start_date_to', startDateTo.value)
|
||||||
|
if (promiseDateFrom.value) params.append('promise_date_from', promiseDateFrom.value)
|
||||||
|
if (promiseDateTo.value) params.append('promise_date_to', promiseDateTo.value)
|
||||||
|
if (onlyMobile.value) params.append('only_mobile', '1')
|
||||||
|
if (onlyValidated.value) params.append('only_validated', '1')
|
||||||
|
params.append('per_page', perPage.value)
|
||||||
|
|
||||||
|
const target = url || `${route('admin.packages.contracts')}?${params.toString()}`
|
||||||
const res = await fetch(target, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
const res = await fetch(target, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
contracts.value = { data: json.data || [], meta: json.meta || { current_page: 1, last_page: 1, per_page: 25, total: 0 } }
|
contracts.value = { data: json.data || [], meta: json.meta || { current_page: 1, last_page: 1, per_page: 25, total: 0 } }
|
||||||
@@ -110,11 +132,51 @@ function clearSelection() {
|
|||||||
selectedContractIds.value = new Set()
|
selectedContractIds.value = new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
const currentPageIds = contracts.value.data.map(c => c.id)
|
||||||
|
const allSelected = currentPageIds.every(id => selectedContractIds.value.has(id))
|
||||||
|
|
||||||
|
if (allSelected) {
|
||||||
|
// Deselect all on current page
|
||||||
|
currentPageIds.forEach(id => selectedContractIds.value.delete(id))
|
||||||
|
} else {
|
||||||
|
// Select all on current page
|
||||||
|
currentPageIds.forEach(id => selectedContractIds.value.add(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force reactivity
|
||||||
|
selectedContractIds.value = new Set(Array.from(selectedContractIds.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const allCurrentPageSelected = computed(() => {
|
||||||
|
if (!contracts.value.data.length) return false
|
||||||
|
return contracts.value.data.every(c => selectedContractIds.value.has(c.id))
|
||||||
|
})
|
||||||
|
|
||||||
|
const someCurrentPageSelected = computed(() => {
|
||||||
|
if (!contracts.value.data.length) return false
|
||||||
|
return contracts.value.data.some(c => selectedContractIds.value.has(c.id)) && !allCurrentPageSelected.value
|
||||||
|
})
|
||||||
|
|
||||||
function goContractsPage(delta) {
|
function goContractsPage(delta) {
|
||||||
const { current_page } = contracts.value.meta
|
const { current_page } = contracts.value.meta
|
||||||
const nextPage = current_page + delta
|
const nextPage = current_page + delta
|
||||||
if (nextPage < 1 || nextPage > contracts.value.meta.last_page) return
|
if (nextPage < 1 || nextPage > contracts.value.meta.last_page) return
|
||||||
const base = `${route('admin.packages.contracts')}?segment_id=${encodeURIComponent(segmentId.value)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ''}${clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ''}${onlyMobile.value ? `&only_mobile=1` : ''}${onlyValidated.value ? `&only_validated=1` : ''}&page=${nextPage}`
|
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (segmentId.value) params.append('segment_id', segmentId.value)
|
||||||
|
if (search.value) params.append('q', search.value)
|
||||||
|
if (clientId.value) params.append('client_id', clientId.value)
|
||||||
|
if (startDateFrom.value) params.append('start_date_from', startDateFrom.value)
|
||||||
|
if (startDateTo.value) params.append('start_date_to', startDateTo.value)
|
||||||
|
if (promiseDateFrom.value) params.append('promise_date_from', promiseDateFrom.value)
|
||||||
|
if (promiseDateTo.value) params.append('promise_date_to', promiseDateTo.value)
|
||||||
|
if (onlyMobile.value) params.append('only_mobile', '1')
|
||||||
|
if (onlyValidated.value) params.append('only_validated', '1')
|
||||||
|
params.append('per_page', perPage.value)
|
||||||
|
params.append('page', nextPage)
|
||||||
|
|
||||||
|
const base = `${route('admin.packages.contracts')}?${params.toString()}`
|
||||||
loadContracts(base)
|
loadContracts(base)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +241,7 @@ function submitCreateFromContracts() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-gray-500 mb-1">Predloga</label>
|
<label class="block text-xs text-gray-500 mb-1">Predloga</label>
|
||||||
<select v-model.number="form.template_id" class="w-full rounded border-gray-300 text-sm">
|
<select v-model.number="form.template_id" @change="onTemplateChange" class="w-full rounded border-gray-300 text-sm">
|
||||||
<option :value="null">—</option>
|
<option :value="null">—</option>
|
||||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -206,56 +268,134 @@ function submitCreateFromContracts() {
|
|||||||
|
|
||||||
<!-- Contracts mode -->
|
<!-- Contracts mode -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<div class="sm:col-span-3 space-y-4">
|
||||||
|
<!-- Basic filters -->
|
||||||
|
<div class="grid sm:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-gray-500 mb-1">Segment</label>
|
<label class="block text-xs font-medium text-gray-700 mb-1">Segment</label>
|
||||||
<select v-model.number="segmentId" @change="loadContracts()" class="w-full rounded border-gray-300 text-sm">
|
<select v-model.number="segmentId" @change="loadContracts()" class="w-full rounded border-gray-300 text-sm">
|
||||||
<option :value="null">—</option>
|
<option :value="null">Vsi segmenti</option>
|
||||||
<option v-for="s in segments" :key="s.id" :value="s.id">{{ s.name }}</option>
|
<option v-for="s in segments" :key="s.id" :value="s.id">{{ s.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-gray-500 mb-1">Stranka</label>
|
<label class="block text-xs font-medium text-gray-700 mb-1">Stranka</label>
|
||||||
<select v-model.number="clientId" @change="loadContracts()" class="w-full rounded border-gray-300 text-sm">
|
<select v-model.number="clientId" @change="loadContracts()" class="w-full rounded border-gray-300 text-sm">
|
||||||
<option :value="null">—</option>
|
<option :value="null">Vse stranke</option>
|
||||||
<option v-for="c in clients" :key="c.id" :value="c.id">{{ c.name }}</option>
|
<option v-for="c in clients" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:col-span-2">
|
<div>
|
||||||
<label class="block text-xs text-gray-500 mb-1">Iskanje</label>
|
<label class="block text-xs font-medium text-gray-700 mb-1">Iskanje po referenci</label>
|
||||||
<div class="flex gap-2">
|
<input v-model="search" @keyup.enter="loadContracts()" type="text" class="w-full rounded border-gray-300 text-sm" placeholder="Vnesi referenco...">
|
||||||
<input v-model="search" @keyup.enter="loadContracts()" type="text" class="w-full rounded border-gray-300 text-sm" placeholder="referenca...">
|
|
||||||
<button @click="loadContracts()" class="px-3 py-1.5 rounded border text-sm">Išči</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:col-span-3 flex items-center gap-6 text-sm text-gray-700">
|
|
||||||
|
<!-- Date range filters -->
|
||||||
|
<div class="border-t pt-4">
|
||||||
|
<h4 class="text-xs font-semibold text-gray-700 mb-3">Datumski filtri</h4>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-medium text-gray-600 mb-2">Datum začetka pogodbe</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">Od</label>
|
||||||
|
<input v-model="startDateFrom" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">Do</label>
|
||||||
|
<input v-model="startDateTo" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-medium text-gray-600 mb-2">Datum obljube plačila</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">Od</label>
|
||||||
|
<input v-model="promiseDateFrom" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">Do</label>
|
||||||
|
<input v-model="promiseDateTo" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Phone filters -->
|
||||||
|
<div class="border-t pt-4">
|
||||||
|
<h4 class="text-xs font-semibold text-gray-700 mb-3">Telefonski filtri</h4>
|
||||||
|
<div class="flex items-center gap-6 text-sm text-gray-700">
|
||||||
<label class="inline-flex items-center gap-2">
|
<label class="inline-flex items-center gap-2">
|
||||||
<input type="checkbox" v-model="onlyMobile" @change="loadContracts()"> Samo s mobilno številko
|
<input type="checkbox" v-model="onlyMobile" @change="loadContracts()" class="rounded border-gray-300">
|
||||||
|
Samo mobilne številke
|
||||||
</label>
|
</label>
|
||||||
<label class="inline-flex items-center gap-2">
|
<label class="inline-flex items-center gap-2">
|
||||||
<input type="checkbox" v-model="onlyValidated" @change="loadContracts()"> Telefonska številka mora biti potrjena
|
<input type="checkbox" v-model="onlyValidated" @change="loadContracts()" class="rounded border-gray-300">
|
||||||
|
Samo potrjene številke
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="flex items-center gap-2 pt-2">
|
||||||
|
<button @click="loadContracts()" class="px-4 py-2 rounded bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-700">
|
||||||
|
Išči pogodbe
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="segmentId = null; clientId = null; search = ''; startDateFrom = ''; startDateTo = ''; promiseDateFrom = ''; promiseDateTo = ''; onlyMobile = false; onlyValidated = false; contracts.value = { data: [], meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 } }"
|
||||||
|
class="px-4 py-2 rounded border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Počisti filtre
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results table -->
|
||||||
<div class="sm:col-span-3">
|
<div class="sm:col-span-3">
|
||||||
<div class="overflow-hidden rounded border bg-white">
|
<div class="overflow-hidden rounded border bg-white">
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr class="text-xs text-gray-500">
|
<tr class="text-xs text-gray-500">
|
||||||
<th class="px-3 py-2"></th>
|
<th class="px-3 py-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="allCurrentPageSelected"
|
||||||
|
:indeterminate="someCurrentPageSelected"
|
||||||
|
@change="toggleSelectAll"
|
||||||
|
:disabled="!contracts.data.length"
|
||||||
|
class="rounded"
|
||||||
|
title="Izberi vse na tej strani"
|
||||||
|
>
|
||||||
|
</th>
|
||||||
<th class="px-3 py-2 text-left">Pogodba</th>
|
<th class="px-3 py-2 text-left">Pogodba</th>
|
||||||
<th class="px-3 py-2 text-left">Primer</th>
|
<th class="px-3 py-2 text-left">Primer</th>
|
||||||
<th class="px-3 py-2 text-left">Stranka</th>
|
<th class="px-3 py-2 text-left">Stranka</th>
|
||||||
|
<th class="px-3 py-2 text-left">Datum začetka</th>
|
||||||
|
<th class="px-3 py-2 text-left">Zadnja obljuba</th>
|
||||||
<th class="px-3 py-2 text-left">Izbrana številka</th>
|
<th class="px-3 py-2 text-left">Izbrana številka</th>
|
||||||
<th class="px-3 py-2 text-left">Opomba</th>
|
<th class="px-3 py-2 text-left">Opomba</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200" v-if="!loadingContracts">
|
<tbody class="divide-y divide-gray-200" v-if="!loadingContracts">
|
||||||
<tr v-for="c in contracts.data" :key="c.id" class="text-sm">
|
<tr v-for="c in contracts.data" :key="c.id" class="text-sm hover:bg-gray-50">
|
||||||
<td class="px-3 py-2">
|
<td class="px-3 py-2">
|
||||||
<input type="checkbox" :checked="selectedContractIds.has(c.id)" @change="toggleSelectContract(c.id)">
|
<input type="checkbox" :checked="selectedContractIds.has(c.id)" @change="toggleSelectContract(c.id)" class="rounded">
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2">
|
<td class="px-3 py-2">
|
||||||
<div class="font-mono text-xs text-gray-600">{{ c.uuid }}</div>
|
<div class="font-mono text-xs text-gray-600">{{ c.uuid }}</div>
|
||||||
<div class="text-xs text-gray-800">{{ c.reference }}</div>
|
<a
|
||||||
|
v-if="c.case?.uuid"
|
||||||
|
:href="route('clientCase.show', c.case.uuid)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-xs font-medium text-indigo-600 hover:text-indigo-800 hover:underline"
|
||||||
|
>
|
||||||
|
{{ c.reference }}
|
||||||
|
</a>
|
||||||
|
<div v-else class="text-xs font-medium text-gray-800">{{ c.reference }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2">
|
<td class="px-3 py-2">
|
||||||
<div class="text-xs text-gray-800">{{ c.person?.full_name || '—' }}</div>
|
<div class="text-xs text-gray-800">{{ c.person?.full_name || '—' }}</div>
|
||||||
@@ -263,6 +403,12 @@ function submitCreateFromContracts() {
|
|||||||
<td class="px-3 py-2">
|
<td class="px-3 py-2">
|
||||||
<div class="text-xs text-gray-800">{{ c.client?.name || '—' }}</div>
|
<div class="text-xs text-gray-800">{{ c.client?.name || '—' }}</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<div class="text-xs text-gray-700">{{ c.start_date ? new Date(c.start_date).toLocaleDateString('sl-SI') : '—' }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<div class="text-xs text-gray-700">{{ c.promise_date ? new Date(c.promise_date).toLocaleDateString('sl-SI') : '—' }}</div>
|
||||||
|
</td>
|
||||||
<td class="px-3 py-2">
|
<td class="px-3 py-2">
|
||||||
<div v-if="c.selected_phone" class="text-xs">
|
<div v-if="c.selected_phone" class="text-xs">
|
||||||
{{ c.selected_phone.number }}
|
{{ c.selected_phone.number }}
|
||||||
@@ -273,27 +419,44 @@ function submitCreateFromContracts() {
|
|||||||
<td class="px-3 py-2 text-xs text-gray-500">{{ c.no_phone_reason || '—' }}</td>
|
<td class="px-3 py-2 text-xs text-gray-500">{{ c.no_phone_reason || '—' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!contracts.data?.length">
|
<tr v-if="!contracts.data?.length">
|
||||||
<td colspan="6" class="px-3 py-8 text-center text-sm text-gray-500">Ni rezultatov.</td>
|
<td colspan="8" class="px-3 py-8 text-center text-sm text-gray-500">
|
||||||
|
Ni rezultatov. Poskusite z drugimi filtri.
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tbody v-else>
|
<tbody v-else>
|
||||||
<tr><td colspan="6" class="px-3 py-6 text-center text-sm text-gray-500">Nalaganje...</td></tr>
|
<tr><td colspan="8" class="px-3 py-6 text-center text-sm text-gray-500">Nalaganje...</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex items-center justify-between text-sm">
|
<div class="mt-3 flex items-center justify-between text-sm">
|
||||||
<div class="text-gray-600">
|
<div class="text-gray-600 flex items-center gap-4">
|
||||||
|
<span v-if="contracts.data.length">
|
||||||
Prikazano stran {{ contracts.meta.current_page }} od {{ contracts.meta.last_page }} (skupaj {{ contracts.meta.total }})
|
Prikazano stran {{ contracts.meta.current_page }} od {{ contracts.meta.last_page }} (skupaj {{ contracts.meta.total }})
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label class="text-xs text-gray-500">Na stran:</label>
|
||||||
|
<select v-model.number="perPage" @change="loadContracts()" class="rounded border-gray-300 text-xs py-1">
|
||||||
|
<option :value="10">10</option>
|
||||||
|
<option :value="25">25</option>
|
||||||
|
<option :value="50">50</option>
|
||||||
|
<option :value="100">100</option>
|
||||||
|
<option :value="200">200</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-2">
|
<div class="space-x-2">
|
||||||
<button @click="goContractsPage(-1)" :disabled="contracts.meta.current_page <= 1" class="px-3 py-1.5 rounded border text-sm disabled:opacity-50">Nazaj</button>
|
<button @click="goContractsPage(-1)" :disabled="contracts.meta.current_page <= 1" class="px-3 py-1.5 rounded border text-sm disabled:opacity-50 disabled:cursor-not-allowed">Nazaj</button>
|
||||||
<button @click="goContractsPage(1)" :disabled="contracts.meta.current_page >= contracts.meta.last_page" class="px-3 py-1.5 rounded border text-sm disabled:opacity-50">Naprej</button>
|
<button @click="goContractsPage(1)" :disabled="contracts.meta.current_page >= contracts.meta.last_page" class="px-3 py-1.5 rounded border text-sm disabled:opacity-50 disabled:cursor-not-allowed">Naprej</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:col-span-3 flex items-center justify-end gap-2">
|
<div class="sm:col-span-3 flex items-center justify-between gap-2 pt-4 border-t">
|
||||||
<div class="text-sm text-gray-600 mr-auto">Izbrano: {{ selectedContractIds.size }}</div>
|
<div class="text-sm text-gray-600">
|
||||||
<button @click="submitCreateFromContracts" :disabled="selectedContractIds.size === 0" class="px-3 py-1.5 rounded bg-emerald-600 text-white text-sm disabled:opacity-50">Ustvari paket</button>
|
<span class="font-medium">Izbrano: {{ selectedContractIds.size }}</span>
|
||||||
|
<span v-if="selectedContractIds.size > 0" class="ml-2 text-gray-500">({{ selectedContractIds.size === 1 ? '1 pogodba' : `${selectedContractIds.size} pogodb` }})</span>
|
||||||
|
</div>
|
||||||
|
<button @click="submitCreateFromContracts" :disabled="selectedContractIds.size === 0" class="px-4 py-2 rounded bg-emerald-600 text-white text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed hover:bg-emerald-700">Ustvari paket</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||||
import { useForm, Link } from "@inertiajs/vue3";
|
import { useForm, Link, router } from "@inertiajs/vue3";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
import { faMagnifyingGlass, faFloppyDisk } from "@fortawesome/free-solid-svg-icons";
|
import { faMagnifyingGlass, faFloppyDisk, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import DialogModal from "@/Components/DialogModal.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
users: Array,
|
users: Array,
|
||||||
@@ -65,6 +66,49 @@ const filteredUsers = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||||
|
|
||||||
|
// Create user modal
|
||||||
|
const showCreateModal = ref(false);
|
||||||
|
const createForm = useForm({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
password_confirmation: "",
|
||||||
|
roles: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
function openCreateModal() {
|
||||||
|
createForm.reset();
|
||||||
|
createForm.clearErrors();
|
||||||
|
showCreateModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCreateModal() {
|
||||||
|
showCreateModal.value = false;
|
||||||
|
createForm.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitCreateUser() {
|
||||||
|
createForm.post(route("admin.users.store"), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
closeCreateModal();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCreateRole(roleId) {
|
||||||
|
const exists = createForm.roles.includes(roleId);
|
||||||
|
createForm.roles = exists
|
||||||
|
? createForm.roles.filter((id) => id !== roleId)
|
||||||
|
: [...createForm.roles, roleId];
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUserActive(userId) {
|
||||||
|
router.patch(route("admin.users.toggle-active", { user: userId }), {}, {
|
||||||
|
preserveScroll: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -127,6 +171,14 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="openCreateModal"
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium bg-indigo-600 border-indigo-600 text-white hover:bg-indigo-500"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" />
|
||||||
|
Ustvari uporabnika
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="submitAll"
|
@click="submitAll"
|
||||||
@@ -151,6 +203,9 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
|||||||
<th class="p-2 text-left font-medium text-[11px] uppercase tracking-wide">
|
<th class="p-2 text-left font-medium text-[11px] uppercase tracking-wide">
|
||||||
Uporabnik
|
Uporabnik
|
||||||
</th>
|
</th>
|
||||||
|
<th class="p-2 text-center font-medium text-[11px] uppercase tracking-wide">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
<th
|
<th
|
||||||
v-for="role in props.roles"
|
v-for="role in props.roles"
|
||||||
:key="role.id"
|
:key="role.id"
|
||||||
@@ -172,6 +227,7 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
|||||||
:class="[
|
:class="[
|
||||||
'border-t border-slate-100',
|
'border-t border-slate-100',
|
||||||
idx % 2 === 1 ? 'bg-slate-50/40' : 'bg-white',
|
idx % 2 === 1 ? 'bg-slate-50/40' : 'bg-white',
|
||||||
|
!user.active && 'opacity-60',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<td class="p-2 whitespace-nowrap align-top">
|
<td class="p-2 whitespace-nowrap align-top">
|
||||||
@@ -191,6 +247,19 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
|||||||
{{ user.email }}
|
{{ user.email }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="p-2 text-center align-top">
|
||||||
|
<button
|
||||||
|
@click="toggleUserActive(user.id)"
|
||||||
|
class="inline-flex items-center px-2 py-1 rounded text-xs font-medium transition"
|
||||||
|
:class="
|
||||||
|
user.active
|
||||||
|
? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200'
|
||||||
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ user.active ? 'Aktiven' : 'Neaktiven' }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
<td
|
<td
|
||||||
v-for="role in props.roles"
|
v-for="role in props.roles"
|
||||||
:key="role.id"
|
:key="role.id"
|
||||||
@@ -223,7 +292,7 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
|||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!filteredUsers.length">
|
<tr v-if="!filteredUsers.length">
|
||||||
<td
|
<td
|
||||||
:colspan="props.roles.length + 2"
|
:colspan="props.roles.length + 3"
|
||||||
class="p-6 text-center text-sm text-gray-500"
|
class="p-6 text-center text-sm text-gray-500"
|
||||||
>
|
>
|
||||||
Ni rezultatov
|
Ni rezultatov
|
||||||
@@ -265,5 +334,107 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Create User Modal -->
|
||||||
|
<DialogModal :show="showCreateModal" @close="closeCreateModal" max-width="2xl">
|
||||||
|
<template #title>Ustvari novega uporabnika</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Ime</label>
|
||||||
|
<input
|
||||||
|
v-model="createForm.name"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
placeholder="Ime uporabnika"
|
||||||
|
/>
|
||||||
|
<div v-if="createForm.errors.name" class="text-red-600 text-xs mt-1">
|
||||||
|
{{ createForm.errors.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">E-pošta</label>
|
||||||
|
<input
|
||||||
|
v-model="createForm.email"
|
||||||
|
type="email"
|
||||||
|
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
placeholder="uporabnik@example.com"
|
||||||
|
/>
|
||||||
|
<div v-if="createForm.errors.email" class="text-red-600 text-xs mt-1">
|
||||||
|
{{ createForm.errors.email }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Geslo</label>
|
||||||
|
<input
|
||||||
|
v-model="createForm.password"
|
||||||
|
type="password"
|
||||||
|
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
placeholder="********"
|
||||||
|
/>
|
||||||
|
<div v-if="createForm.errors.password" class="text-red-600 text-xs mt-1">
|
||||||
|
{{ createForm.errors.password }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Potrdi geslo</label>
|
||||||
|
<input
|
||||||
|
v-model="createForm.password_confirmation"
|
||||||
|
type="password"
|
||||||
|
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
placeholder="********"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Vloge</label>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<label
|
||||||
|
v-for="role in props.roles"
|
||||||
|
:key="'create-role-' + role.id"
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-2 rounded-md border cursor-pointer transition"
|
||||||
|
:class="
|
||||||
|
createForm.roles.includes(role.id)
|
||||||
|
? 'bg-indigo-50 border-indigo-600 text-indigo-700'
|
||||||
|
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
|
:checked="createForm.roles.includes(role.id)"
|
||||||
|
@change="toggleCreateRole(role.id)"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium">{{ role.name }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="createForm.errors.roles" class="text-red-600 text-xs mt-1">
|
||||||
|
{{ createForm.errors.roles }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="closeCreateModal"
|
||||||
|
class="px-4 py-2 rounded-md text-sm font-medium bg-white border border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Prekliči
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="submitCreateUser"
|
||||||
|
:disabled="createForm.processing"
|
||||||
|
class="ml-3 px-4 py-2 rounded-md text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<span v-if="createForm.processing">Ustvarjanje...</span>
|
||||||
|
<span v-else>Ustvari</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ const store = async () => {
|
|||||||
form
|
form
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
...data,
|
...data,
|
||||||
|
phone_view: props.phoneMode,
|
||||||
due_date: formatDateForSubmit(data.due_date),
|
due_date: formatDateForSubmit(data.due_date),
|
||||||
attachment_document_ids:
|
attachment_document_ids:
|
||||||
templateAllowsAttachments.value && data.attach_documents
|
templateAllowsAttachments.value && data.attach_documents
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from "vue";
|
||||||
|
import { useForm } from "@inertiajs/vue3";
|
||||||
|
import DialogModal from "@/Components/DialogModal.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: { type: Boolean, default: false },
|
||||||
|
contract: { type: Object, default: null },
|
||||||
|
clientCase: { type: Object, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["close"]);
|
||||||
|
|
||||||
|
const metaFields = ref([]);
|
||||||
|
|
||||||
|
// Extract meta fields when contract changes
|
||||||
|
watch(
|
||||||
|
() => props.contract,
|
||||||
|
(c) => {
|
||||||
|
if (!c) {
|
||||||
|
metaFields.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metaFields.value = extractMetaFields(c?.meta || {});
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
function extractMetaFields(meta, parentKey = "") {
|
||||||
|
const results = [];
|
||||||
|
const visit = (node, path) => {
|
||||||
|
if (node === null || node === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(node)) {
|
||||||
|
node.forEach((el, idx) => visit(el, `${path}[${idx}]`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof node === "object") {
|
||||||
|
const hasValue = Object.prototype.hasOwnProperty.call(node, "value");
|
||||||
|
const hasTitle = Object.prototype.hasOwnProperty.call(node, "title");
|
||||||
|
if (hasValue || hasTitle) {
|
||||||
|
const title = (node.title || path || "Meta").toString().trim();
|
||||||
|
const type = node.type || (typeof node.value === "number" ? "number" : "text");
|
||||||
|
results.push({
|
||||||
|
path,
|
||||||
|
title,
|
||||||
|
value: node.value ?? "",
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [k, v] of Object.entries(node)) {
|
||||||
|
const newPath = path ? `${path}.${k}` : k;
|
||||||
|
visit(v, newPath);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (path) {
|
||||||
|
results.push({ path, title: path, value: node, type: "text" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(meta, "");
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
meta: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(val) => {
|
||||||
|
if (val && props.contract) {
|
||||||
|
// Rebuild meta structure for form
|
||||||
|
const metaObj = {};
|
||||||
|
metaFields.value.forEach((field) => {
|
||||||
|
setNestedValue(metaObj, field.path, {
|
||||||
|
title: field.title,
|
||||||
|
value: field.value,
|
||||||
|
type: field.type,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
form.meta = metaObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function setNestedValue(obj, path, value) {
|
||||||
|
const parts = path.split(/\.|\[|\]/).filter(Boolean);
|
||||||
|
let current = obj;
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
if (!current[part]) {
|
||||||
|
const nextPart = parts[i + 1];
|
||||||
|
current[part] = /^\d+$/.test(nextPart) ? [] : {};
|
||||||
|
}
|
||||||
|
current = current[part];
|
||||||
|
}
|
||||||
|
current[parts[parts.length - 1]] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFieldValue(field, newValue) {
|
||||||
|
field.value = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitForm() {
|
||||||
|
if (!props.contract?.uuid) return;
|
||||||
|
|
||||||
|
// Rebuild meta object from fields
|
||||||
|
const metaObj = {};
|
||||||
|
metaFields.value.forEach((field) => {
|
||||||
|
setNestedValue(metaObj, field.path, {
|
||||||
|
title: field.title,
|
||||||
|
value: field.value,
|
||||||
|
type: field.type,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
form.meta = metaObj;
|
||||||
|
|
||||||
|
form.patch(
|
||||||
|
route("clientCase.contract.patchMeta", {
|
||||||
|
client_case: props.clientCase.uuid,
|
||||||
|
uuid: props.contract.uuid,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
closeDialog();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatInputType(type) {
|
||||||
|
if (type === "date") return "date";
|
||||||
|
if (type === "number") return "number";
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal :show="show" max-width="2xl" @close="closeDialog">
|
||||||
|
<template #title>
|
||||||
|
Uredi meta podatke
|
||||||
|
<span v-if="contract" class="text-gray-500 font-normal">
|
||||||
|
- {{ contract.reference }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div v-if="metaFields.length === 0" class="text-sm text-gray-500">
|
||||||
|
Ni meta podatkov za urejanje.
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(field, idx) in metaFields"
|
||||||
|
:key="idx"
|
||||||
|
class="grid grid-cols-3 gap-3 items-start"
|
||||||
|
>
|
||||||
|
<div class="col-span-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700">
|
||||||
|
{{ field.title }}
|
||||||
|
</label>
|
||||||
|
<div class="text-xs text-gray-400 mt-0.5">{{ field.path }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<input
|
||||||
|
v-if="
|
||||||
|
field.type !== 'text' || field.type === 'date' || field.type === 'number'
|
||||||
|
"
|
||||||
|
:type="formatInputType(field.type)"
|
||||||
|
v-model="field.value"
|
||||||
|
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 text-sm"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-else
|
||||||
|
v-model="field.value"
|
||||||
|
rows="2"
|
||||||
|
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 text-sm"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="form.errors.meta" class="mt-3 text-sm text-red-600">
|
||||||
|
{{ form.errors.meta }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-4 py-2 text-sm rounded-md border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 mr-2"
|
||||||
|
@click="closeDialog"
|
||||||
|
>
|
||||||
|
Prekliči
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-4 py-2 text-sm rounded-md bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50"
|
||||||
|
:disabled="form.processing || metaFields.length === 0"
|
||||||
|
@click="submitForm"
|
||||||
|
>
|
||||||
|
Shrani
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user