webmoney/backend/app/Http/Controllers/Api/ServiceSheetController.php
marco 54cccdd095 refactor: migração para desenvolvimento direto no servidor
- 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
2025-12-19 11:45:32 +01:00

432 lines
16 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\ServiceSheet;
use App\Models\ServiceSheetItem;
use App\Models\BusinessSetting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class ServiceSheetController extends Controller
{
/**
* Lista todas as fichas de serviço do usuário
*/
public function index(Request $request): JsonResponse
{
$userId = $request->user()->id;
$query = ServiceSheet::ofUser($userId)
->with(['items', 'businessSetting:id,name,currency,markup_factor,fixed_expenses,employees_count,hours_per_week,working_days_per_week,working_days_per_month,productivity_rate']);
// Filtro por categoria
if ($request->has('category')) {
$query->where('category', $request->category);
}
// Filtro por status
if ($request->has('active')) {
$query->where('is_active', $request->boolean('active'));
}
// Ordenação
$sortBy = $request->get('sort_by', 'name');
$sortDir = $request->get('sort_dir', 'asc');
$query->orderBy($sortBy, $sortDir);
$sheets = $query->get()->map(function ($sheet) {
return array_merge($sheet->toArray(), [
'cost_breakdown' => $sheet->cost_breakdown,
]);
});
return response()->json($sheets);
}
/**
* Cria uma nova ficha de serviço
*/
public function store(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'code' => 'nullable|string|max:100',
'description' => 'nullable|string',
'category' => 'nullable|string|max:100',
'business_setting_id' => 'required|exists:business_settings,id',
'duration_minutes' => 'required|numeric|min:1',
'items' => 'nullable|array',
'items.*.name' => 'required|string|max:255',
'items.*.type' => 'required|in:supply,consumable,material,equipment_usage,other',
'items.*.unit_cost' => 'required|numeric|min:0',
'items.*.quantity_used' => 'nullable|numeric|min:0',
'items.*.unit' => 'nullable|string|max:20',
// Strategic pricing fields
'competitor_price' => 'nullable|numeric|min:0',
'min_price' => 'nullable|numeric|min:0',
'max_price' => 'nullable|numeric|min:0',
'premium_multiplier' => 'nullable|numeric|min:0.1|max:5',
'pricing_strategy' => 'nullable|in:aggressive,neutral,premium',
'psychological_pricing' => 'nullable|boolean',
'target_margin' => 'nullable|numeric|min:0|max:99',
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
$userId = $request->user()->id;
// Verificar se business_setting_id pertence ao usuário
$setting = BusinessSetting::ofUser($userId)->find($request->business_setting_id);
if (!$setting) {
return response()->json(['message' => 'Configuração de negócio não encontrada'], 404);
}
// Verificar se a configuração suporta serviços
if ($setting->business_type === 'products') {
return response()->json(['message' => 'Esta configuração é apenas para produtos. Altere para "Serviços" ou "Ambos".'], 422);
}
DB::beginTransaction();
try {
// Criar a ficha de serviço
$sheet = ServiceSheet::create([
'user_id' => $userId,
'business_setting_id' => $request->business_setting_id,
'name' => $request->name,
'code' => $request->code,
'description' => $request->description,
'category' => $request->category,
'duration_minutes' => $request->duration_minutes,
'competitor_price' => $request->competitor_price,
'min_price' => $request->min_price,
'max_price' => $request->max_price,
'premium_multiplier' => $request->premium_multiplier ?? 1,
'pricing_strategy' => $request->pricing_strategy ?? 'neutral',
'psychological_pricing' => $request->psychological_pricing ?? false,
'target_margin' => $request->target_margin,
]);
// Criar os itens (insumos)
if ($request->has('items') && is_array($request->items)) {
foreach ($request->items as $itemData) {
ServiceSheetItem::create([
'service_sheet_id' => $sheet->id,
'name' => $itemData['name'],
'type' => $itemData['type'],
'unit_cost' => $itemData['unit_cost'],
'quantity_used' => $itemData['quantity_used'] ?? 1,
'unit' => $itemData['unit'] ?? null,
'notes' => $itemData['notes'] ?? null,
]);
}
}
// Recalcular tudo
$sheet->recalculate();
DB::commit();
// Recarregar com relacionamentos
$sheet->load(['items', 'businessSetting:id,name,currency,markup_factor']);
return response()->json(array_merge($sheet->toArray(), [
'cost_breakdown' => $sheet->cost_breakdown,
]), 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['message' => 'Erro ao criar ficha de serviço: ' . $e->getMessage()], 500);
}
}
/**
* Exibe uma ficha de serviço específica
*/
public function show(Request $request, $id): JsonResponse
{
$sheet = ServiceSheet::ofUser($request->user()->id)
->with(['items', 'businessSetting'])
->findOrFail($id);
return response()->json(array_merge($sheet->toArray(), [
'cost_breakdown' => $sheet->cost_breakdown,
]));
}
/**
* Atualiza uma ficha de serviço
*/
public function update(Request $request, $id): JsonResponse
{
$sheet = ServiceSheet::ofUser($request->user()->id)->findOrFail($id);
$validator = Validator::make($request->all(), [
'name' => 'sometimes|string|max:255',
'code' => 'nullable|string|max:100',
'description' => 'nullable|string',
'category' => 'nullable|string|max:100',
'business_setting_id' => 'sometimes|exists:business_settings,id',
'duration_minutes' => 'sometimes|numeric|min:1',
'is_active' => 'sometimes|boolean',
// Strategic pricing fields
'competitor_price' => 'nullable|numeric|min:0',
'min_price' => 'nullable|numeric|min:0',
'max_price' => 'nullable|numeric|min:0',
'premium_multiplier' => 'nullable|numeric|min:0.1|max:5',
'pricing_strategy' => 'nullable|in:aggressive,neutral,premium',
'psychological_pricing' => 'nullable|boolean',
'target_margin' => 'nullable|numeric|min:0|max:99',
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
// Verificar se business_setting_id pertence ao usuário
if ($request->has('business_setting_id') && $request->business_setting_id) {
$setting = BusinessSetting::ofUser($request->user()->id)->find($request->business_setting_id);
if (!$setting) {
return response()->json(['message' => 'Configuração de negócio não encontrada'], 404);
}
// Verificar se suporta serviços
if ($setting->business_type === 'products') {
return response()->json(['message' => 'Esta configuração é apenas para produtos.'], 422);
}
}
$sheet->update($validator->validated());
// Recalcular preços
$sheet->recalculate();
$sheet->load(['items', 'businessSetting:id,name,currency,markup_factor']);
return response()->json(array_merge($sheet->toArray(), [
'cost_breakdown' => $sheet->cost_breakdown,
]));
}
/**
* Remove uma ficha de serviço
*/
public function destroy(Request $request, $id): JsonResponse
{
$sheet = ServiceSheet::ofUser($request->user()->id)->findOrFail($id);
$sheet->delete();
return response()->json(['message' => 'Ficha de serviço excluída com sucesso']);
}
/**
* Adiciona um item à ficha de serviço
*/
public function addItem(Request $request, $id): JsonResponse
{
$sheet = ServiceSheet::ofUser($request->user()->id)->findOrFail($id);
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'type' => 'required|in:supply,consumable,material,equipment_usage,other',
'unit_cost' => 'required|numeric|min:0',
'quantity_used' => 'nullable|numeric|min:0',
'unit' => 'nullable|string|max:20',
'notes' => 'nullable|string',
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
ServiceSheetItem::create([
'service_sheet_id' => $sheet->id,
'name' => $request->name,
'type' => $request->type,
'unit_cost' => $request->unit_cost,
'quantity_used' => $request->quantity_used ?? 1,
'unit' => $request->unit,
'notes' => $request->notes,
]);
// Recalcular preço
$sheet->recalculate();
$sheet->load(['items', 'businessSetting:id,name,currency,markup_factor']);
return response()->json(array_merge($sheet->toArray(), [
'cost_breakdown' => $sheet->cost_breakdown,
]));
}
/**
* Atualiza um item da ficha de serviço
*/
public function updateItem(Request $request, $sheetId, $itemId): JsonResponse
{
$sheet = ServiceSheet::ofUser($request->user()->id)->findOrFail($sheetId);
$item = ServiceSheetItem::where('service_sheet_id', $sheet->id)->findOrFail($itemId);
$validator = Validator::make($request->all(), [
'name' => 'sometimes|string|max:255',
'type' => 'sometimes|in:supply,consumable,material,equipment_usage,other',
'unit_cost' => 'sometimes|numeric|min:0',
'quantity_used' => 'sometimes|numeric|min:0',
'unit' => 'nullable|string|max:20',
'notes' => 'nullable|string',
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
$item->update($validator->validated());
// Recalcular preço
$sheet->recalculate();
$sheet->load(['items', 'businessSetting:id,name,currency,markup_factor']);
return response()->json(array_merge($sheet->toArray(), [
'cost_breakdown' => $sheet->cost_breakdown,
]));
}
/**
* Remove um item da ficha de serviço
*/
public function removeItem(Request $request, $sheetId, $itemId): JsonResponse
{
$sheet = ServiceSheet::ofUser($request->user()->id)->findOrFail($sheetId);
$item = ServiceSheetItem::where('service_sheet_id', $sheet->id)->findOrFail($itemId);
$item->delete();
// Recalcular preço
$sheet->recalculate();
$sheet->load(['items', 'businessSetting:id,name,currency,markup_factor']);
return response()->json(array_merge($sheet->toArray(), [
'cost_breakdown' => $sheet->cost_breakdown,
]));
}
/**
* Lista as categorias de serviços do usuário
*/
public function categories(Request $request): JsonResponse
{
$categories = ServiceSheet::ofUser($request->user()->id)
->whereNotNull('category')
->distinct()
->pluck('category');
return response()->json($categories);
}
/**
* Lista os tipos de componentes disponíveis
*/
public function itemTypes(): JsonResponse
{
return response()->json(ServiceSheetItem::TYPES);
}
/**
* Duplica uma ficha de serviço
*/
public function duplicate(Request $request, $id): JsonResponse
{
$sheet = ServiceSheet::ofUser($request->user()->id)
->with('items')
->findOrFail($id);
DB::beginTransaction();
try {
// Criar cópia da ficha
$newSheet = $sheet->replicate();
$newSheet->name = $sheet->name . ' (cópia)';
$newSheet->save();
// Copiar os itens
foreach ($sheet->items as $item) {
$newItem = $item->replicate();
$newItem->service_sheet_id = $newSheet->id;
$newItem->save();
}
// Recalcular
$newSheet->recalculate();
DB::commit();
$newSheet->load(['items', 'businessSetting:id,name,currency,markup_factor']);
return response()->json(array_merge($newSheet->toArray(), [
'cost_breakdown' => $newSheet->cost_breakdown,
]), 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['message' => 'Erro ao duplicar ficha de serviço: ' . $e->getMessage()], 500);
}
}
/**
* Simula o preço de um serviço (para calculadora)
*/
public function simulate(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'business_setting_id' => 'required|exists:business_settings,id',
'duration_minutes' => 'required|numeric|min:1',
'csv' => 'required|numeric|min:0',
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
$setting = BusinessSetting::ofUser($request->user()->id)->findOrFail($request->business_setting_id);
$price = $setting->calculateServicePrice(
(float) $request->duration_minutes,
(float) $request->csv
);
// Calcular breakdown
$fixedCostPortion = $setting->fixed_cost_per_minute * $request->duration_minutes;
$baseCost = $fixedCostPortion + $request->csv;
$markup = $setting->markup_factor ?? $setting->calculateMarkup();
$priceWithoutVat = $baseCost * $markup;
$vatAmount = 0;
if ($setting->price_includes_tax) {
$vatAmount = $price - $priceWithoutVat;
}
return response()->json([
'duration_minutes' => (float) $request->duration_minutes,
'duration_hours' => round($request->duration_minutes / 60, 2),
'csv' => (float) $request->csv,
'fixed_cost_per_hour' => $setting->fixed_cost_per_hour,
'fixed_cost_portion' => round($fixedCostPortion, 2),
'base_cost' => round($baseCost, 2),
'markup' => $markup,
'price_without_vat' => round($priceWithoutVat, 2),
'vat_rate' => $setting->price_includes_tax ? (float) $setting->vat_rate : 0,
'vat_amount' => round($vatAmount, 2),
'final_price' => $price,
'productive_hours' => $setting->productive_hours,
]);
}
}