<?php
namespace Plugin\NZEstimateSystem42\Controller;
use Eccube\Controller\AbstractController;
use Eccube\Entity\Product;
use Eccube\Entity\ProductClass;
use Eccube\Entity\ProductStock;
use Eccube\Entity\ProductImage;
use Eccube\Entity\Master\ProductStatus;
use Eccube\Entity\Master\SaleType;
use Eccube\Repository\ProductRepository;
use Eccube\Repository\Master\ProductStatusRepository;
use Eccube\Repository\Master\SaleTypeRepository;
use Eccube\Service\CartService;
use Eccube\Service\PurchaseFlow\PurchaseContext;
use Plugin\NZEstimateSystem42\Repository\PartsCategoryRepository;
use Plugin\NZEstimateSystem42\Repository\PartsRepository;
use Plugin\NZEstimateSystem42\Repository\ConfigRepository;
use Plugin\NZEstimateSystem42\Service\EstimateMailService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Doctrine\ORM\EntityManagerInterface;
/**
* 見積もりシミュレーター(フロント)
*/
class EstimateSimulatorController extends AbstractController
{
/**
* @var PartsCategoryRepository
*/
protected $partsCategoryRepository;
/**
* @var PartsRepository
*/
protected $partsRepository;
/**
* @var ConfigRepository
*/
protected $configRepository;
/**
* @var EstimateMailService
*/
protected $mailService;
/**
* @var EntityManagerInterface
*/
protected $entityManager;
/**
* @var ProductRepository
*/
protected $productRepository;
/**
* @var ProductStatusRepository
*/
protected $productStatusRepository;
/**
* @var SaleTypeRepository
*/
protected $saleTypeRepository;
/**
* @var CartService
*/
protected $cartService;
/**
* EstimateSimulatorController constructor.
*/
public function __construct(
PartsCategoryRepository $partsCategoryRepository,
PartsRepository $partsRepository,
ConfigRepository $configRepository,
EstimateMailService $mailService,
EntityManagerInterface $entityManager,
ProductRepository $productRepository,
ProductStatusRepository $productStatusRepository,
SaleTypeRepository $saleTypeRepository,
CartService $cartService
) {
$this->partsCategoryRepository = $partsCategoryRepository;
$this->partsRepository = $partsRepository;
$this->configRepository = $configRepository;
$this->mailService = $mailService;
$this->entityManager = $entityManager;
$this->productRepository = $productRepository;
$this->productStatusRepository = $productStatusRepository;
$this->saleTypeRepository = $saleTypeRepository;
$this->cartService = $cartService;
}
/**
* 見積もりシミュレーター
*
* @Route("/estimate/simulator", name="nz_estimate_simulator")
* @Template("@NZEstimateSystem42/default/simulator.twig")
*/
public function index(Request $request)
{
// 全カテゴリを取得
$categories = $this->partsCategoryRepository->findAllOrderBySortNo();
$config = $this->configRepository->get();
// 子カテゴリとして使用されているカテゴリIDを取得
$childCategoryIds = [];
$allParts = $this->partsRepository->findBy(['is_active' => true]);
foreach ($allParts as $parts) {
if ($parts->getChildCategoryId()) {
// カンマ区切りの場合は分割して配列に追加
$ids = explode(',', $parts->getChildCategoryId());
foreach ($ids as $id) {
$id = trim($id);
if (is_numeric($id)) {
$childCategoryIds[] = (int)$id;
}
}
}
}
$childCategoryIds = array_unique($childCategoryIds);
// 親カテゴリのみを表示(子カテゴリとして使用されていないカテゴリ)
$categoriesWithParts = [];
$allCategoriesWithParts = []; // 子カテゴリを含むすべてのカテゴリ
foreach ($categories as $category) {
$parts = $this->partsRepository->findByCategory($category, true);
if (!empty($parts)) {
// 子カテゴリを含むすべてのカテゴリデータ(JavaScript用)
$allCategoriesWithParts[] = [
'category' => $category,
'parts' => $parts,
];
// 子カテゴリとして使用されているカテゴリは初期表示しない
if (!in_array($category->getId(), $childCategoryIds)) {
$categoriesWithParts[] = [
'category' => $category,
'parts' => $parts,
];
}
}
}
// デバイスに応じてレイアウトを取得
$Layout = $this->getApplicableLayout($config, $request);
return [
'categoriesWithParts' => $categoriesWithParts,
'allCategoriesWithParts' => $allCategoriesWithParts, // JavaScript用に追加
'config' => $config,
'Layout' => $Layout,
];
}
/**
* デバイスに応じて適用するレイアウトを取得
*/
private function getApplicableLayout($config, Request $request)
{
// モバイルデバイスの判定
$userAgent = $request->headers->get('User-Agent');
$isMobile = $this->detectMobileDevice($userAgent);
// デバイスに応じてレイアウトを取得
if ($isMobile && $config->getLayoutMobile()) {
return $config->getLayoutMobile();
}
if ($config->getLayout()) {
return $config->getLayout();
}
// デフォルトレイアウトを返す
return null;
}
/**
* User-Agentからモバイルデバイスかどうかを判定
*/
private function detectMobileDevice($userAgent)
{
if (empty($userAgent)) {
return false;
}
// モバイルデバイスのパターン
$mobilePatterns = [
'iPhone',
'iPod',
'Android.*Mobile',
'Windows Phone',
'BlackBerry',
'webOS',
'Mobile',
'IEMobile',
];
$pattern = '/' . implode('|', $mobilePatterns) . '/i';
return preg_match($pattern, $userAgent) === 1;
}
/**
* カテゴリ別パーツ一覧取得(Ajax)
*
* @Route("/estimate/simulator/parts/{category_id}", name="nz_estimate_simulator_parts", methods={"GET"})
*/
public function getPartsByCategory(Request $request, $category_id)
{
if (!$request->isXmlHttpRequest()) {
throw $this->createNotFoundException();
}
$category = $this->partsCategoryRepository->find($category_id);
if (!$category) {
return $this->json(['error' => 'カテゴリが見つかりません。'], 404);
}
$parts = $this->partsRepository->findByCategory($category, true); // 有効なパーツのみ
$partsData = [];
foreach ($parts as $p) {
$partsData[] = [
'id' => $p->getId(),
'model_number' => $p->getModelNumber(),
'name' => $p->getName(),
'specifications' => $p->getSpecifications(),
'price' => $p->getPrice(),
'stock_status' => $p->getStockStatus(),
];
}
return $this->json([
'success' => true,
'parts' => $partsData,
]);
}
/**
* 見積もり確認・メール送信
*
* @Route("/estimate/simulator/send", name="nz_estimate_simulator_send", methods={"POST"})
*/
public function send(Request $request)
{
if (!$request->isXmlHttpRequest()) {
throw $this->createNotFoundException();
}
$this->isTokenValid();
$name = $request->request->get('name');
$email = $request->request->get('email');
$tel = $request->request->get('tel');
$company = $request->request->get('company');
$message = $request->request->get('message');
$items = $request->request->get('items', []);
// デバッグログ
log_info('NZEstimateSystem42: 見積もり送信開始', [
'name' => $name,
'email' => $email,
'items_raw' => $items,
]);
// バリデーション
if (empty($name) || empty($email) || empty($items)) {
log_error('NZEstimateSystem42: バリデーションエラー(必須項目)', [
'name' => $name,
'email' => $email,
'items_count' => is_array($items) ? count($items) : 0,
]);
return $this->json([
'success' => false,
'message' => '必須項目を入力してください。',
], 400);
}
// メールアドレスバリデーション
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
log_error('NZEstimateSystem42: メールアドレス形式エラー', ['email' => $email]);
return $this->json([
'success' => false,
'message' => '正しいメールアドレスを入力してください。',
], 400);
}
// itemsがJSON文字列の場合はデコード
if (is_string($items)) {
$items = json_decode($items, true);
if (json_last_error() !== JSON_ERROR_NONE) {
log_error('NZEstimateSystem42: JSON デコードエラー', [
'error' => json_last_error_msg(),
'items' => $items,
]);
return $this->json([
'success' => false,
'message' => 'データ形式が正しくありません。',
], 400);
}
}
// パーツ情報を取得して見積もり内容を作成
$estimateItems = [];
$totalPrice = 0;
foreach ($items as $item) {
$partsId = isset($item['parts_id']) ? $item['parts_id'] : null;
if (!$partsId) {
log_warning('NZEstimateSystem42: parts_id が空', ['item' => $item]);
continue;
}
$parts = $this->partsRepository->find($partsId);
if (!$parts || !$parts->getIsActive()) {
log_warning('NZEstimateSystem42: パーツが見つからないか無効', ['parts_id' => $partsId]);
continue;
}
// quantityは常に1とする(シミュレーターは数量選択なし)
$quantity = 1;
$subtotal = $parts->getPrice() * $quantity;
$estimateItems[] = [
'model_number' => $parts->getModelNumber(),
'name' => $parts->getProductName(), // getName() → getProductName()
'price' => number_format($parts->getPrice()),
'quantity' => $quantity,
'subtotal' => number_format($subtotal),
];
$totalPrice += $subtotal;
}
if (empty($estimateItems)) {
log_error('NZEstimateSystem42: 有効なパーツが1つもない');
return $this->json([
'success' => false,
'message' => '選択されたパーツが無効です。',
], 400);
}
log_info('NZEstimateSystem42: 見積もり内容作成完了', [
'items_count' => count($estimateItems),
'total_price' => $totalPrice,
]);
// 設定から料金調整を取得
$config = $this->configRepository->get();
$displayPrice = $totalPrice; // 顧客に表示する価格(パーツ合計)
$finalPrice = $config->calculateFinalPrice($totalPrice); // 内部的な最終金額
log_info('NZEstimateSystem42: 料金調整適用', [
'display_price' => $displayPrice,
'additional_charge' => $config->getAdditionalCharge(),
'discount_rate' => $config->getDiscountRate(),
'final_price' => $finalPrice,
]);
// メール送信
try {
$mailSent = $this->mailService->sendEstimateRequest(
$name,
$email,
$tel,
$company,
$message,
$estimateItems,
$displayPrice, // 顧客向けメールには表示価格
$finalPrice, // 管理者向けメールには最終金額
$config // 設定情報を渡す
);
if (!$mailSent) {
log_error('NZEstimateSystem42: メール送信失敗(戻り値がfalse)');
return $this->json([
'success' => false,
'message' => 'メール送信に失敗しました。しばらくしてから再度お試しください。',
], 500);
}
log_info('NZEstimateSystem42: メール送信成功');
return $this->json([
'success' => true,
'message' => 'お見積もり依頼を受け付けました。担当者より折り返しご連絡いたします。',
]);
} catch (\Exception $e) {
log_error('NZEstimateSystem42: メール送信で例外発生', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return $this->json([
'success' => false,
'message' => 'メール送信中にエラーが発生しました: ' . $e->getMessage(),
], 500);
}
}
/**
* カートに追加
*
* @Route("/estimate_simulator/add_to_cart", name="nz_estimate_add_to_cart", methods={"POST"})
*/
public function addToCart(Request $request)
{
// デバッグ:メソッドが呼ばれたことを確認
error_log('=== NZEstimateSystem42: addToCart method called ===');
log_info('NZEstimateSystem42: addToCart method called');
if (!$request->isXmlHttpRequest()) {
error_log('NZEstimateSystem42: Not AJAX request');
return $this->json(['success' => false, 'message' => '不正なリクエストです'], 400);
}
$data = json_decode($request->getContent(), true);
error_log('NZEstimateSystem42: Request data: ' . json_encode($data));
$selectedParts = $data['selected_parts'] ?? [];
$totalPrice = (int)($data['total_price'] ?? 0);
if (empty($selectedParts) || $totalPrice <= 0) {
return $this->json(['success' => false, 'message' => 'パーツが選択されていません'], 400);
}
try {
log_info('NZEstimateSystem42: カート追加処理開始', [
'total_price' => $totalPrice,
'parts_count' => count($selectedParts)
]);
// 商品名: オーダーメイドPC10/29 08:15
$now = new \DateTime();
$productName = sprintf('オーダーメイドPC%s', $now->format('n/j H:i'));
// 14日後の有効期限を計算
$expirationDate = (new \DateTime())->modify('+14 days');
$expirationDateStr = $expirationDate->format('n/j');
// 商品説明(有効期限のみ、赤文字・中サイズ)
$description = sprintf(
'<p style="color: #e53e3e; font-size: 18px; font-weight: 600; margin: 15px 0;">この見積り商品ページは%sまで有効</p>',
$expirationDateStr
);
// フリーエリア用のHTMLを自動生成
$freeAreaHtml = "<h3>選択されたパーツ構成</h3>\n";
$freeAreaHtml .= "<div class=\"ec-descriptionRole__spec\">\n";
foreach ($selectedParts as $part) {
$freeAreaHtml .= " <dl>\n";
$freeAreaHtml .= " <dt>" . htmlspecialchars($part['category_name']) . "</dt>\n";
$freeAreaHtml .= " <dd>" . htmlspecialchars($part['name']) . "</dd>\n";
$freeAreaHtml .= " </dl>\n";
}
$freeAreaHtml .= "</div>\n";
$freeAreaHtml .= "<div style=\"margin-top: 20px; padding: 15px; background: #f7fafc; border-radius: 8px;\">\n";
$freeAreaHtml .= " <p style=\"margin: 0; font-size: 14px; color: #718096;\">※ パーツのメーカー等も指定したい場合はこの商品ページの上部「この商品を問い合わせる」ボタンより各種質問をお送りください。</p>\n";
$freeAreaHtml .= "</div>\n";
// 販売中ステータスを取得
$ProductStatus = $this->productStatusRepository->find(ProductStatus::DISPLAY_SHOW);
if (!$ProductStatus) {
throw new \Exception('商品ステータスが取得できませんでした');
}
// 販売種別を取得(販売種別クレカ対応 = 3)
$SaleType = $this->saleTypeRepository->find(3);
if (!$SaleType) {
throw new \Exception('販売種別が取得できませんでした');
}
// 商品を作成
$Product = new Product();
$Product->setName($productName);
$Product->setStatus($ProductStatus);
$Product->setDescriptionDetail($description);
$Product->setDescriptionList(''); // 一覧説明文
$Product->setSearchWord(''); // 検索ワード
$Product->setFreeArea($freeAreaHtml); // フリーエリアに見積もり内容を自動設定
$Product->setCreateDate(new \DateTime());
$Product->setUpdateDate(new \DateTime());
// ProductClassを作成
$ProductClass = new ProductClass();
$ProductClass->setProduct($Product);
$ProductClass->setSaleType($SaleType);
$ProductClass->setPrice01(null); // 通常価格は空白
$ProductClass->setPrice02($totalPrice); // 販売価格のみ
$ProductClass->setDeliveryFee(0); // 送料0円
$ProductClass->setStockUnlimited(true); // 在庫無制限
$ProductClass->setVisible(true);
$ProductClass->setCreateDate(new \DateTime());
$ProductClass->setUpdateDate(new \DateTime());
log_info('NZEstimateSystem42: ProductClass作成', [
'price01' => null,
'price02' => $totalPrice,
'delivery_fee' => 0,
'stock_unlimited' => true
]);
// ProductStockを作成
$ProductStock = new ProductStock();
$ProductStock->setCreateDate(new \DateTime());
$ProductStock->setUpdateDate(new \DateTime());
// 双方向の関連付け
$ProductStock->setProductClass($ProductClass);
$ProductClass->setProductStock($ProductStock);
$Product->addProductClass($ProductClass);
// データベースに保存
$this->entityManager->persist($Product);
$this->entityManager->persist($ProductClass);
$this->entityManager->persist($ProductStock);
$this->entityManager->flush();
// デフォルト画像を設定
$defaultImageFileName = '1031020313_69039a51941e2.jpg';
$saveImagePath = __DIR__ . '/../../../../html/upload/save_image/' . $defaultImageFileName;
if (file_exists($saveImagePath)) {
$ProductImage = new ProductImage();
$ProductImage->setProduct($Product);
$ProductImage->setFileName($defaultImageFileName);
$ProductImage->setSortNo(1);
$ProductImage->setCreateDate(new \DateTime());
$Product->addProductImage($ProductImage);
$this->entityManager->persist($ProductImage);
$this->entityManager->flush();
}
log_info('NZEstimateSystem42: データベース保存完了', [
'product_id' => $Product->getId(),
'product_class_id' => $ProductClass->getId()
]);
// 商品詳細ページのURLを生成
$productUrl = $this->generateUrl('product_detail', ['id' => $Product->getId()]);
log_info('NZEstimateSystem42: 商品作成成功、商品詳細ページへリダイレクト', [
'product_id' => $Product->getId(),
'product_url' => $productUrl
]);
return $this->json([
'success' => true,
'message' => '商品を作成しました',
'product_url' => $productUrl,
]);
} catch (\Exception $e) {
log_error('NZEstimateSystem42: カート追加で例外発生', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return $this->json([
'success' => false,
'message' => 'カート追加中にエラーが発生しました: ' . $e->getMessage(),
], 500);
}
}
}