Mager updated

This commit is contained in:
Simon Pocrnjič
2025-09-27 17:45:55 +02:00
parent d17e34941b
commit 7227c888d4
74 changed files with 6339 additions and 342 deletions
@@ -4,6 +4,9 @@
use App\Models\ClientCase;
use App\Models\Contract;
use App\Models\Document;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Exception;
use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
@@ -162,6 +165,119 @@ public function deleteContract(ClientCase $clientCase, String $uuid, Request $re
return to_route('clientCase.show', $clientCase);
}
public function storeDocument(ClientCase $clientCase, Request $request)
{
$validated = $request->validate([
'file' => 'required|file|max:25600|mimes:doc,docx,pdf,txt,csv,xls,xlsx,jpeg,png', // 25MB and allowed types
'name' => 'nullable|string|max:255',
'description' => 'nullable|string',
'is_public' => 'sometimes|boolean',
]);
$file = $validated['file'];
$disk = 'public';
$directory = 'cases/' . $clientCase->uuid . '/documents';
$path = $file->store($directory, $disk);
$doc = new Document([
'name' => $validated['name'] ?? pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME),
'description' => $validated['description'] ?? null,
'user_id' => optional($request->user())->id,
'disk' => $disk,
'path' => $path,
'file_name' => basename($path),
'original_name' => $file->getClientOriginalName(),
'extension' => $file->getClientOriginalExtension(),
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
'checksum' => null,
'is_public' => (bool)($validated['is_public'] ?? false),
]);
$clientCase->documents()->save($doc);
// Generate preview immediately for Office docs to avoid first-view delay
$ext = strtolower($doc->extension ?? pathinfo($doc->original_name ?? $doc->file_name, PATHINFO_EXTENSION));
if (in_array($ext, ['doc','docx'])) {
\App\Jobs\GenerateDocumentPreview::dispatch($doc->id);
}
return back()->with('success', 'Document uploaded.');
}
public function viewDocument(ClientCase $clientCase, Document $document, Request $request)
{
// Ensure the document belongs to this client case
if ($document->documentable_type !== ClientCase::class || $document->documentable_id !== $clientCase->id) {
abort(404);
}
// Optional: add authz checks here (e.g., policies)
$disk = $document->disk ?: 'public';
// If a preview exists (e.g., PDF generated for doc/docx), stream that
$previewDisk = config('files.preview_disk', 'public');
if ($document->preview_path && Storage::disk($previewDisk)->exists($document->preview_path)) {
$stream = Storage::disk($previewDisk)->readStream($document->preview_path);
if ($stream === false) abort(404);
return response()->stream(function () use ($stream) { fpassthru($stream); }, 200, [
'Content-Type' => $document->preview_mime ?: 'application/pdf',
'Content-Disposition' => 'inline; filename="' . addslashes(($document->original_name ?: $document->file_name) . '.pdf') . '"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
]);
}
// If it's a DOC/DOCX and no preview yet, queue generation and show 202 Accepted
$ext = strtolower(pathinfo($document->original_name ?: $document->file_name, PATHINFO_EXTENSION));
if (in_array($ext, ['doc','docx'])) {
\App\Jobs\GenerateDocumentPreview::dispatch($document->id);
return response('Preview is being generated. Please try again shortly.', 202);
}
if (!Storage::disk($disk)->exists($document->path)) {
abort(404);
}
$stream = Storage::disk($disk)->readStream($document->path);
if ($stream === false) {
abort(404);
}
return response()->stream(function () use ($stream) {
fpassthru($stream);
}, 200, [
'Content-Type' => $document->mime_type ?: 'application/octet-stream',
'Content-Disposition' => 'inline; filename="' . addslashes($document->original_name ?: $document->file_name) . '"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
]);
}
public function downloadDocument(ClientCase $clientCase, Document $document, Request $request)
{
if ($document->documentable_type !== ClientCase::class || $document->documentable_id !== $clientCase->id) {
abort(404);
}
$disk = $document->disk ?: 'public';
if (!Storage::disk($disk)->exists($document->path)) {
abort(404);
}
$name = $document->original_name ?: $document->file_name;
$stream = Storage::disk($disk)->readStream($document->path);
if ($stream === false) {
abort(404);
}
return response()->stream(function () use ($stream) {
fpassthru($stream);
}, 200, [
'Content-Type' => $document->mime_type ?: 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . addslashes($name) . '"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
]);
}
/**
* Display the specified resource.
*/
@@ -185,6 +301,7 @@ public function show(ClientCase $clientCase)
'activities' => $case->activities()->with(['action', 'decision'])
->orderByDesc('created_at')
->paginate(20, ['*'], 'activities'),
'documents' => $case->documents()->orderByDesc('created_at')->get(),
'contract_types' => \App\Models\ContractType::whereNull('deleted_at')->get(),
'actions' => \App\Models\Action::with('decisions')->get(),
'types' => $types
+358
View File
@@ -0,0 +1,358 @@
<?php
namespace App\Http\Controllers;
use App\Models\Client;
use App\Models\Import;
use App\Models\ImportTemplate;
use App\Models\ImportRow;
use App\Models\ImportEvent;
use App\Services\ImportProcessor;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Inertia\Inertia;
use App\Services\CsvImportService;
class ImportController extends Controller
{
// List imports (paginated)
public function index(Request $request)
{
$paginator = Import::query()
->with(['client:id,uuid', 'template:id,name'])
->orderByDesc('created_at')
->paginate(15);
$imports = [
'data' => $paginator->items(),
'links' => [
'first' => $paginator->url(1),
'last' => $paginator->url($paginator->lastPage()),
'prev' => $paginator->previousPageUrl(),
'next' => $paginator->nextPageUrl(),
],
'meta' => [
'current_page' => $paginator->currentPage(),
'from' => $paginator->firstItem(),
'last_page' => $paginator->lastPage(),
'path' => $paginator->path(),
'per_page' => $paginator->perPage(),
'to' => $paginator->lastItem(),
'total' => $paginator->total(),
],
];
// Map items into a simpler shape
$imports['data'] = array_map(function ($imp) {
return [
'id' => $imp->id,
'uuid' => $imp->uuid,
'created_at' => $imp->created_at,
'original_name' => $imp->original_name,
'size' => $imp->size,
'status' => $imp->status,
'client' => $imp->client ? [ 'id' => $imp->client_id, 'uuid' => $imp->client->uuid ] : null,
'template' => $imp->template ? [ 'id' => $imp->import_template_id, 'name' => $imp->template->name ] : null,
];
}, $imports['data']);
return Inertia::render('Imports/Index', [
'imports' => $imports,
]);
}
// Show the import creation page
public function create(Request $request)
{
$templates = ImportTemplate::query()
->leftJoin('clients', 'clients.id', '=', 'import_templates.client_id')
->where('import_templates.is_active', true)
->orderBy('import_templates.name')
->get([
'import_templates.id',
'import_templates.uuid',
'import_templates.name',
'import_templates.source_type',
'import_templates.default_record_type',
'import_templates.client_id',
DB::raw('clients.uuid as client_uuid'),
]);
$clients = Client::query()
->join('person', 'person.id', '=', 'clients.person_id')
->orderBy('person.full_name')
->get([
'clients.id',
'clients.uuid',
DB::raw('person.full_name as name'),
]);
return Inertia::render('Imports/Create', [
'templates' => $templates,
'clients' => $clients,
// no existing import on create
]);
}
// Create a new import job, store file, and return basic info
public function store(Request $request)
{
$validated = $request->validate([
'file' => 'required|file|max:20480', // 20MB; adjust as needed
'source_type' => 'nullable|string|in:csv,xml,xls,xlsx,json,txt',
'sheet_name' => 'nullable|string|max:64',
'has_header' => 'nullable|boolean',
'import_template_id' => 'nullable|integer|exists:import_templates,id',
'client_uuid' => 'nullable|string|exists:clients,uuid',
]);
$file = $validated['file'];
$ext = strtolower($file->getClientOriginalExtension());
$sourceType = $validated['source_type'] ?? ($ext === 'txt' ? 'csv' : $ext);
$uuid = (string) Str::uuid();
$disk = 'local';
$path = $file->storeAs('imports', $uuid.'.'.$ext, $disk);
// Resolve client_uuid to client_id if provided
$clientId = null;
if (!empty($validated['client_uuid'] ?? null)) {
$clientId = Client::where('uuid', $validated['client_uuid'])->value('id');
}
$import = Import::create([
'uuid' => $uuid,
'user_id' => $request->user()?->id,
'import_template_id' => $validated['import_template_id'] ?? null,
'client_id' => $clientId,
'source_type' => $sourceType,
'file_name' => basename($path),
'original_name' => $file->getClientOriginalName(),
'disk' => $disk,
'path' => $path,
'size' => $file->getSize(),
'sheet_name' => $validated['sheet_name'] ?? null,
'status' => 'uploaded',
'meta' => [
'has_header' => $validated['has_header'] ?? true,
],
]);
return response()->json([
'id' => $import->id,
'uuid' => $import->uuid,
'status' => $import->status,
]);
}
// Kick off processing of an import - simple synchronous step for now
public function process(Import $import, Request $request, ImportProcessor $processor)
{
$import->update(['status' => 'validating', 'started_at' => now()]);
$result = $processor->process($import, user: $request->user());
return response()->json($result);
}
// Analyze the uploaded file and return column headers or positional indices
public function columns(Request $request, Import $import, CsvImportService $csv)
{
$validated = $request->validate([
'has_header' => 'nullable|boolean',
]);
$hasHeader = array_key_exists('has_header', $validated)
? (bool) $validated['has_header']
: (bool) ($import->meta['has_header'] ?? true);
// Only implement CSV/TSV detection for now; others can be added later
if (!in_array($import->source_type, ['csv','txt'])) {
return response()->json([
'columns' => [],
'note' => 'Column preview supported for CSV/TXT at this step.',
]);
}
$fullPath = Storage::disk($import->disk)->path($import->path);
[$delimiter, $columns] = $csv->detectColumnsFromCsv($fullPath, $hasHeader);
// Save meta
$meta = $import->meta ?? [];
$meta['has_header'] = $hasHeader;
$meta['detected_delimiter'] = $delimiter;
$meta['columns'] = $columns;
$import->update([
'meta' => $meta,
'status' => $import->status === 'uploaded' ? 'parsed' : $import->status,
]);
return response()->json([
'columns' => $columns,
'has_header' => $hasHeader,
'detected_delimiter' => $delimiter,
]);
}
// CSV helpers moved to App\Services\CsvImportService
// Save ad-hoc mappings for a specific import (when no template is selected)
public function saveMappings(Request $request, Import $import)
{
$data = $request->validate([
'mappings' => 'required|array',
'mappings.*.source_column' => 'required|string',
'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts',
'mappings.*.target_field' => 'required|string',
'mappings.*.transform' => 'nullable|string|in:trim,upper,lower',
'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both',
'mappings.*.options' => 'nullable|array',
]);
// Upsert by (import_id, source_column): update existing rows; insert new ones; avoid duplicates
$now = now();
$existing = \DB::table('import_mappings')
->where('import_id', $import->id)
->get(['id','source_column','position']);
$bySource = [];
$dupes = [];
foreach ($existing as $row) {
$src = (string) $row->source_column;
if (!array_key_exists($src, $bySource)) {
$bySource[$src] = [ 'id' => $row->id, 'position' => $row->position ];
} else {
$dupes[$src] = ($dupes[$src] ?? []);
$dupes[$src][] = $row->id;
}
}
$basePosition = (int) (\DB::table('import_mappings')->where('import_id', $import->id)->max('position') ?? -1);
$inserted = 0; $updated = 0; $deduped = 0;
foreach ($data['mappings'] as $pos => $m) {
$src = (string) $m['source_column'];
$payload = [
'entity' => $m['entity'] ?? null,
'target_field' => $m['target_field'],
'transform' => $m['transform'] ?? null,
'apply_mode' => $m['apply_mode'] ?? 'both',
'options' => $m['options'] ?? null,
'position' => $pos, // keep UI order
'updated_at' => $now,
];
if (array_key_exists($src, $bySource)) {
// Update first occurrence
\DB::table('import_mappings')->where('id', $bySource[$src]['id'])->update($payload);
$updated++;
// Remove duplicates if any
if (!empty($dupes[$src])) {
$deleted = \DB::table('import_mappings')->whereIn('id', $dupes[$src])->delete();
$deduped += (int) $deleted;
unset($dupes[$src]);
}
} else {
// Insert new
\DB::table('import_mappings')->insert([
'import_id' => $import->id,
'entity' => $payload['entity'],
'source_column' => $src,
'target_field' => $payload['target_field'],
'transform' => $payload['transform'],
'apply_mode' => $payload['apply_mode'],
'options' => $payload['options'],
'position' => ++$basePosition,
'created_at' => $now,
'updated_at' => $now,
]);
$inserted++;
}
}
// Mark this as ad-hoc mapping usage
$import->update(['import_template_id' => null]);
return response()->json(['ok' => true, 'saved' => ($inserted + $updated), 'inserted' => $inserted, 'updated' => $updated, 'deduped' => $deduped]);
}
// Fetch current mappings for an import (after applying a template or saving ad-hoc mappings)
public function getMappings(Import $import)
{
$rows = \DB::table('import_mappings')
->where('import_id', $import->id)
->orderBy('position')
->orderBy('id')
->get([
'id',
'entity',
'source_column',
'target_field',
'transform',
'apply_mode',
'options',
'position'
]);
return response()->json(['mappings' => $rows]);
}
// Fetch recent import events (logs) for an import
public function getEvents(Import $import)
{
$limit = (int) request()->query('limit', 200);
$limit = max(1, min($limit, 1000));
$events = ImportEvent::query()
->where('import_id', $import->id)
->orderByDesc('id')
->limit($limit)
->get(['id','created_at','level','event','message','import_row_id','context']);
return response()->json(['events' => $events]);
}
// Show an existing import by UUID to continue where left off
public function show(Import $import)
{
// Load templates (global + client specific) and clients for selection on continue page
$templates = ImportTemplate::query()
->leftJoin('clients', 'clients.id', '=', 'import_templates.client_id')
->where('import_templates.is_active', true)
->orderBy('import_templates.name')
->get([
'import_templates.id',
'import_templates.uuid',
'import_templates.name',
'import_templates.source_type',
'import_templates.default_record_type',
'import_templates.client_id',
DB::raw('clients.uuid as client_uuid'),
]);
$clients = Client::query()
->join('person', 'person.id', '=', 'clients.person_id')
->orderBy('person.full_name')
->get([
'clients.id',
'clients.uuid',
DB::raw('person.full_name as name'),
]);
// Render a dedicated page to continue the import
return Inertia::render('Imports/Import', [
'import' => [
'id' => $import->id,
'uuid' => $import->uuid,
'status' => $import->status,
'meta' => $import->meta,
'client_id' => $import->client_id,
'import_template_id' => $import->import_template_id,
'total_rows' => $import->total_rows,
'imported_rows' => $import->imported_rows,
'invalid_rows' => $import->invalid_rows,
'valid_rows' => $import->valid_rows,
'finished_at' => $import->finished_at,
],
'templates' => $templates,
'clients' => $clients,
]);
}
}
@@ -0,0 +1,432 @@
<?php
namespace App\Http\Controllers;
use App\Models\Import;
use App\Models\ImportTemplate;
use App\Models\ImportTemplateMapping;
use App\Models\Client;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Inertia;
class ImportTemplateController extends Controller
{
public function index()
{
$templates = ImportTemplate::query()
->with(['client:id,uuid,person_id', 'client.person:id,full_name'])
->orderBy('name')
->get();
return Inertia::render('Imports/Templates/Index', [
'templates' => $templates->map(fn($t) => [
'uuid' => $t->uuid,
'name' => $t->name,
'description' => $t->description,
'source_type' => $t->source_type,
'is_active' => $t->is_active,
'client' => $t->client ? [
'uuid' => $t->client->uuid,
'name' => $t->client->person?->full_name,
] : null,
]),
]);
}
// Show the template creation page
public function create()
{
// Preload clients for optional association (global when null)
$clients = Client::query()
->join('person', 'person.id', '=', 'clients.person_id')
->orderBy('person.full_name')
->get([
'clients.id', // kept for compatibility, UI will use uuid
'clients.uuid',
DB::raw('person.full_name as name'),
]);
return Inertia::render('Imports/Templates/Create', [
'clients' => $clients,
]);
}
public function store(Request $request)
{
// Normalize payload to be resilient to UI variations
$raw = $request->all();
// Resolve client by uuid if provided, or cast string numeric to int
if (!empty($raw['client_uuid'] ?? null)) {
$raw['client_id'] = Client::where('uuid', $raw['client_uuid'])->value('id');
} elseif (isset($raw['client_id']) && is_string($raw['client_id']) && ctype_digit($raw['client_id'])) {
$raw['client_id'] = (int) $raw['client_id'];
}
// Normalize entities to array of strings
if (isset($raw['entities']) && is_array($raw['entities'])) {
$raw['entities'] = array_values(array_filter(array_map(function ($e) {
if (is_string($e)) return $e;
if (is_array($e) && array_key_exists('value', $e)) return (string) $e['value'];
return null;
}, $raw['entities'])));
}
$data = validator($raw, [
'name' => 'required|string|max:100',
'description' => 'nullable|string|max:255',
'source_type' => 'required|string|in:csv,xml,xls,xlsx,json',
'default_record_type' => 'nullable|string|max:50',
'sample_headers' => 'nullable|array',
'client_id' => 'nullable|integer|exists:clients,id',
'is_active' => 'boolean',
'entities' => 'nullable|array',
'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts',
'mappings' => 'array',
'mappings.*.source_column' => 'required|string',
'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts',
'mappings.*.target_field' => 'nullable|string',
'mappings.*.transform' => 'nullable|string|max:50',
'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both',
'mappings.*.options' => 'nullable|array',
'mappings.*.position' => 'nullable|integer',
])->validate();
$template = null;
DB::transaction(function () use (&$template, $request, $data) {
$template = ImportTemplate::create([
'uuid' => (string) Str::uuid(),
'name' => $data['name'],
'description' => $data['description'] ?? null,
'source_type' => $data['source_type'],
'default_record_type' => $data['default_record_type'] ?? null,
'sample_headers' => $data['sample_headers'] ?? null,
'user_id' => $request->user()?->id,
'client_id' => $data['client_id'] ?? null,
'is_active' => $data['is_active'] ?? true,
'meta' => [
'entities' => $data['entities'] ?? [],
],
]);
foreach (($data['mappings'] ?? []) as $m) {
ImportTemplateMapping::create([
'import_template_id' => $template->id,
'entity' => $m['entity'] ?? null,
'source_column' => $m['source_column'],
'target_field' => $m['target_field'] ?? null,
'transform' => $m['transform'] ?? null,
'apply_mode' => $m['apply_mode'] ?? 'both',
'options' => $m['options'] ?? null,
'position' => $m['position'] ?? null,
]);
}
});
// Redirect to edit page for the newly created template
return redirect()
->route('importTemplates.edit', ['template' => $template->uuid])
->with('success', 'Template created successfully.');
}
// Edit template UI (by uuid)
public function edit(ImportTemplate $template)
{
// Eager-load mappings
$template->load(['mappings']);
// Preload clients list (uuid + name) for possible reassignment
$clients = Client::query()
->join('person', 'person.id', '=', 'clients.person_id')
->orderBy('person.full_name')
->get([
'clients.uuid',
DB::raw('person.full_name as name'),
]);
return Inertia::render('Imports/Templates/Edit', [
'template' => [
'uuid' => $template->uuid,
'name' => $template->name,
'description' => $template->description,
'source_type' => $template->source_type,
'default_record_type' => $template->default_record_type,
'is_active' => $template->is_active,
'client_uuid' => $template->client?->uuid,
'sample_headers' => $template->sample_headers,
'meta' => $template->meta,
'mappings' => $template->mappings()->orderBy('position')->get(['id','entity','source_column','target_field','transform','apply_mode','options','position']),
],
'clients' => $clients,
]);
}
// Add a new mapping to a template (by uuid)
public function addMapping(Request $request, ImportTemplate $template)
{
// Normalize empty transform to null
$raw = $request->all();
if (array_key_exists('transform', $raw) && $raw['transform'] === '') {
$raw['transform'] = null;
}
$data = validator($raw, [
'source_column' => 'required|string',
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts',
'target_field' => 'nullable|string',
'transform' => 'nullable|string|in:trim,upper,lower',
'apply_mode' => 'nullable|string|in:insert,update,both',
'options' => 'nullable|array',
'position' => 'nullable|integer',
])->validate();
// Avoid duplicates by source_column within the same template: update if exists
$existing = ImportTemplateMapping::where('import_template_id', $template->id)
->where('source_column', $data['source_column'])
->first();
if ($existing) {
$existing->update([
'target_field' => $data['target_field'] ?? $existing->target_field,
'entity' => $data['entity'] ?? $existing->entity,
'transform' => $data['transform'] ?? $existing->transform,
'apply_mode' => $data['apply_mode'] ?? $existing->apply_mode ?? 'both',
'options' => $data['options'] ?? $existing->options,
'position' => $data['position'] ?? $existing->position,
]);
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
->with('info', 'Mapping already existed. Updated existing mapping.');
} else {
$position = $data['position'] ?? (int) (($template->mappings()->max('position') ?? 0) + 1);
ImportTemplateMapping::create([
'import_template_id' => $template->id,
'entity' => $data['entity'] ?? null,
'source_column' => $data['source_column'],
'target_field' => $data['target_field'] ?? null,
'transform' => $data['transform'] ?? null,
'apply_mode' => $data['apply_mode'] ?? 'both',
'options' => $data['options'] ?? null,
'position' => $position,
]);
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
->with('success', 'Mapping added');
}
}
// Update template basic fields
public function update(Request $request, ImportTemplate $template)
{
$raw = $request->all();
if (!empty($raw['client_uuid'] ?? null)) {
$raw['client_id'] = Client::where('uuid', $raw['client_uuid'])->value('id');
}
$data = validator($raw, [
'name' => 'required|string|max:100',
'description' => 'nullable|string|max:255',
'source_type' => 'required|string|in:csv,xml,xls,xlsx,json',
'default_record_type' => 'nullable|string|max:50',
'client_id' => 'nullable|integer|exists:clients,id',
'is_active' => 'boolean',
'sample_headers' => 'nullable|array',
])->validate();
$template->update([
'name' => $data['name'],
'description' => $data['description'] ?? null,
'source_type' => $data['source_type'],
'default_record_type' => $data['default_record_type'] ?? null,
'client_id' => $data['client_id'] ?? null,
'is_active' => $data['is_active'] ?? $template->is_active,
'sample_headers' => $data['sample_headers'] ?? $template->sample_headers,
]);
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
->with('success', 'Template updated');
}
// Bulk add multiple mappings from a textarea input
public function bulkAddMappings(Request $request, ImportTemplate $template)
{
// Accept either commas or newlines as separators
$raw = $request->all();
if (array_key_exists('transform', $raw) && $raw['transform'] === '') {
$raw['transform'] = null;
}
$data = validator($raw, [
'sources' => 'required|string', // comma and/or newline separated
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts',
'default_field' => 'nullable|string', // if provided, used as the field name for all entries
'apply_mode' => 'nullable|string|in:insert,update,both',
'transform' => 'nullable|string|in:trim,upper,lower',
])->validate();
$list = preg_split('/\r?\n|,/', $data['sources']);
$list = array_values(array_filter(array_map(fn($s) => trim($s), $list), fn($s) => $s !== ''));
if (empty($list)) {
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
->with('warning', 'No valid source columns provided.');
}
$basePosition = (int) (($template->mappings()->max('position') ?? 0));
$apply = $data['apply_mode'] ?? 'both';
$transform = $data['transform'] ?? null;
$entity = $data['entity'] ?? null;
$defaultField = $data['default_field'] ?? null; // allows forcing a specific field for all
$created = 0; $updated = 0;
DB::transaction(function () use ($template, $list, $apply, $transform, $entity, $defaultField, $basePosition, &$created, &$updated) {
foreach ($list as $idx => $source) {
$targetField = null;
if ($defaultField) {
$targetField = $entity ? ($entity . '.' . $defaultField) : $defaultField;
} elseif ($entity) {
$targetField = $entity . '.' . $source;
}
$existing = ImportTemplateMapping::where('import_template_id', $template->id)
->where('source_column', $source)
->first();
if ($existing) {
$existing->update([
'target_field' => $targetField ?? $existing->target_field,
'entity' => $entity ?? $existing->entity,
'transform' => $transform ?? $existing->transform,
'apply_mode' => $apply ?? $existing->apply_mode ?? 'both',
'options' => $existing->options,
// keep existing position
]);
$updated++;
} else {
ImportTemplateMapping::create([
'import_template_id' => $template->id,
'entity' => $entity,
'source_column' => $source,
'target_field' => $targetField,
'transform' => $transform,
'apply_mode' => $apply,
'options' => null,
'position' => $basePosition + $idx + 1,
]);
$created++;
}
}
});
$msg = [];
if ($created) $msg[] = "$created created";
if ($updated) $msg[] = "$updated updated";
$text = 'Mappings processed';
if (!empty($msg)) $text .= ': ' . implode(', ', $msg);
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
->with('success', $text);
}
// Update an existing mapping
public function updateMapping(Request $request, ImportTemplate $template, ImportTemplateMapping $mapping)
{
if ($mapping->import_template_id !== $template->id) abort(404);
$raw = $request->all();
if (array_key_exists('transform', $raw) && $raw['transform'] === '') {
$raw['transform'] = null;
}
$data = validator($raw, [
'source_column' => 'required|string',
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts',
'target_field' => 'nullable|string',
'transform' => 'nullable|string|in:trim,upper,lower',
'apply_mode' => 'nullable|string|in:insert,update,both',
'options' => 'nullable|array',
'position' => 'nullable|integer',
])->validate();
$mapping->update([
'source_column' => $data['source_column'],
'entity' => $data['entity'] ?? null,
'target_field' => $data['target_field'] ?? null,
'transform' => $data['transform'] ?? null,
'apply_mode' => $data['apply_mode'] ?? 'both',
'options' => $data['options'] ?? null,
'position' => $data['position'] ?? $mapping->position,
]);
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
->with('success', 'Mapping updated');
}
// Delete a mapping
public function deleteMapping(ImportTemplate $template, ImportTemplateMapping $mapping)
{
if ($mapping->import_template_id !== $template->id) abort(404);
$mapping->delete();
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
->with('success', 'Mapping deleted');
}
// Reorder mappings in bulk
public function reorderMappings(Request $request, ImportTemplate $template)
{
$data = $request->validate([
'order' => 'required|array',
'order.*' => 'integer',
]);
$ids = $data['order'];
// Ensure all ids belong to template
$validIds = ImportTemplateMapping::where('import_template_id', $template->id)
->whereIn('id', $ids)->pluck('id')->all();
if (count($validIds) !== count($ids)) abort(422, 'Invalid mapping ids');
// Apply new positions
foreach ($ids as $idx => $id) {
ImportTemplateMapping::where('id', $id)->update(['position' => $idx]);
}
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
->with('success', 'Mappings reordered');
}
// Apply a templates mappings to a specific import (copy into import_mappings)
public function applyToImport(Request $request, ImportTemplate $template, Import $import)
{
// optional: clear previous mappings
$clear = $request->boolean('clear', true);
$copied = 0;
DB::transaction(function () use ($clear, $template, $import, &$copied) {
if ($clear) {
\DB::table('import_mappings')->where('import_id', $import->id)->delete();
}
$rows = $template->mappings()->orderBy('position')->get();
foreach ($rows as $row) {
\DB::table('import_mappings')->insert([
'import_id' => $import->id,
'entity' => $row->entity,
'source_column' => $row->source_column,
'target_field' => $row->target_field,
'transform' => $row->transform,
'apply_mode' => $row->apply_mode ?? 'both',
'options' => $row->options,
'position' => $row->position ?? null,
'created_at' => now(),
'updated_at' => now(),
]);
$copied++;
}
$import->update(['import_template_id' => $template->id]);
});
return response()->json(['ok' => true, 'copied' => $copied, 'cleared' => $clear]);
}
// Delete a template and cascade delete its mappings; detach from imports
public function destroy(ImportTemplate $template)
{
DB::transaction(function () use ($template) {
// Nullify references from imports to this template
\DB::table('imports')->where('import_template_id', $template->id)->update(['import_template_id' => null]);
// Delete mappings first (if FK cascade not set)
\DB::table('import_template_mappings')->where('import_template_id', $template->id)->delete();
// Delete the template
$template->delete();
});
return redirect()->route('importTemplates.index')->with('success', 'Template deleted');
}
}
+57 -4
View File
@@ -49,10 +49,14 @@ public function createAddress(Person $person, Request $request){
'description' => 'nullable|string|max:125'
]);
$address_id = $person->addresses()->create($attributes)->id;
// Dedup: avoid duplicate address per person by (address, country)
$address = $person->addresses()->firstOrCreate([
'address' => $attributes['address'],
'country' => $attributes['country'] ?? null,
], $attributes);
return response()->json([
'address' => \App\Models\Person\PersonAddress::with(['type'])->findOrFail($address_id)
'address' => \App\Models\Person\PersonAddress::with(['type'])->findOrFail($address->id)
]);
}
@@ -83,10 +87,14 @@ public function createPhone(Person $person, Request $request)
'description' => 'nullable|string|max:125'
]);
$phone_id = $person->phones()->create($attributes)->id;
// Dedup: avoid duplicate phone per person by (nu, country_code)
$phone = $person->phones()->firstOrCreate([
'nu' => $attributes['nu'],
'country_code' => $attributes['country_code'] ?? null,
], $attributes);
return response()->json([
'phone' => \App\Models\Person\PersonPhone::with(['type'])->findOrFail($phone_id)
'phone' => \App\Models\Person\PersonPhone::with(['type'])->findOrFail($phone->id)
]);
}
@@ -107,4 +115,49 @@ public function updatePhone(Person $person, int $phone_id, Request $request)
'phone' => $phone
]);
}
public function createEmail(Person $person, Request $request)
{
$attributes = $request->validate([
'value' => 'required|email:rfc,dns|max:255',
'label' => 'nullable|string|max:50',
'is_primary' => 'boolean',
'is_active' => 'boolean',
'valid' => 'boolean',
'verified_at' => 'nullable|date',
'preferences' => 'nullable|array',
'meta' => 'nullable|array',
]);
// Dedup: avoid duplicate email per person by value
$email = $person->emails()->firstOrCreate([
'value' => $attributes['value'],
], $attributes);
return response()->json([
'email' => \App\Models\Email::findOrFail($email->id)
]);
}
public function updateEmail(Person $person, int $email_id, Request $request)
{
$attributes = $request->validate([
'value' => 'required|email:rfc,dns|max:255',
'label' => 'nullable|string|max:50',
'is_primary' => 'boolean',
'is_active' => 'boolean',
'valid' => 'boolean',
'verified_at' => 'nullable|date',
'preferences' => 'nullable|array',
'meta' => 'nullable|array',
]);
$email = $person->emails()->findOrFail($email_id);
$email->update($attributes);
return response()->json([
'email' => $email
]);
}
}
@@ -36,7 +36,12 @@ public function version(Request $request): ?string
public function share(Request $request): array
{
return array_merge(parent::share($request), [
//
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
'warning' => fn () => $request->session()->get('warning'),
'info' => fn () => $request->session()->get('info'),
],
]);
}
}