- Removido README.md padrão do Laravel (backend) - Removidos scripts de deploy (não mais necessários) - Atualizado copilot-instructions.md para novo fluxo - Adicionada documentação de auditoria do servidor - Sincronizado código de produção com repositório Novo workflow: - Trabalhamos diretamente em /root/webmoney (symlink para /var/www/webmoney) - Mudanças PHP são instantâneas - Mudanças React requerem 'npm run build' - Commit após validação funcional
402 lines
13 KiB
PHP
Executable File
402 lines
13 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\ImportMapping;
|
|
use App\Models\ImportLog;
|
|
use App\Services\Import\ImportService;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Support\Str;
|
|
|
|
class ImportController extends Controller
|
|
{
|
|
protected ImportService $importService;
|
|
|
|
public function __construct(ImportService $importService)
|
|
{
|
|
$this->importService = $importService;
|
|
}
|
|
|
|
/**
|
|
* Upload file and get preview
|
|
*/
|
|
public function upload(Request $request): JsonResponse
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'file' => 'required|file|max:10240', // 10MB max
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Validation failed',
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$file = $request->file('file');
|
|
$extension = strtolower($file->getClientOriginalExtension());
|
|
|
|
// Verificar extensão permitida
|
|
if (!in_array($extension, ImportMapping::FILE_TYPES)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => "Unsupported file type: $extension. Allowed: " . implode(', ', ImportMapping::FILE_TYPES),
|
|
], 422);
|
|
}
|
|
|
|
// Salvar arquivo temporariamente
|
|
$filename = Str::uuid() . '.' . $extension;
|
|
$path = $file->storeAs('imports/temp', $filename);
|
|
$fullPath = Storage::path($path);
|
|
|
|
try {
|
|
// Obter preview
|
|
$preview = $this->importService->getPreview($fullPath, 15);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'temp_file' => $filename,
|
|
'original_name' => $file->getClientOriginalName(),
|
|
'file_type' => $extension,
|
|
'size' => $file->getSize(),
|
|
'preview' => $preview,
|
|
],
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
// Limpar arquivo em caso de erro
|
|
Storage::delete($path);
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Error processing file: ' . $e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get headers from file with specific row
|
|
*/
|
|
public function getHeaders(Request $request): JsonResponse
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'temp_file' => 'required|string',
|
|
'header_row' => 'required|integer|min:0',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$fullPath = Storage::path('imports/temp/' . $request->temp_file);
|
|
|
|
if (!file_exists($fullPath)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Temporary file not found. Please upload again.',
|
|
], 404);
|
|
}
|
|
|
|
try {
|
|
$headers = $this->importService->getHeaders($fullPath, [
|
|
'header_row' => $request->header_row,
|
|
]);
|
|
|
|
// Sugerir mapeamentos
|
|
$suggestions = $this->importService->suggestMapping($headers);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'headers' => $headers,
|
|
'suggestions' => $suggestions,
|
|
],
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Error reading headers: ' . $e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process import with mapping
|
|
*/
|
|
public function import(Request $request): JsonResponse
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'temp_file' => 'required|string',
|
|
'mapping_id' => 'nullable|exists:import_mappings,id',
|
|
'column_mappings' => 'required_without:mapping_id|array',
|
|
'header_row' => 'required_without:mapping_id|integer|min:0',
|
|
'data_start_row' => 'required_without:mapping_id|integer|min:0',
|
|
'date_format' => 'nullable|string',
|
|
'decimal_separator' => 'nullable|string|max:1',
|
|
'thousands_separator' => 'nullable|string|max:1',
|
|
'account_id' => 'nullable|exists:accounts,id',
|
|
'category_id' => 'nullable|exists:categories,id',
|
|
'cost_center_id' => 'nullable|exists:cost_centers,id',
|
|
'save_mapping' => 'nullable|boolean',
|
|
'mapping_name' => 'required_if:save_mapping,true|nullable|string|max:255',
|
|
'bank_name' => 'nullable|string|max:100',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$fullPath = Storage::path('imports/temp/' . $request->temp_file);
|
|
|
|
if (!file_exists($fullPath)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Temporary file not found. Please upload again.',
|
|
], 404);
|
|
}
|
|
|
|
$userId = auth()->id();
|
|
|
|
try {
|
|
// Usar mapeamento existente ou criar novo
|
|
if ($request->mapping_id) {
|
|
$mapping = ImportMapping::where('user_id', $userId)
|
|
->findOrFail($request->mapping_id);
|
|
} else {
|
|
$extension = pathinfo($request->temp_file, PATHINFO_EXTENSION);
|
|
|
|
$mappingData = [
|
|
'user_id' => $userId,
|
|
'name' => $request->mapping_name ?? 'Importação ' . now()->format('d/m/Y H:i'),
|
|
'bank_name' => $request->bank_name,
|
|
'file_type' => $extension,
|
|
'header_row' => $request->header_row,
|
|
'data_start_row' => $request->data_start_row,
|
|
'date_format' => $request->date_format ?? 'd/m/Y',
|
|
'decimal_separator' => $request->decimal_separator ?? ',',
|
|
'thousands_separator' => $request->thousands_separator ?? '.',
|
|
'column_mappings' => $request->column_mappings,
|
|
'default_account_id' => $request->account_id,
|
|
'default_category_id' => $request->category_id,
|
|
'default_cost_center_id' => $request->cost_center_id,
|
|
'is_active' => $request->save_mapping ?? false,
|
|
];
|
|
|
|
if ($request->save_mapping) {
|
|
$mapping = ImportMapping::create($mappingData);
|
|
} else {
|
|
$mapping = new ImportMapping($mappingData);
|
|
// Não definir ID para mapeamento temporário - será tratado no ImportService
|
|
}
|
|
}
|
|
|
|
// Executar importação
|
|
$importLog = $this->importService->importTransactions(
|
|
$fullPath,
|
|
$mapping,
|
|
$userId,
|
|
$request->account_id,
|
|
$request->category_id,
|
|
$request->cost_center_id
|
|
);
|
|
|
|
// Limpar arquivo temporário
|
|
Storage::delete('imports/temp/' . $request->temp_file);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => "Importação concluída: {$importLog->imported_rows} transações importadas",
|
|
'data' => [
|
|
'import_log' => $importLog,
|
|
'mapping_saved' => $request->save_mapping && isset($mapping->id) && $mapping->id > 0,
|
|
],
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Import failed: ' . $e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List saved mappings
|
|
*/
|
|
public function mappings(Request $request): JsonResponse
|
|
{
|
|
$mappings = ImportMapping::where('user_id', auth()->id())
|
|
->where('is_active', true)
|
|
->with(['defaultAccount', 'defaultCategory', 'defaultCostCenter'])
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $mappings,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get single mapping
|
|
*/
|
|
public function getMapping(int $id): JsonResponse
|
|
{
|
|
$mapping = ImportMapping::where('user_id', auth()->id())
|
|
->with(['defaultAccount', 'defaultCategory', 'defaultCostCenter'])
|
|
->findOrFail($id);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $mapping,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Update mapping
|
|
*/
|
|
public function updateMapping(Request $request, int $id): JsonResponse
|
|
{
|
|
$mapping = ImportMapping::where('user_id', auth()->id())
|
|
->findOrFail($id);
|
|
|
|
$validator = Validator::make($request->all(), [
|
|
'name' => 'sometimes|string|max:255',
|
|
'bank_name' => 'nullable|string|max:100',
|
|
'header_row' => 'sometimes|integer|min:0',
|
|
'data_start_row' => 'sometimes|integer|min:0',
|
|
'date_format' => 'sometimes|string',
|
|
'decimal_separator' => 'sometimes|string|max:1',
|
|
'thousands_separator' => 'sometimes|string|max:1',
|
|
'column_mappings' => 'sometimes|array',
|
|
'default_account_id' => 'nullable|exists:accounts,id',
|
|
'default_category_id' => 'nullable|exists:categories,id',
|
|
'default_cost_center_id' => 'nullable|exists:cost_centers,id',
|
|
'is_active' => 'sometimes|boolean',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$mapping->update($request->all());
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $mapping->fresh(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Delete mapping
|
|
*/
|
|
public function deleteMapping(int $id): JsonResponse
|
|
{
|
|
$mapping = ImportMapping::where('user_id', auth()->id())
|
|
->findOrFail($id);
|
|
|
|
$mapping->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Mapping deleted successfully',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get available bank presets
|
|
*/
|
|
public function presets(): JsonResponse
|
|
{
|
|
$presets = $this->importService->getAvailablePresets();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $presets,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Create mapping from preset
|
|
*/
|
|
public function createFromPreset(Request $request): JsonResponse
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'preset' => 'required|string',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
try {
|
|
$mapping = $this->importService->createBankPreset(
|
|
$request->preset,
|
|
auth()->id()
|
|
);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $mapping,
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
], 400);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get import history
|
|
*/
|
|
public function history(Request $request): JsonResponse
|
|
{
|
|
$logs = ImportLog::where('user_id', auth()->id())
|
|
->with('importMapping')
|
|
->orderBy('created_at', 'desc')
|
|
->limit(50)
|
|
->get();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $logs,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get mappable fields info
|
|
*/
|
|
public function fields(): JsonResponse
|
|
{
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'fields' => ImportMapping::MAPPABLE_FIELDS,
|
|
'date_formats' => ImportMapping::DATE_FORMATS,
|
|
'file_types' => ImportMapping::FILE_TYPES,
|
|
],
|
|
]);
|
|
}
|
|
}
|