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, ]); } }