- Redesigned category create/edit modal with elegant wizard-style UI - Redesigned batch categorization modal with visual cards and better preview - Added missing i18n translations (common.continue, creating, remove) - Added budgets.general and wizard translations for ES, PT-BR, EN - Fixed 3 demo user transactions that were missing categories
421 lines
14 KiB
PHP
421 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\AssetAccount;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
class AssetAccountController extends Controller
|
|
{
|
|
/**
|
|
* Listar todos os ativos do usuário
|
|
*/
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$query = AssetAccount::where('user_id', Auth::id());
|
|
|
|
// Filtros
|
|
if ($request->has('asset_type') && $request->asset_type) {
|
|
$query->where('asset_type', $request->asset_type);
|
|
}
|
|
|
|
if ($request->has('status') && $request->status) {
|
|
$query->where('status', $request->status);
|
|
}
|
|
|
|
if ($request->has('search') && $request->search) {
|
|
$search = $request->search;
|
|
$query->where(function($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('description', 'like', "%{$search}%")
|
|
->orWhere('document_number', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// Ordenação
|
|
$sortBy = $request->get('sort_by', 'created_at');
|
|
$sortDir = $request->get('sort_dir', 'desc');
|
|
$query->orderBy($sortBy, $sortDir);
|
|
|
|
// Paginação ou todos
|
|
if ($request->has('per_page')) {
|
|
$assets = $query->paginate($request->per_page);
|
|
} else {
|
|
$assets = $query->get();
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $assets,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Criar novo ativo
|
|
*/
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'asset_type' => ['required', Rule::in(array_keys(AssetAccount::ASSET_TYPES))],
|
|
'name' => 'required|string|max:255',
|
|
'description' => 'nullable|string',
|
|
'currency' => 'required|string|size:3',
|
|
'color' => 'nullable|string|max:7',
|
|
'acquisition_value' => 'required|numeric|min:0',
|
|
'current_value' => 'required|numeric|min:0',
|
|
'acquisition_date' => 'nullable|date',
|
|
// Campos opcionais conforme tipo
|
|
'property_type' => 'nullable|string',
|
|
'investment_type' => 'nullable|string',
|
|
'depreciation_method' => 'nullable|string',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$data = $request->all();
|
|
$data['user_id'] = Auth::id();
|
|
$data['business_id'] = $request->business_id ?? Auth::user()->businesses()->first()?->id;
|
|
|
|
$asset = AssetAccount::create($data);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Activo creado con éxito',
|
|
'data' => $asset,
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* Ver um ativo específico
|
|
*/
|
|
public function show(AssetAccount $assetAccount): JsonResponse
|
|
{
|
|
// Verificar se pertence ao usuário
|
|
if ($assetAccount->user_id !== Auth::id()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No autorizado',
|
|
], 403);
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $assetAccount->load('linkedLiability'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Atualizar ativo
|
|
*/
|
|
public function update(Request $request, AssetAccount $assetAccount): JsonResponse
|
|
{
|
|
if ($assetAccount->user_id !== Auth::id()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No autorizado',
|
|
], 403);
|
|
}
|
|
|
|
$validator = Validator::make($request->all(), [
|
|
'asset_type' => ['nullable', Rule::in(array_keys(AssetAccount::ASSET_TYPES))],
|
|
'name' => 'nullable|string|max:255',
|
|
'current_value' => 'nullable|numeric|min:0',
|
|
'status' => ['nullable', Rule::in(array_keys(AssetAccount::STATUSES))],
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$assetAccount->update($request->all());
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Activo actualizado con éxito',
|
|
'data' => $assetAccount->fresh(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Deletar ativo
|
|
*/
|
|
public function destroy(AssetAccount $assetAccount): JsonResponse
|
|
{
|
|
if ($assetAccount->user_id !== Auth::id()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No autorizado',
|
|
], 403);
|
|
}
|
|
|
|
$assetAccount->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Activo eliminado con éxito',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Retornar tipos de ativos e opções para o wizard
|
|
*/
|
|
public function assetTypes(): JsonResponse
|
|
{
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => AssetAccount::ASSET_TYPES,
|
|
'property_types' => AssetAccount::PROPERTY_TYPES,
|
|
'investment_types' => AssetAccount::INVESTMENT_TYPES,
|
|
'depreciation_methods' => AssetAccount::DEPRECIATION_METHODS,
|
|
'index_types' => AssetAccount::INDEX_TYPES,
|
|
'statuses' => AssetAccount::STATUSES,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Criar ativo via wizard
|
|
*/
|
|
public function storeWithWizard(Request $request): JsonResponse
|
|
{
|
|
$rules = [
|
|
// Step 1 - Tipo
|
|
'asset_type' => ['required', Rule::in(array_keys(AssetAccount::ASSET_TYPES))],
|
|
|
|
// Step 2 - Dados básicos
|
|
'name' => 'required|string|max:255',
|
|
'description' => 'nullable|string|max:1000',
|
|
'currency' => 'required|string|size:3',
|
|
'color' => 'nullable|string|max:7',
|
|
|
|
// Step 3 - Valores
|
|
'acquisition_value' => 'required|numeric|min:0',
|
|
'current_value' => 'required|numeric|min:0',
|
|
'acquisition_date' => 'nullable|date',
|
|
|
|
// Depreciação
|
|
'is_depreciable' => 'nullable|boolean',
|
|
'depreciation_method' => ['nullable', Rule::in(array_keys(AssetAccount::DEPRECIATION_METHODS))],
|
|
'useful_life_years' => 'nullable|numeric|min:0.5|max:100',
|
|
'residual_value' => 'nullable|numeric|min:0',
|
|
|
|
// Imóveis
|
|
'property_type' => ['nullable', Rule::in(array_keys(AssetAccount::PROPERTY_TYPES))],
|
|
'address' => 'nullable|string|max:500',
|
|
'city' => 'nullable|string|max:100',
|
|
'state' => 'nullable|string|max:100',
|
|
'postal_code' => 'nullable|string|max:20',
|
|
'country' => 'nullable|string|size:2',
|
|
'property_area_m2' => 'nullable|numeric|min:0',
|
|
'registry_number' => 'nullable|string|max:100',
|
|
|
|
// Veículos
|
|
'vehicle_brand' => 'nullable|string|max:100',
|
|
'vehicle_model' => 'nullable|string|max:100',
|
|
'vehicle_year' => 'nullable|integer|min:1900|max:2100',
|
|
'vehicle_plate' => 'nullable|string|max:20',
|
|
'vehicle_vin' => 'nullable|string|max:50',
|
|
'vehicle_mileage' => 'nullable|integer|min:0',
|
|
|
|
// Investimentos
|
|
'investment_type' => ['nullable', Rule::in(array_keys(AssetAccount::INVESTMENT_TYPES))],
|
|
'institution' => 'nullable|string|max:100',
|
|
'account_number' => 'nullable|string|max:100',
|
|
'quantity' => 'nullable|integer|min:0',
|
|
'unit_price' => 'nullable|numeric|min:0',
|
|
'ticker' => 'nullable|string|max:20',
|
|
'maturity_date' => 'nullable|date',
|
|
'interest_rate' => 'nullable|numeric|min:0|max:100',
|
|
'index_type' => ['nullable', Rule::in(array_keys(AssetAccount::INDEX_TYPES))],
|
|
|
|
// Equipamentos
|
|
'equipment_brand' => 'nullable|string|max:100',
|
|
'equipment_model' => 'nullable|string|max:100',
|
|
'serial_number' => 'nullable|string|max:100',
|
|
'warranty_expiry' => 'nullable|date',
|
|
|
|
// Recebíveis
|
|
'debtor_name' => 'nullable|string|max:200',
|
|
'debtor_document' => 'nullable|string|max:50',
|
|
'receivable_due_date' => 'nullable|date',
|
|
'receivable_amount' => 'nullable|numeric|min:0',
|
|
|
|
// Garantias
|
|
'is_collateral' => 'nullable|boolean',
|
|
'collateral_for' => 'nullable|string|max:200',
|
|
'linked_liability_id' => 'nullable|integer|exists:liability_accounts,id',
|
|
|
|
// Seguros
|
|
'has_insurance' => 'nullable|boolean',
|
|
'insurance_company' => 'nullable|string|max:100',
|
|
'insurance_policy' => 'nullable|string|max:100',
|
|
'insurance_value' => 'nullable|numeric|min:0',
|
|
'insurance_expiry' => 'nullable|date',
|
|
|
|
// Gestão
|
|
'alert_days_before' => 'nullable|integer|min:0|max:365',
|
|
'internal_responsible' => 'nullable|string|max:200',
|
|
'internal_notes' => 'nullable|string|max:2000',
|
|
'document_number' => 'nullable|string|max:100',
|
|
];
|
|
|
|
$validator = Validator::make($request->all(), $rules);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
// Criar o ativo
|
|
$data = $validator->validated();
|
|
$data['user_id'] = Auth::id();
|
|
$data['business_id'] = $request->business_id ?? Auth::user()->businesses()->first()?->id;
|
|
$data['status'] = 'active';
|
|
|
|
$asset = AssetAccount::create($data);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Activo creado con éxito',
|
|
'data' => $asset,
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* Resumo dos ativos do usuário
|
|
*/
|
|
public function summary(): JsonResponse
|
|
{
|
|
$userId = Auth::id();
|
|
|
|
$summary = [
|
|
'total_assets' => AssetAccount::where('user_id', $userId)->active()->count(),
|
|
'total_value' => AssetAccount::where('user_id', $userId)->active()->sum('current_value'),
|
|
'total_acquisition' => AssetAccount::where('user_id', $userId)->active()->sum('acquisition_value'),
|
|
'by_type' => [],
|
|
];
|
|
|
|
// Agrupar por tipo
|
|
$byType = AssetAccount::where('user_id', $userId)
|
|
->active()
|
|
->selectRaw('asset_type, COUNT(*) as count, SUM(current_value) as total_value')
|
|
->groupBy('asset_type')
|
|
->get();
|
|
|
|
foreach ($byType as $item) {
|
|
$summary['by_type'][$item->asset_type] = [
|
|
'name' => AssetAccount::ASSET_TYPES[$item->asset_type]['name'] ?? $item->asset_type,
|
|
'count' => $item->count,
|
|
'total_value' => $item->total_value,
|
|
];
|
|
}
|
|
|
|
// Ganho/perda total
|
|
$summary['total_gain_loss'] = $summary['total_value'] - $summary['total_acquisition'];
|
|
$summary['total_gain_loss_percent'] = $summary['total_acquisition'] > 0
|
|
? (($summary['total_value'] - $summary['total_acquisition']) / $summary['total_acquisition']) * 100
|
|
: 0;
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $summary,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Atualizar valor de mercado de um ativo
|
|
*/
|
|
public function updateValue(Request $request, AssetAccount $assetAccount): JsonResponse
|
|
{
|
|
if ($assetAccount->user_id !== Auth::id()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No autorizado',
|
|
], 403);
|
|
}
|
|
|
|
$validator = Validator::make($request->all(), [
|
|
'current_value' => 'required|numeric|min:0',
|
|
'note' => 'nullable|string|max:500',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$assetAccount->update([
|
|
'current_value' => $request->current_value,
|
|
]);
|
|
|
|
// Adicionar nota se fornecida
|
|
if ($request->note) {
|
|
$notes = $assetAccount->internal_notes ?? '';
|
|
$notes .= "\n[" . now()->format('d/m/Y') . "] Valor actualizado: {$request->note}";
|
|
$assetAccount->update(['internal_notes' => trim($notes)]);
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Valor actualizado con éxito',
|
|
'data' => $assetAccount->fresh(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Registrar venda/baixa de ativo
|
|
*/
|
|
public function dispose(Request $request, AssetAccount $assetAccount): JsonResponse
|
|
{
|
|
if ($assetAccount->user_id !== Auth::id()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No autorizado',
|
|
], 403);
|
|
}
|
|
|
|
$validator = Validator::make($request->all(), [
|
|
'disposal_date' => 'required|date',
|
|
'disposal_value' => 'required|numeric|min:0',
|
|
'disposal_reason' => 'nullable|string|max:200',
|
|
'status' => ['required', Rule::in(['sold', 'written_off', 'depreciated'])],
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$assetAccount->update([
|
|
'status' => $request->status,
|
|
'disposal_date' => $request->disposal_date,
|
|
'disposal_value' => $request->disposal_value,
|
|
'disposal_reason' => $request->disposal_reason,
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Baja registrada con éxito',
|
|
'data' => $assetAccount->fresh(),
|
|
]);
|
|
}
|
|
}
|