Warranty module added (draft only)
This commit is contained in:
parent
21fb6b7c09
commit
aae6a74b0c
182
hesabixCore/src/Cog/HookService.php
Normal file
182
hesabixCore/src/Cog/HookService.php
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\Hook;
|
||||||
|
use App\Entity\Business;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
|
||||||
|
class HookService
|
||||||
|
{
|
||||||
|
private $entityManager;
|
||||||
|
private $httpClient;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
HttpClientInterface $httpClient
|
||||||
|
) {
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
$this->httpClient = $httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* دریافت تمام هوکهای یک کسب و کار
|
||||||
|
*/
|
||||||
|
public function getHooksByBusiness(Business $business): array
|
||||||
|
{
|
||||||
|
return $this->entityManager->getRepository(Hook::class)->findBy([
|
||||||
|
'bid' => $business
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ارسال داده به تمام هوکهای یک کسب و کار
|
||||||
|
*/
|
||||||
|
public function sendToHooks(Business $business, array $data, string $event = 'general'): array
|
||||||
|
{
|
||||||
|
$hooks = $this->getHooksByBusiness($business);
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($hooks as $hook) {
|
||||||
|
$result = $this->sendToHook($hook, $data, $event);
|
||||||
|
$results[] = [
|
||||||
|
'hook_id' => $hook->getId(),
|
||||||
|
'url' => $hook->getUrl(),
|
||||||
|
'success' => $result['success'],
|
||||||
|
'response' => $result['response'],
|
||||||
|
'error' => $result['error'] ?? null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ارسال داده به یک هوک خاص
|
||||||
|
*/
|
||||||
|
public function sendToHook(Hook $hook, array $data, string $event = 'general'): array
|
||||||
|
{
|
||||||
|
$url = $hook->getUrl();
|
||||||
|
$password = $hook->getPassword();
|
||||||
|
|
||||||
|
// آمادهسازی دادههای ارسالی
|
||||||
|
$payload = [
|
||||||
|
'event' => $event,
|
||||||
|
'timestamp' => time(),
|
||||||
|
'data' => $data,
|
||||||
|
'password' => $password
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->httpClient->request('POST', $url, [
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'Hesabix-Hook-Service/1.0'
|
||||||
|
],
|
||||||
|
'json' => $payload,
|
||||||
|
'timeout' => 10,
|
||||||
|
'max_redirects' => 3
|
||||||
|
]);
|
||||||
|
|
||||||
|
$statusCode = $response->getStatusCode();
|
||||||
|
$content = $response->getContent(false);
|
||||||
|
|
||||||
|
if ($statusCode >= 200 && $statusCode < 300) {
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'response' => json_decode($content, true) ?: $content,
|
||||||
|
'status_code' => $statusCode
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => "HTTP Error: {$statusCode}",
|
||||||
|
'response' => $content,
|
||||||
|
'status_code' => $statusCode
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'status_code' => 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ارسال اعلان تغییر شخص
|
||||||
|
*/
|
||||||
|
public function sendPersonChange(Business $business, array $personData, string $action = 'update'): array
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'action' => $action,
|
||||||
|
'person' => $personData,
|
||||||
|
'business_id' => $business->getId(),
|
||||||
|
'business_name' => $business->getName()
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->sendToHooks($business, $data, 'person_change');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ارسال اعلان تغییر کالا
|
||||||
|
*/
|
||||||
|
public function sendCommodityChange(Business $business, array $commodityData, string $action = 'update'): array
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'action' => $action,
|
||||||
|
'commodity' => $commodityData,
|
||||||
|
'business_id' => $business->getId(),
|
||||||
|
'business_name' => $business->getName()
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->sendToHooks($business, $data, 'commodity_change');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ارسال اعلان تغییر فاکتور
|
||||||
|
*/
|
||||||
|
public function sendInvoiceChange(Business $business, array $invoiceData, string $action = 'update'): array
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'action' => $action,
|
||||||
|
'invoice' => $invoiceData,
|
||||||
|
'business_id' => $business->getId(),
|
||||||
|
'business_name' => $business->getName()
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->sendToHooks($business, $data, 'invoice_change');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ارسال اعلان مالیاتی
|
||||||
|
*/
|
||||||
|
public function sendTaxNotification(Business $business, array $taxData, string $action = 'send'): array
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'action' => $action,
|
||||||
|
'tax_invoice' => $taxData,
|
||||||
|
'business_id' => $business->getId(),
|
||||||
|
'business_name' => $business->getName()
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->sendToHooks($business, $data, 'tax_notification');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تست اتصال به هوک
|
||||||
|
*/
|
||||||
|
public function testHook(Hook $hook): array
|
||||||
|
{
|
||||||
|
$testData = [
|
||||||
|
'test' => true,
|
||||||
|
'message' => 'تست اتصال هوک',
|
||||||
|
'timestamp' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->sendToHook($hook, $testData, 'test');
|
||||||
|
}
|
||||||
|
}
|
|
@ -545,6 +545,7 @@ class BusinessController extends AbstractController
|
||||||
'plugHrmDocs' => true,
|
'plugHrmDocs' => true,
|
||||||
'plugGhestaManager' => true,
|
'plugGhestaManager' => true,
|
||||||
'plugTaxSettings' => true,
|
'plugTaxSettings' => true,
|
||||||
|
'plugWarranty' => true,
|
||||||
'inquiry' => true,
|
'inquiry' => true,
|
||||||
'ai' => true,
|
'ai' => true,
|
||||||
];
|
];
|
||||||
|
@ -591,6 +592,7 @@ class BusinessController extends AbstractController
|
||||||
'plugHrmDocs' => $perm->isPlugHrmDocs(),
|
'plugHrmDocs' => $perm->isPlugHrmDocs(),
|
||||||
'plugGhestaManager' => $perm->isPlugGhestaManager(),
|
'plugGhestaManager' => $perm->isPlugGhestaManager(),
|
||||||
'plugTaxSettings' => $perm->isPlugTaxSettings(),
|
'plugTaxSettings' => $perm->isPlugTaxSettings(),
|
||||||
|
'plugWarranty' => $perm->isPlugWarrantyManager(),
|
||||||
'inquiry' => $perm->isInquiry(),
|
'inquiry' => $perm->isInquiry(),
|
||||||
'ai' => $perm->isAi(),
|
'ai' => $perm->isAi(),
|
||||||
];
|
];
|
||||||
|
@ -662,6 +664,7 @@ class BusinessController extends AbstractController
|
||||||
$perm->setPlugRepservice($params['plugRepservice']);
|
$perm->setPlugRepservice($params['plugRepservice']);
|
||||||
$perm->setPlugHrmDocs($params['plugHrmDocs']);
|
$perm->setPlugHrmDocs($params['plugHrmDocs']);
|
||||||
$perm->setPlugGhestaManager($params['plugGhestaManager']);
|
$perm->setPlugGhestaManager($params['plugGhestaManager']);
|
||||||
|
$perm->setPlugWarrantyManager($params['plugWarranty'] ?? false);
|
||||||
$perm->setPlugTaxSettings($params['plugTaxSettings']);
|
$perm->setPlugTaxSettings($params['plugTaxSettings']);
|
||||||
$perm->setInquiry($params['inquiry']);
|
$perm->setInquiry($params['inquiry']);
|
||||||
$perm->setAi($params['ai']);
|
$perm->setAi($params['ai']);
|
||||||
|
|
|
@ -182,7 +182,23 @@ class CommodityController extends AbstractController
|
||||||
]);
|
]);
|
||||||
$res = [];
|
$res = [];
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
$res[] = Explore::ExploreCommodity($item);
|
$temp = Explore::ExploreCommodity($item);
|
||||||
|
if (!$item->isKhadamat()) {
|
||||||
|
$rows = $entityManager->getRepository('App\Entity\HesabdariRow')->findBy([
|
||||||
|
'bid' => $acc['bid'],
|
||||||
|
'commodity' => $item
|
||||||
|
]);
|
||||||
|
$count = 0;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if ($row->getDoc()->getType() === 'buy' || $row->getDoc()->getType() === 'open_balance') {
|
||||||
|
$count += $row->getCommdityCount();
|
||||||
|
} else {
|
||||||
|
$count -= $row->getCommdityCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$temp['count'] = $count;
|
||||||
|
}
|
||||||
|
$res[] = $temp;
|
||||||
}
|
}
|
||||||
return $this->json($extractor->operationSuccess([
|
return $this->json($extractor->operationSuccess([
|
||||||
'List' => $res,
|
'List' => $res,
|
||||||
|
|
|
@ -470,6 +470,15 @@ class PluginController extends AbstractController
|
||||||
'icon' => ' taxplugin.jpg',
|
'icon' => ' taxplugin.jpg',
|
||||||
'defaultOn' => null,
|
'defaultOn' => null,
|
||||||
],
|
],
|
||||||
|
// [
|
||||||
|
// 'name' => 'مدیریت گارانتی',
|
||||||
|
// 'code' => 'warranty',
|
||||||
|
// 'timestamp' => '32104000',
|
||||||
|
// 'timelabel' => 'یک سال',
|
||||||
|
// 'price' => '200000',
|
||||||
|
// 'icon' => 'warranty.png',
|
||||||
|
// 'defaultOn' => null,
|
||||||
|
// ],
|
||||||
];
|
];
|
||||||
|
|
||||||
$repo = $entityManager->getRepository(PluginProdect::class);
|
$repo = $entityManager->getRepository(PluginProdect::class);
|
||||||
|
|
556
hesabixCore/src/Controller/Plugins/PlugWarrantyController.php
Normal file
556
hesabixCore/src/Controller/Plugins/PlugWarrantyController.php
Normal file
|
@ -0,0 +1,556 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Plugins;
|
||||||
|
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use App\Entity\PlugWarrantySerial;
|
||||||
|
use App\Entity\Commodity;
|
||||||
|
use App\Service\Access;
|
||||||
|
use App\Service\Log;
|
||||||
|
use App\Service\Provider;
|
||||||
|
use App\Service\Jdate;
|
||||||
|
|
||||||
|
class PlugWarrantyController extends AbstractController
|
||||||
|
{
|
||||||
|
private $entityManager;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateExpiredSerials(EntityManagerInterface $entityManager, $businessId): void
|
||||||
|
{
|
||||||
|
$repository = $entityManager->getRepository(PlugWarrantySerial::class);
|
||||||
|
$jdate = new Jdate();
|
||||||
|
$today = $jdate->GetTodayDate();
|
||||||
|
|
||||||
|
$expiredSerials = $repository->createQueryBuilder('s')
|
||||||
|
->where('s.bid = :businessId')
|
||||||
|
->andWhere('s.status = :status')
|
||||||
|
->andWhere('s.warrantyEndDate IS NOT NULL')
|
||||||
|
->andWhere('s.warrantyEndDate < :today')
|
||||||
|
->setParameter('businessId', $businessId)
|
||||||
|
->setParameter('status', 'active')
|
||||||
|
->setParameter('today', $today)
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
|
||||||
|
foreach ($expiredSerials as $serial) {
|
||||||
|
$serial->setStatus('expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($expiredSerials)) {
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkAndUpdateSerialStatus($serial, EntityManagerInterface $entityManager): void
|
||||||
|
{
|
||||||
|
$jdate = new Jdate();
|
||||||
|
$today = $jdate->GetTodayDate();
|
||||||
|
$warrantyEndDate = $serial->getWarrantyEndDate();
|
||||||
|
|
||||||
|
if ($serial->getStatus() === 'active' &&
|
||||||
|
$warrantyEndDate &&
|
||||||
|
$warrantyEndDate < $today) {
|
||||||
|
$serial->setStatus('expired');
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/plugins/warranty/serials', name: 'plugin_warranty_serials', methods: ['GET'])]
|
||||||
|
public function plugin_warranty_serials(EntityManagerInterface $entityManager, Access $access, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$acc = $access->hasRole('plugWarrantyManager');
|
||||||
|
if(!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
|
||||||
|
$this->updateExpiredSerials($entityManager, $acc['bid']);
|
||||||
|
|
||||||
|
$page = $request->query->get('page', 1);
|
||||||
|
$limit = $request->query->get('limit', 20);
|
||||||
|
$status = $request->query->get('status');
|
||||||
|
$commodityId = $request->query->get('commodity_id');
|
||||||
|
$search = $request->query->get('search');
|
||||||
|
|
||||||
|
$repository = $entityManager->getRepository(PlugWarrantySerial::class);
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$serials = $repository->searchSerials($acc['bid'], $search);
|
||||||
|
} elseif ($status) {
|
||||||
|
$serials = $repository->findByStatus($acc['bid'], $status);
|
||||||
|
} elseif ($commodityId) {
|
||||||
|
$serials = $repository->findByCommodity($acc['bid'], $commodityId);
|
||||||
|
} else {
|
||||||
|
$serials = $repository->findByBusiness($acc['bid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
foreach($serials as $serial){
|
||||||
|
$commodity = $serial->getCommodity();
|
||||||
|
$submitter = $serial->getSubmitter();
|
||||||
|
|
||||||
|
$data[] = [
|
||||||
|
'id' => $serial->getId(),
|
||||||
|
'serialNumber' => $serial->getSerialNumber(),
|
||||||
|
'commodity' => $commodity ? [
|
||||||
|
'id' => $commodity->getId(),
|
||||||
|
'name' => $commodity->getName(),
|
||||||
|
'code' => $commodity->getCode()
|
||||||
|
] : null,
|
||||||
|
'dateSubmit' => $serial->getDateSubmit(),
|
||||||
|
'description' => $serial->getDescription(),
|
||||||
|
'warrantyStartDate' => $serial->getWarrantyStartDate(),
|
||||||
|
'warrantyEndDate' => $serial->getWarrantyEndDate(),
|
||||||
|
'status' => $serial->getStatus(),
|
||||||
|
'notes' => $serial->getNotes(),
|
||||||
|
'submitter' => $submitter ? [
|
||||||
|
'id' => $submitter->getId(),
|
||||||
|
'name' => $submitter->getFullName()
|
||||||
|
] : null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json($data);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/plugins/warranty/serials/{id}', name: 'plugin_warranty_serial', methods: ['GET'])]
|
||||||
|
public function plugin_warranty_serial(EntityManagerInterface $entityManager, Access $access, $id): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$acc = $access->hasRole('plugWarrantyManager');
|
||||||
|
if(!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
|
||||||
|
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
||||||
|
'id' => $id,
|
||||||
|
'bid' => $acc['bid']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if(!$serial)
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
|
||||||
|
$this->checkAndUpdateSerialStatus($serial, $entityManager);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'id' => $serial->getId(),
|
||||||
|
'serialNumber' => $serial->getSerialNumber(),
|
||||||
|
'commodity' => [
|
||||||
|
'id' => $serial->getCommodity()->getId(),
|
||||||
|
'name' => $serial->getCommodity()->getName(),
|
||||||
|
'code' => $serial->getCommodity()->getCode()
|
||||||
|
],
|
||||||
|
'dateSubmit' => $serial->getDateSubmit(),
|
||||||
|
'description' => $serial->getDescription(),
|
||||||
|
'warrantyStartDate' => $serial->getWarrantyStartDate(),
|
||||||
|
'warrantyEndDate' => $serial->getWarrantyEndDate(),
|
||||||
|
'status' => $serial->getStatus(),
|
||||||
|
'notes' => $serial->getNotes(),
|
||||||
|
'submitter' => [
|
||||||
|
'id' => $serial->getSubmitter()->getId(),
|
||||||
|
'name' => $serial->getSubmitter()->getFullName()
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->json($data);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/plugins/warranty/serials/add', name: 'plugin_warranty_serial_add', methods: ['POST'])]
|
||||||
|
public function plugin_warranty_serial_add(Request $request, EntityManagerInterface $entityManager, Access $access, Log $log): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$acc = $access->hasRole('plugWarrantyManager');
|
||||||
|
if(!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
if ($content = $request->getContent()) {
|
||||||
|
$params = json_decode($content, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!array_key_exists('serialNumber', $params) || !array_key_exists('commodity_id', $params))
|
||||||
|
throw $this->createAccessDeniedException('پارامترهای ناقص');
|
||||||
|
|
||||||
|
$repository = $entityManager->getRepository(PlugWarrantySerial::class);
|
||||||
|
|
||||||
|
if($repository->isSerialNumberExists($params['serialNumber'], $acc['bid'])) {
|
||||||
|
return $this->json(['error' => 'شماره سریال تکراری است'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$commodity = $entityManager->getRepository(Commodity::class)->findOneBy([
|
||||||
|
'id' => $params['commodity_id'],
|
||||||
|
'bid' => $acc['bid']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if(!$commodity) {
|
||||||
|
return $this->json(['error' => 'محصول یافت نشد'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$serial = new PlugWarrantySerial();
|
||||||
|
$serial->setSerialNumber($params['serialNumber']);
|
||||||
|
$serial->setCommodity($commodity);
|
||||||
|
$serial->setBid($acc['bid']);
|
||||||
|
$serial->setSubmitter($this->getUser());
|
||||||
|
$serial->setDescription($params['description'] ?? null);
|
||||||
|
$serial->setWarrantyStartDate($params['warrantyStartDate'] ?? null);
|
||||||
|
$serial->setWarrantyEndDate($params['warrantyEndDate'] ?? null);
|
||||||
|
$serial->setStatus($params['status'] ?? 'active');
|
||||||
|
$serial->setNotes($params['notes'] ?? null);
|
||||||
|
|
||||||
|
$entityManager->persist($serial);
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
|
$log->insert(
|
||||||
|
'گارانتی',
|
||||||
|
'افزودن سریال جدید: ' . $params['serialNumber'],
|
||||||
|
$this->getUser(),
|
||||||
|
$acc['bid']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'سریال با موفقیت افزوده شد',
|
||||||
|
'id' => $serial->getId()
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/plugins/warranty/serials/edit/{id}', name: 'plugin_warranty_serial_edit', methods: ['POST'])]
|
||||||
|
public function plugin_warranty_serial_edit(Request $request, EntityManagerInterface $entityManager, Access $access, $id, Log $log): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$acc = $access->hasRole('plugWarrantyManager');
|
||||||
|
if(!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
|
||||||
|
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
||||||
|
'id' => $id,
|
||||||
|
'bid' => $acc['bid']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if(!$serial)
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
if ($content = $request->getContent()) {
|
||||||
|
$params = json_decode($content, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists('serialNumber', $params)) {
|
||||||
|
$repository = $entityManager->getRepository(PlugWarrantySerial::class);
|
||||||
|
$existingSerial = $repository->createQueryBuilder('p')
|
||||||
|
->andWhere('p.serialNumber = :serialNumber')
|
||||||
|
->andWhere('p.bid = :bid')
|
||||||
|
->andWhere('p.id != :id')
|
||||||
|
->setParameter('serialNumber', $params['serialNumber'])
|
||||||
|
->setParameter('bid', $acc['bid'])
|
||||||
|
->setParameter('id', $id)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
|
||||||
|
if($existingSerial) {
|
||||||
|
return $this->json(['error' => 'شماره سریال تکراری است'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$serial->setSerialNumber($params['serialNumber']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists('commodity_id', $params)) {
|
||||||
|
$commodity = $entityManager->getRepository(Commodity::class)->findOneBy([
|
||||||
|
'id' => $params['commodity_id'],
|
||||||
|
'bid' => $acc['bid']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if(!$commodity) {
|
||||||
|
return $this->json(['error' => 'محصول یافت نشد'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$serial->setCommodity($commodity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists('description', $params)) {
|
||||||
|
$serial->setDescription($params['description']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists('warrantyStartDate', $params)) {
|
||||||
|
$serial->setWarrantyStartDate($params['warrantyStartDate']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists('warrantyEndDate', $params)) {
|
||||||
|
$serial->setWarrantyEndDate($params['warrantyEndDate']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists('status', $params)) {
|
||||||
|
$serial->setStatus($params['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists('notes', $params)) {
|
||||||
|
$serial->setNotes($params['notes']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
|
$log->insert(
|
||||||
|
'گارانتی',
|
||||||
|
'ویرایش سریال: ' . $serial->getSerialNumber(),
|
||||||
|
$this->getUser(),
|
||||||
|
$acc['bid']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'سریال با موفقیت ویرایش شد'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/plugins/warranty/serials/{id}', name: 'plugin_warranty_serial_delete', methods: ['DELETE'])]
|
||||||
|
public function plugin_warranty_serial_delete(EntityManagerInterface $entityManager, Access $access, $id, Log $log): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$acc = $access->hasRole('plugWarrantyManager');
|
||||||
|
if(!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
|
||||||
|
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
||||||
|
'id' => $id,
|
||||||
|
'bid' => $acc['bid']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if(!$serial)
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
|
||||||
|
$serialNumber = $serial->getSerialNumber();
|
||||||
|
$entityManager->remove($serial);
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
|
$log->insert(
|
||||||
|
'گارانتی',
|
||||||
|
'حذف سریال: ' . $serialNumber,
|
||||||
|
$this->getUser(),
|
||||||
|
$acc['bid']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'سریال با موفقیت حذف شد'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/plugins/warranty/serials/bulk-import', name: 'plugin_warranty_serial_bulk_import', methods: ['POST'])]
|
||||||
|
public function plugin_warranty_serial_bulk_import(Request $request, EntityManagerInterface $entityManager, Access $access, Log $log): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$acc = $access->hasRole('plugWarrantyManager');
|
||||||
|
if(!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
if ($content = $request->getContent()) {
|
||||||
|
$params = json_decode($content, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!array_key_exists('serials', $params) || !is_array($params['serials']))
|
||||||
|
throw $this->createAccessDeniedException('دادههای نامعتبر');
|
||||||
|
|
||||||
|
$repository = $entityManager->getRepository(PlugWarrantySerial::class);
|
||||||
|
$commodityRepo = $entityManager->getRepository(Commodity::class);
|
||||||
|
|
||||||
|
$successCount = 0;
|
||||||
|
$errorCount = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach($params['serials'] as $index => $serialData) {
|
||||||
|
try {
|
||||||
|
if(!array_key_exists('serialNumber', $serialData) || !array_key_exists('commodity_id', $serialData)) {
|
||||||
|
$errors[] = "ردیف " . ($index + 1) . ": پارامترهای ناقص";
|
||||||
|
$errorCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($repository->isSerialNumberExists($serialData['serialNumber'], $acc['bid'])) {
|
||||||
|
$errors[] = "ردیف " . ($index + 1) . ": شماره سریال تکراری است";
|
||||||
|
$errorCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$commodity = $commodityRepo->findOneBy([
|
||||||
|
'id' => $serialData['commodity_id'],
|
||||||
|
'bid' => $acc['bid']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if(!$commodity) {
|
||||||
|
$errors[] = "ردیف " . ($index + 1) . ": محصول یافت نشد";
|
||||||
|
$errorCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$serial = new PlugWarrantySerial();
|
||||||
|
$serial->setSerialNumber($serialData['serialNumber']);
|
||||||
|
$serial->setCommodity($commodity);
|
||||||
|
$serial->setBid($acc['bid']);
|
||||||
|
$serial->setSubmitter($this->getUser());
|
||||||
|
$serial->setDescription($serialData['description'] ?? null);
|
||||||
|
$serial->setWarrantyStartDate($serialData['warrantyStartDate'] ?? null);
|
||||||
|
$serial->setWarrantyEndDate($serialData['warrantyEndDate'] ?? null);
|
||||||
|
$serial->setStatus($serialData['status'] ?? 'active');
|
||||||
|
$serial->setNotes($serialData['notes'] ?? null);
|
||||||
|
|
||||||
|
$entityManager->persist($serial);
|
||||||
|
$successCount++;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = "ردیف " . ($index + 1) . ": " . $e->getMessage();
|
||||||
|
$errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
|
$log->insert(
|
||||||
|
'گارانتی',
|
||||||
|
'وارد کردن انبوه سریالهای گارانتی: ' . $successCount . ' موفق، ' . $errorCount . ' ناموفق',
|
||||||
|
$this->getUser(),
|
||||||
|
$acc['bid']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'عملیات وارد کردن انبوه تکمیل شد',
|
||||||
|
'successCount' => $successCount,
|
||||||
|
'errorCount' => $errorCount,
|
||||||
|
'errors' => $errors
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/plugins/warranty/stats', name: 'plugin_warranty_stats', methods: ['GET'])]
|
||||||
|
public function plugin_warranty_stats(EntityManagerInterface $entityManager, Access $access): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$acc = $access->hasRole('plugWarrantyManager');
|
||||||
|
if(!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
|
||||||
|
$this->updateExpiredSerials($entityManager, $acc['bid']);
|
||||||
|
|
||||||
|
$repository = $entityManager->getRepository(PlugWarrantySerial::class);
|
||||||
|
|
||||||
|
$allSerials = $repository->createQueryBuilder('p')
|
||||||
|
->andWhere('p.bid = :bid')
|
||||||
|
->setParameter('bid', $acc['bid'])
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
|
||||||
|
$totalSerials = count($allSerials);
|
||||||
|
$activeSerials = 0;
|
||||||
|
$inactiveSerials = 0;
|
||||||
|
$expiredSerials = 0;
|
||||||
|
|
||||||
|
foreach ($allSerials as $serial) {
|
||||||
|
$status = $serial->getStatus();
|
||||||
|
switch ($status) {
|
||||||
|
case 'active':
|
||||||
|
$activeSerials++;
|
||||||
|
break;
|
||||||
|
case 'inactive':
|
||||||
|
$inactiveSerials++;
|
||||||
|
break;
|
||||||
|
case 'expired':
|
||||||
|
$expiredSerials++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'totalSerials' => $totalSerials,
|
||||||
|
'activeSerials' => $activeSerials,
|
||||||
|
'inactiveSerials' => $inactiveSerials,
|
||||||
|
'expiredSerials' => $expiredSerials,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/plugins/warranty/serials/update-expired', name: 'plugin_warranty_update_expired', methods: ['POST'])]
|
||||||
|
public function plugin_warranty_update_expired(EntityManagerInterface $entityManager, Access $access): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$acc = $access->hasRole('plugWarrantyManager');
|
||||||
|
if(!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
|
||||||
|
$repository = $entityManager->getRepository(PlugWarrantySerial::class);
|
||||||
|
$jdate = new Jdate();
|
||||||
|
$today = $jdate->GetTodayDate();
|
||||||
|
|
||||||
|
$expiredSerials = $repository->createQueryBuilder('s')
|
||||||
|
->where('s.bid = :businessId')
|
||||||
|
->andWhere('s.status = :status')
|
||||||
|
->andWhere('s.warrantyEndDate IS NOT NULL')
|
||||||
|
->andWhere('s.warrantyEndDate < :today')
|
||||||
|
->setParameter('businessId', $acc['bid'])
|
||||||
|
->setParameter('status', 'active')
|
||||||
|
->setParameter('today', $today)
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
|
||||||
|
$updatedCount = 0;
|
||||||
|
|
||||||
|
foreach ($expiredSerials as $serial) {
|
||||||
|
$serial->setStatus('expired');
|
||||||
|
$updatedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($updatedCount > 0) {
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'بهروزرسانی وضعیت سریالهای منقضی شده تکمیل شد',
|
||||||
|
'updatedCount' => $updatedCount
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -303,6 +303,10 @@ class Business
|
||||||
#[ORM\OneToMany(mappedBy: 'business', targetEntity: AIConversation::class, orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'business', targetEntity: AIConversation::class, orphanRemoval: true)]
|
||||||
private Collection $aiConversations;
|
private Collection $aiConversations;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'bid', targetEntity: PlugWarrantySerial::class, orphanRemoval: true)]
|
||||||
|
#[Ignore]
|
||||||
|
private Collection $plugWarrantySerials;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->logs = new ArrayCollection();
|
$this->logs = new ArrayCollection();
|
||||||
|
@ -347,6 +351,7 @@ class Business
|
||||||
$this->PlugGhestaDocs = new ArrayCollection();
|
$this->PlugGhestaDocs = new ArrayCollection();
|
||||||
$this->plugHrmDocs = new ArrayCollection();
|
$this->plugHrmDocs = new ArrayCollection();
|
||||||
$this->aiConversations = new ArrayCollection();
|
$this->aiConversations = new ArrayCollection();
|
||||||
|
$this->plugWarrantySerials = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
|
@ -2120,4 +2125,34 @@ class Business
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, PlugWarrantySerial>
|
||||||
|
*/
|
||||||
|
public function getPlugWarrantySerials(): Collection
|
||||||
|
{
|
||||||
|
return $this->plugWarrantySerials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPlugWarrantySerial(PlugWarrantySerial $plugWarrantySerial): static
|
||||||
|
{
|
||||||
|
if (!$this->plugWarrantySerials->contains($plugWarrantySerial)) {
|
||||||
|
$this->plugWarrantySerials->add($plugWarrantySerial);
|
||||||
|
$plugWarrantySerial->setBid($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removePlugWarrantySerial(PlugWarrantySerial $plugWarrantySerial): static
|
||||||
|
{
|
||||||
|
if ($this->plugWarrantySerials->removeElement($plugWarrantySerial)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($plugWarrantySerial->getBid() === $this) {
|
||||||
|
$plugWarrantySerial->setBid(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,10 @@ class Commodity
|
||||||
#[ORM\OneToMany(mappedBy: 'commodity', targetEntity: PlugRepserviceOrder::class, orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'commodity', targetEntity: PlugRepserviceOrder::class, orphanRemoval: true)]
|
||||||
private Collection $plugRepserviceOrders;
|
private Collection $plugRepserviceOrders;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'commodity', targetEntity: PlugWarrantySerial::class, orphanRemoval: true)]
|
||||||
|
#[Ignore]
|
||||||
|
private Collection $plugWarrantySerials;
|
||||||
|
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?bool $withoutTax = null;
|
private ?bool $withoutTax = null;
|
||||||
|
|
||||||
|
@ -115,6 +119,7 @@ class Commodity
|
||||||
$this->commodityDropLinks = new ArrayCollection();
|
$this->commodityDropLinks = new ArrayCollection();
|
||||||
$this->storeroomItems = new ArrayCollection();
|
$this->storeroomItems = new ArrayCollection();
|
||||||
$this->plugRepserviceOrders = new ArrayCollection();
|
$this->plugRepserviceOrders = new ArrayCollection();
|
||||||
|
$this->plugWarrantySerials = new ArrayCollection();
|
||||||
$this->priceListDetails = new ArrayCollection();
|
$this->priceListDetails = new ArrayCollection();
|
||||||
$this->preInvoiceItems = new ArrayCollection();
|
$this->preInvoiceItems = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
@ -424,6 +429,36 @@ class Commodity
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, PlugWarrantySerial>
|
||||||
|
*/
|
||||||
|
public function getPlugWarrantySerials(): Collection
|
||||||
|
{
|
||||||
|
return $this->plugWarrantySerials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPlugWarrantySerial(PlugWarrantySerial $plugWarrantySerial): static
|
||||||
|
{
|
||||||
|
if (!$this->plugWarrantySerials->contains($plugWarrantySerial)) {
|
||||||
|
$this->plugWarrantySerials->add($plugWarrantySerial);
|
||||||
|
$plugWarrantySerial->setCommodity($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removePlugWarrantySerial(PlugWarrantySerial $plugWarrantySerial): static
|
||||||
|
{
|
||||||
|
if ($this->plugWarrantySerials->removeElement($plugWarrantySerial)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($plugWarrantySerial->getCommodity() === $this) {
|
||||||
|
$plugWarrantySerial->setCommodity(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function isWithoutTax(): ?bool
|
public function isWithoutTax(): ?bool
|
||||||
{
|
{
|
||||||
return $this->withoutTax;
|
return $this->withoutTax;
|
||||||
|
|
|
@ -129,6 +129,9 @@ class Permission
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?bool $plugGhestaManager = null;
|
private ?bool $plugGhestaManager = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?bool $plugWarrantyManager = null;
|
||||||
|
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?bool $plugTaxSettings = null;
|
private ?bool $plugTaxSettings = null;
|
||||||
|
|
||||||
|
@ -599,6 +602,18 @@ class Permission
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isPlugWarrantyManager(): ?bool
|
||||||
|
{
|
||||||
|
return $this->plugWarrantyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPlugWarrantyManager(?bool $plugWarrantyManager): static
|
||||||
|
{
|
||||||
|
$this->plugWarrantyManager = $plugWarrantyManager;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function isPlugTaxSettings(): ?bool
|
public function isPlugTaxSettings(): ?bool
|
||||||
{
|
{
|
||||||
return $this->plugTaxSettings;
|
return $this->plugTaxSettings;
|
||||||
|
|
171
hesabixCore/src/Entity/PlugWarrantySerial.php
Normal file
171
hesabixCore/src/Entity/PlugWarrantySerial.php
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\PlugWarrantySerialRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Annotation\Ignore;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: PlugWarrantySerialRepository::class)]
|
||||||
|
class PlugWarrantySerial
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Ignore]
|
||||||
|
private ?Business $bid = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Ignore]
|
||||||
|
private ?Commodity $commodity = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, unique: true)]
|
||||||
|
private ?string $serialNumber = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 25)]
|
||||||
|
private ?string $dateSubmit = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')]
|
||||||
|
#[Ignore]
|
||||||
|
private ?User $submitter = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 25, nullable: true)]
|
||||||
|
private ?string $warrantyStartDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 25, nullable: true)]
|
||||||
|
private ?string $warrantyEndDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 50, nullable: true)]
|
||||||
|
private ?string $status = 'active';
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $notes = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->dateSubmit = date('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBid(): ?Business
|
||||||
|
{
|
||||||
|
return $this->bid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBid(?Business $bid): static
|
||||||
|
{
|
||||||
|
$this->bid = $bid;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCommodity(): ?Commodity
|
||||||
|
{
|
||||||
|
return $this->commodity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCommodity(?Commodity $commodity): static
|
||||||
|
{
|
||||||
|
$this->commodity = $commodity;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSerialNumber(): ?string
|
||||||
|
{
|
||||||
|
return $this->serialNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSerialNumber(string $serialNumber): static
|
||||||
|
{
|
||||||
|
$this->serialNumber = $serialNumber;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateSubmit(): ?string
|
||||||
|
{
|
||||||
|
return $this->dateSubmit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateSubmit(string $dateSubmit): static
|
||||||
|
{
|
||||||
|
$this->dateSubmit = $dateSubmit;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubmitter(): ?User
|
||||||
|
{
|
||||||
|
return $this->submitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSubmitter(?User $submitter): static
|
||||||
|
{
|
||||||
|
$this->submitter = $submitter;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWarrantyStartDate(): ?string
|
||||||
|
{
|
||||||
|
return $this->warrantyStartDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setWarrantyStartDate(?string $warrantyStartDate): static
|
||||||
|
{
|
||||||
|
$this->warrantyStartDate = $warrantyStartDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWarrantyEndDate(): ?string
|
||||||
|
{
|
||||||
|
return $this->warrantyEndDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setWarrantyEndDate(?string $warrantyEndDate): static
|
||||||
|
{
|
||||||
|
$this->warrantyEndDate = $warrantyEndDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): ?string
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(?string $status): static
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNotes(): ?string
|
||||||
|
{
|
||||||
|
return $this->notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNotes(?string $notes): static
|
||||||
|
{
|
||||||
|
$this->notes = $notes;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -98,6 +98,10 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
#[ORM\OneToMany(mappedBy: 'submitter', targetEntity: PlugRepserviceOrder::class, orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'submitter', targetEntity: PlugRepserviceOrder::class, orphanRemoval: true)]
|
||||||
private Collection $plugRepserviceOrders;
|
private Collection $plugRepserviceOrders;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'submitter', targetEntity: PlugWarrantySerial::class, orphanRemoval: true)]
|
||||||
|
#[Ignore]
|
||||||
|
private Collection $plugWarrantySerials;
|
||||||
|
|
||||||
#[ORM\OneToMany(mappedBy: 'submitter', targetEntity: Note::class, orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'submitter', targetEntity: Note::class, orphanRemoval: true)]
|
||||||
private Collection $notes;
|
private Collection $notes;
|
||||||
|
|
||||||
|
@ -159,6 +163,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
$this->cheques = new ArrayCollection();
|
$this->cheques = new ArrayCollection();
|
||||||
$this->mostDes = new ArrayCollection();
|
$this->mostDes = new ArrayCollection();
|
||||||
$this->plugRepserviceOrders = new ArrayCollection();
|
$this->plugRepserviceOrders = new ArrayCollection();
|
||||||
|
$this->plugWarrantySerials = new ArrayCollection();
|
||||||
$this->notes = new ArrayCollection();
|
$this->notes = new ArrayCollection();
|
||||||
$this->preInvoiceDocs = new ArrayCollection();
|
$this->preInvoiceDocs = new ArrayCollection();
|
||||||
$this->dashboardSettings = new ArrayCollection();
|
$this->dashboardSettings = new ArrayCollection();
|
||||||
|
@ -798,6 +803,36 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, PlugWarrantySerial>
|
||||||
|
*/
|
||||||
|
public function getPlugWarrantySerials(): Collection
|
||||||
|
{
|
||||||
|
return $this->plugWarrantySerials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPlugWarrantySerial(PlugWarrantySerial $plugWarrantySerial): static
|
||||||
|
{
|
||||||
|
if (!$this->plugWarrantySerials->contains($plugWarrantySerial)) {
|
||||||
|
$this->plugWarrantySerials->add($plugWarrantySerial);
|
||||||
|
$plugWarrantySerial->setSubmitter($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removePlugWarrantySerial(PlugWarrantySerial $plugWarrantySerial): static
|
||||||
|
{
|
||||||
|
if ($this->plugWarrantySerials->removeElement($plugWarrantySerial)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($plugWarrantySerial->getSubmitter() === $this) {
|
||||||
|
$plugWarrantySerial->setSubmitter(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection<int, Note>
|
* @return Collection<int, Note>
|
||||||
*/
|
*/
|
||||||
|
|
98
hesabixCore/src/Repository/PlugWarrantySerialRepository.php
Normal file
98
hesabixCore/src/Repository/PlugWarrantySerialRepository.php
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\PlugWarrantySerial;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<PlugWarrantySerial>
|
||||||
|
*/
|
||||||
|
class PlugWarrantySerialRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, PlugWarrantySerial::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return PlugWarrantySerial[] Returns an array of PlugWarrantySerial objects
|
||||||
|
*/
|
||||||
|
public function findByBusiness($bid): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('p')
|
||||||
|
->andWhere('p.bid = :val')
|
||||||
|
->setParameter('val', $bid)
|
||||||
|
->orderBy('p.dateSubmit', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return PlugWarrantySerial[] Returns an array of PlugWarrantySerial objects by commodity
|
||||||
|
*/
|
||||||
|
public function findByCommodity($bid, $commodityId): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('p')
|
||||||
|
->andWhere('p.bid = :bid')
|
||||||
|
->andWhere('p.commodity = :commodityId')
|
||||||
|
->setParameter('bid', $bid)
|
||||||
|
->setParameter('commodityId', $commodityId)
|
||||||
|
->orderBy('p.dateSubmit', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return PlugWarrantySerial[] Returns an array of PlugWarrantySerial objects by status
|
||||||
|
*/
|
||||||
|
public function findByStatus($bid, $status): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('p')
|
||||||
|
->andWhere('p.bid = :bid')
|
||||||
|
->andWhere('p.status = :status')
|
||||||
|
->setParameter('bid', $bid)
|
||||||
|
->setParameter('status', $status)
|
||||||
|
->orderBy('p.dateSubmit', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if serial number exists
|
||||||
|
*/
|
||||||
|
public function isSerialNumberExists($serialNumber, $bid = null): bool
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('p')
|
||||||
|
->andWhere('p.serialNumber = :serialNumber')
|
||||||
|
->setParameter('serialNumber', $serialNumber);
|
||||||
|
|
||||||
|
if ($bid) {
|
||||||
|
$qb->andWhere('p.bid = :bid')
|
||||||
|
->setParameter('bid', $bid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->getOneOrNullResult() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search serials by keyword
|
||||||
|
*/
|
||||||
|
public function searchSerials($bid, $keyword): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('p')
|
||||||
|
->leftJoin('p.commodity', 'c')
|
||||||
|
->andWhere('p.bid = :bid')
|
||||||
|
->andWhere('p.serialNumber LIKE :keyword OR c.name LIKE :keyword OR p.description LIKE :keyword')
|
||||||
|
->setParameter('bid', $bid)
|
||||||
|
->setParameter('keyword', '%' . $keyword . '%')
|
||||||
|
->orderBy('p.dateSubmit', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
532
webUI/src/components/plugins/warranty/BulkImportDialog.vue
Normal file
532
webUI/src/components/plugins/warranty/BulkImportDialog.vue
Normal file
|
@ -0,0 +1,532 @@
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="dialog" max-width="800px" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center p-3 gap-2">
|
||||||
|
<v-icon class="mr-3" color="success">mdi-file-excel</v-icon>
|
||||||
|
<span>وارد کردن انبوه سریالهای گارانتی</span>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-tabs v-model="activeTab" color="primary">
|
||||||
|
<v-tab value="manual">ورود دستی</v-tab>
|
||||||
|
<v-tab value="file">آپلود فایل</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
|
<v-window v-model="activeTab">
|
||||||
|
<!-- Manual Entry -->
|
||||||
|
<v-window-item value="manual">
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="d-flex justify-space-between align-center mb-4">
|
||||||
|
<h3 class="text-h6">ورود دستی سریالها</h3>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="addManualRow"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
افزودن ردیف
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="manual-entries">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in manualSerials"
|
||||||
|
:key="index"
|
||||||
|
class="entry-card mb-4"
|
||||||
|
>
|
||||||
|
<v-card variant="outlined" class="pa-4">
|
||||||
|
<div class="d-flex justify-space-between align-center mb-3">
|
||||||
|
<h4 class="text-subtitle-1">سریال {{ index + 1 }}</h4>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="removeManualRow(index)"
|
||||||
|
:disabled="manualSerials.length === 1"
|
||||||
|
></v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.serialNumber"
|
||||||
|
label="شماره سریال *"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[rules.serialNumber]"
|
||||||
|
hide-details="auto"
|
||||||
|
maxlength="50"
|
||||||
|
counter
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-autocomplete
|
||||||
|
v-model="item.commodity_id"
|
||||||
|
:items="commodities"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
label="محصول *"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[rules.commodity]"
|
||||||
|
hide-details="auto"
|
||||||
|
:filter="customFilter"
|
||||||
|
clearable
|
||||||
|
return-object
|
||||||
|
@update:model-value="(selectedCommodity) => handleCommoditySelect(selectedCommodity, index)"
|
||||||
|
></v-autocomplete>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="item.warrantyStartDate"
|
||||||
|
label="شروع گارانتی"
|
||||||
|
:rules="[rules.date]"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="item.warrantyEndDate"
|
||||||
|
label="پایان گارانتی"
|
||||||
|
:rules="[rules.date, (value: any) => rules.endDate(value, item.warrantyStartDate)]"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="item.status"
|
||||||
|
:items="statusOptions"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
label="وضعیت"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details="auto"
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="item.description"
|
||||||
|
label="توضیحات"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
rows="2"
|
||||||
|
auto-grow
|
||||||
|
hide-details="auto"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="manualSerials.length === 0"
|
||||||
|
type="info"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
|
<div class="text-center">
|
||||||
|
<v-icon size="48" color="info" class="mb-2">mdi-information</v-icon>
|
||||||
|
<div class="text-subtitle-1">هیچ سریالی اضافه نشده است</div>
|
||||||
|
<div class="text-body-2">برای شروع، روی دکمه "افزودن ردیف" کلیک کنید</div>
|
||||||
|
</div>
|
||||||
|
</v-alert>
|
||||||
|
</div>
|
||||||
|
</v-window-item>
|
||||||
|
|
||||||
|
<!-- File Upload -->
|
||||||
|
<v-window-item value="file">
|
||||||
|
<div class="mt-4">
|
||||||
|
<v-card variant="outlined" class="pa-4">
|
||||||
|
<div class="text-subtitle-1 mb-3">آپلود فایل Excel</div>
|
||||||
|
|
||||||
|
<v-file-input
|
||||||
|
v-model="uploadedFile"
|
||||||
|
label="انتخاب فایل"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
prepend-icon="mdi-file-excel"
|
||||||
|
show-size
|
||||||
|
counter
|
||||||
|
@change="handleFileUpload"
|
||||||
|
></v-file-input>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="filePreview.length > 0"
|
||||||
|
type="info"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
|
<div class="text-subtitle-2 mb-2">پیشنمایش دادهها:</div>
|
||||||
|
<v-data-table
|
||||||
|
:headers="previewHeaders"
|
||||||
|
:items="filePreview"
|
||||||
|
:items-per-page="5"
|
||||||
|
class="elevation-1"
|
||||||
|
></v-data-table>
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="fileErrors.length > 0"
|
||||||
|
type="warning"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
|
<div class="text-subtitle-2 mb-2">خطاهای موجود:</div>
|
||||||
|
<ul>
|
||||||
|
<li v-for="error in fileErrors" :key="error">{{ error }}</li>
|
||||||
|
</ul>
|
||||||
|
</v-alert>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</v-window-item>
|
||||||
|
</v-window>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="grey" @click="close">انصراف</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="success"
|
||||||
|
@click="importData"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!canImport"
|
||||||
|
>
|
||||||
|
وارد کردن
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
commodities: any[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
import: [data: any]
|
||||||
|
close: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const activeTab = ref('manual')
|
||||||
|
const uploadedFile = ref(null)
|
||||||
|
const filePreview = ref<any[]>([])
|
||||||
|
const fileErrors = ref<string[]>([])
|
||||||
|
|
||||||
|
const manualSerials = ref<any[]>([
|
||||||
|
{
|
||||||
|
serialNumber: '',
|
||||||
|
commodity_id: '',
|
||||||
|
description: '',
|
||||||
|
warrantyStartDate: '',
|
||||||
|
warrantyEndDate: '',
|
||||||
|
status: 'active'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
required: (value: any) => !!value || 'این فیلد الزامی است',
|
||||||
|
serialNumber: (value: any) => {
|
||||||
|
if (!value) return 'شماره سریال الزامی است'
|
||||||
|
if (!/^[A-Za-z0-9]+$/.test(value)) {
|
||||||
|
return 'شماره سریال فقط میتواند شامل حروف انگلیسی و اعداد باشد'
|
||||||
|
}
|
||||||
|
if (value.length < 3) {
|
||||||
|
return 'شماره سریال باید حداقل ۳ کاراکتر باشد'
|
||||||
|
}
|
||||||
|
if (value.length > 50) {
|
||||||
|
return 'شماره سریال نمیتواند بیشتر از ۵۰ کاراکتر باشد'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
commodity: (value: any) => {
|
||||||
|
if (!value) return 'انتخاب محصول الزامی است'
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
date: (value: any) => {
|
||||||
|
if (!value) return true
|
||||||
|
const date = new Date(value)
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return 'تاریخ نامعتبر است'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
endDate: (value: any, startDate: any) => {
|
||||||
|
if (!value || !startDate) return true
|
||||||
|
const end = new Date(value)
|
||||||
|
const start = new Date(startDate)
|
||||||
|
if (end <= start) {
|
||||||
|
return 'تاریخ پایان باید بعد از تاریخ شروع باشد'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ title: 'فعال', value: 'active' },
|
||||||
|
{ title: 'غیرفعال', value: 'inactive' },
|
||||||
|
{ title: 'منقضی شده', value: 'expired' },
|
||||||
|
// { title: 'استفاده شده', value: 'used' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const tableHeaders = [
|
||||||
|
{ title: 'شماره سریال', key: 'serialNumber' },
|
||||||
|
{ title: 'محصول', key: 'commodity_id' },
|
||||||
|
{ title: 'توضیحات', key: 'description' },
|
||||||
|
{ title: 'شروع گارانتی', key: 'warrantyStartDate' },
|
||||||
|
{ title: 'پایان گارانتی', key: 'warrantyEndDate' },
|
||||||
|
{ title: 'وضعیت', key: 'status' },
|
||||||
|
{ title: 'عملیات', key: 'actions', sortable: false }
|
||||||
|
]
|
||||||
|
|
||||||
|
const previewHeaders = [
|
||||||
|
{ title: 'شماره سریال', key: 'serialNumber' },
|
||||||
|
{ title: 'محصول', key: 'commodity' },
|
||||||
|
{ title: 'توضیحات', key: 'description' },
|
||||||
|
{ title: 'شروع گارانتی', key: 'warrantyStartDate' },
|
||||||
|
{ title: 'پایان گارانتی', key: 'warrantyEndDate' },
|
||||||
|
{ title: 'وضعیت', key: 'status' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const dialog = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const canImport = computed(() => {
|
||||||
|
if (activeTab.value === 'manual') {
|
||||||
|
return manualSerials.value.length > 0 &&
|
||||||
|
manualSerials.value.some(s => {
|
||||||
|
if (!s.serialNumber || !/^[A-Za-z0-9]{3,50}$/.test(s.serialNumber)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!s.commodity_id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (s.warrantyStartDate && s.warrantyEndDate) {
|
||||||
|
const start = new Date(s.warrantyStartDate)
|
||||||
|
const end = new Date(s.warrantyEndDate)
|
||||||
|
if (end <= start) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return uploadedFile.value && !fileErrors.value.length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const addManualRow = () => {
|
||||||
|
manualSerials.value.push({
|
||||||
|
serialNumber: '',
|
||||||
|
commodity_id: '',
|
||||||
|
description: '',
|
||||||
|
warrantyStartDate: '',
|
||||||
|
warrantyEndDate: '',
|
||||||
|
status: 'active'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeManualRow = (index: number) => {
|
||||||
|
manualSerials.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileUpload = async (file: File) => {
|
||||||
|
if (!file) {
|
||||||
|
filePreview.value = []
|
||||||
|
fileErrors.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
const response = await axios.post('/api/plugins/warranty/serials/preview-import', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filePreview.value = response.data.preview || []
|
||||||
|
fileErrors.value = response.data.errors || []
|
||||||
|
} catch (error) {
|
||||||
|
fileErrors.value = ['خطا در خواندن فایل']
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importData = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
let data
|
||||||
|
|
||||||
|
if (activeTab.value === 'manual') {
|
||||||
|
const validSerials = manualSerials.value.filter(s => {
|
||||||
|
if (!s.serialNumber || !/^[A-Za-z0-9]{3,50}$/.test(s.serialNumber)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!s.commodity_id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (s.warrantyStartDate && s.warrantyEndDate) {
|
||||||
|
const start = new Date(s.warrantyStartDate)
|
||||||
|
const end = new Date(s.warrantyEndDate)
|
||||||
|
if (end <= start) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (validSerials.length === 0) {
|
||||||
|
await Swal.fire({
|
||||||
|
text: 'هیچ سریال معتبری برای وارد کردن وجود ندارد. لطفاً فیلدهای الزامی را پر کنید.',
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: 'قبول'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data = validSerials
|
||||||
|
} else {
|
||||||
|
if (!uploadedFile.value) {
|
||||||
|
await Swal.fire({
|
||||||
|
text: 'فایل انتخاب نشده است',
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: 'قبول'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', uploadedFile.value)
|
||||||
|
|
||||||
|
const response = await axios.post('/api/plugins/warranty/serials/import-excel', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
data = response.data.serials || []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
await Swal.fire({
|
||||||
|
text: 'دادهای برای وارد کردن وجود ندارد',
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: 'قبول'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('import', { serials: data })
|
||||||
|
|
||||||
|
await Swal.fire({
|
||||||
|
text: `عملیات وارد کردن با موفقیت انجام شد. ${data.length} سریال وارد شد.`,
|
||||||
|
icon: 'success',
|
||||||
|
confirmButtonText: 'قبول'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
await Swal.fire({
|
||||||
|
text: 'خطا در وارد کردن دادهها',
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: 'قبول'
|
||||||
|
})
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const customFilter = (item: any, queryText: string) => {
|
||||||
|
const text = item.name.toLowerCase()
|
||||||
|
const searchText = queryText.toLowerCase()
|
||||||
|
return text.indexOf(searchText) > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCommoditySelect = (selectedCommodity: any, index: number) => {
|
||||||
|
if (selectedCommodity && selectedCommodity.id) {
|
||||||
|
manualSerials.value[index].commodity_id = selectedCommodity.id
|
||||||
|
} else {
|
||||||
|
manualSerials.value[index].commodity_id = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetData = () => {
|
||||||
|
manualSerials.value = [{
|
||||||
|
serialNumber: '',
|
||||||
|
commodity_id: '',
|
||||||
|
description: '',
|
||||||
|
warrantyStartDate: '',
|
||||||
|
warrantyEndDate: '',
|
||||||
|
status: 'active'
|
||||||
|
} as any]
|
||||||
|
uploadedFile.value = null
|
||||||
|
filePreview.value = []
|
||||||
|
fileErrors.value = []
|
||||||
|
activeTab.value = 'manual'
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
resetData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-dialog {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-entries {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-card .v-card {
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-card .v-card:hover {
|
||||||
|
border-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-entries::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-entries::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-entries::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-entries::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
</style>
|
288
webUI/src/components/plugins/warranty/SerialDialog.vue
Normal file
288
webUI/src/components/plugins/warranty/SerialDialog.vue
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="dialog" max-width="600px" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center p-3 gap-2">
|
||||||
|
<v-icon class="mr-3" color="primary">mdi-shield-check</v-icon>
|
||||||
|
<span>{{ isEdit ? 'ویرایش سریال گارانتی' : 'افزودن سریال گارانتی جدید' }}</span>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-form ref="form" v-model="valid">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.serialNumber"
|
||||||
|
label="شماره سریال *"
|
||||||
|
:rules="[rules.serialNumber]"
|
||||||
|
required
|
||||||
|
:disabled="isEdit"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
maxlength="50"
|
||||||
|
counter
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-autocomplete
|
||||||
|
v-model="formData.commodity_id"
|
||||||
|
label="محصول *"
|
||||||
|
:items="commodities"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
:rules="[rules.commodity]"
|
||||||
|
required
|
||||||
|
:filter="customFilter"
|
||||||
|
clearable
|
||||||
|
return-object
|
||||||
|
@update:model-value="handleCommoditySelect"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
></v-autocomplete>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="formData.warrantyStartDate"
|
||||||
|
label="تاریخ شروع گارانتی"
|
||||||
|
:rules="[rules.date]"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="formData.warrantyEndDate"
|
||||||
|
label="تاریخ پایان گارانتی"
|
||||||
|
:rules="[rules.date, (value: any) => rules.endDate(value, formData.warrantyStartDate)]"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.status"
|
||||||
|
label="وضعیت"
|
||||||
|
:items="statusOptions"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
label="توضیحات"
|
||||||
|
rows="3"
|
||||||
|
auto-grow
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
<!-- <v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.notes"
|
||||||
|
label="یادداشتها"
|
||||||
|
rows="2"
|
||||||
|
auto-grow
|
||||||
|
></v-textarea>
|
||||||
|
</v-col> -->
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="grey" @click="close">انصراف</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="save"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!valid"
|
||||||
|
>
|
||||||
|
{{ isEdit ? 'ویرایش' : 'ذخیره' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
serial?: any
|
||||||
|
commodities: any[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
save: [data: any]
|
||||||
|
close: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const valid = ref(false)
|
||||||
|
const form = ref()
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
serialNumber: '',
|
||||||
|
commodity_id: '',
|
||||||
|
description: '',
|
||||||
|
warrantyStartDate: '',
|
||||||
|
warrantyEndDate: '',
|
||||||
|
status: 'active',
|
||||||
|
notes: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
required: (value: any) => !!value || 'این فیلد الزامی است',
|
||||||
|
serialNumber: (value: any) => {
|
||||||
|
if (!value) return 'شماره سریال الزامی است'
|
||||||
|
if (!/^[A-Za-z0-9]+$/.test(value)) {
|
||||||
|
return 'شماره سریال فقط میتواند شامل حروف انگلیسی و اعداد باشد'
|
||||||
|
}
|
||||||
|
if (value.length < 3) {
|
||||||
|
return 'شماره سریال باید حداقل ۳ کاراکتر باشد'
|
||||||
|
}
|
||||||
|
if (value.length > 50) {
|
||||||
|
return 'شماره سریال نمیتواند بیشتر از ۵۰ کاراکتر باشد'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
commodity: (value: any) => {
|
||||||
|
if (!value) return 'انتخاب محصول الزامی است'
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
date: (value: any) => {
|
||||||
|
if (!value) return true
|
||||||
|
const date = new Date(value)
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return 'تاریخ نامعتبر است'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
endDate: (value: any, startDate: any) => {
|
||||||
|
if (!value || !startDate) return true
|
||||||
|
const end = new Date(value)
|
||||||
|
const start = new Date(startDate)
|
||||||
|
if (end <= start) {
|
||||||
|
return 'تاریخ پایان باید بعد از تاریخ شروع باشد'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ title: 'فعال', value: 'active' },
|
||||||
|
{ title: 'غیرفعال', value: 'inactive' },
|
||||||
|
{ title: 'منقضی شده', value: 'expired' },
|
||||||
|
// { title: 'استفاده شده', value: 'used' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const dialog = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isEdit = computed(() => !!props.serial)
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
const isValid = await form.value.validate()
|
||||||
|
if (!isValid) {
|
||||||
|
console.log('فرم دارای خطا است')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.value.serialNumber || !formData.value.commodity_id) {
|
||||||
|
console.log('فیلدهای الزامی پر نشدهاند')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const data = { ...formData.value }
|
||||||
|
emit('save', data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
clearValidationErrors()
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
serialNumber: '',
|
||||||
|
commodity_id: '',
|
||||||
|
description: '',
|
||||||
|
warrantyStartDate: '',
|
||||||
|
warrantyEndDate: '',
|
||||||
|
status: 'active',
|
||||||
|
notes: ''
|
||||||
|
}
|
||||||
|
if (form.value) {
|
||||||
|
form.value.resetValidation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearValidationErrors = () => {
|
||||||
|
if (form.value) {
|
||||||
|
form.value.resetValidation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSerialData = () => {
|
||||||
|
if (props.serial) {
|
||||||
|
formData.value = {
|
||||||
|
serialNumber: props.serial.serialNumber || '',
|
||||||
|
commodity_id: props.serial.commodity?.id || '',
|
||||||
|
description: props.serial.description || '',
|
||||||
|
warrantyStartDate: props.serial.warrantyStartDate || '',
|
||||||
|
warrantyEndDate: props.serial.warrantyEndDate || '',
|
||||||
|
status: props.serial.status || 'active',
|
||||||
|
notes: props.serial.notes || ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const customFilter = (item: any, queryText: string) => {
|
||||||
|
const text = item.name.toLowerCase()
|
||||||
|
const searchText = queryText.toLowerCase()
|
||||||
|
return text.indexOf(searchText) > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCommoditySelect = (selectedCommodity: any) => {
|
||||||
|
if (selectedCommodity && selectedCommodity.id) {
|
||||||
|
formData.value.commodity_id = selectedCommodity.id
|
||||||
|
} else {
|
||||||
|
formData.value.commodity_id = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.serial, () => {
|
||||||
|
nextTick(() => {
|
||||||
|
loadSerialData()
|
||||||
|
})
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
nextTick(() => {
|
||||||
|
loadSerialData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-dialog {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
</style>
|
183
webUI/src/components/plugins/warranty/SerialViewDialog.vue
Normal file
183
webUI/src/components/plugins/warranty/SerialViewDialog.vue
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="dialog" max-width="700px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center p-3 gap-2">
|
||||||
|
<v-icon class="mr-3" color="primary">mdi-shield-check</v-icon>
|
||||||
|
<span>جزئیات سریال گارانتی</span>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-chip
|
||||||
|
:color="getStatusColor(serial?.status)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getStatusText(serial?.status) }}
|
||||||
|
</v-chip>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text v-if="serial">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card variant="outlined" class="pa-4">
|
||||||
|
<div class="text-subtitle-2 text-grey mb-2">اطلاعات سریال</div>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="primary">mdi-barcode</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>شماره سریال</v-list-item-title>
|
||||||
|
<v-list-item-subtitle class="font-weight-bold">
|
||||||
|
{{ serial.serialNumber }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="primary">mdi-package-variant</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>محصول</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<div class="font-weight-bold">{{ serial.commodity?.name }}</div>
|
||||||
|
<div class="text-caption">کد: {{ serial.commodity?.code }}</div>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="primary">mdi-calendar</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>تاریخ ثبت</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ formatDate(serial.dateSubmit) }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card variant="outlined" class="pa-4">
|
||||||
|
<div class="text-subtitle-2 text-grey mb-2">اطلاعات گارانتی</div>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="success">mdi-calendar-start</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>شروع گارانتی</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ serial.warrantyStartDate ? formatDate(serial.warrantyStartDate) : 'تعیین نشده' }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="warning">mdi-calendar-end</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>پایان گارانتی</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ serial.warrantyEndDate ? formatDate(serial.warrantyEndDate) : 'تعیین نشده' }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="info">mdi-account</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>ثبت کننده</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ serial.submitter?.name }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" v-if="serial.description">
|
||||||
|
<v-card variant="outlined" class="pa-4">
|
||||||
|
<div class="text-subtitle-2 text-grey mb-2">توضیحات</div>
|
||||||
|
<p class="text-body-1">{{ serial.description }}</p>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" v-if="serial.notes">
|
||||||
|
<v-card variant="outlined" class="pa-4">
|
||||||
|
<div class="text-subtitle-2 text-grey mb-2">یادداشتها</div>
|
||||||
|
<p class="text-body-1">{{ serial.notes }}</p>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" @click="close">بستن</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import moment from 'jalali-moment';
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
serial?: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
close: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dialog = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active': return 'success'
|
||||||
|
case 'inactive': return 'grey'
|
||||||
|
case 'expired': return 'warning'
|
||||||
|
case 'used': return 'info'
|
||||||
|
default: return 'grey'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active': return 'فعال'
|
||||||
|
case 'inactive': return 'غیرفعال'
|
||||||
|
case 'expired': return 'منقضی شده'
|
||||||
|
// case 'used': return 'استفاده شده'
|
||||||
|
default: return status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (date: string) => {
|
||||||
|
if (!date) return '-'
|
||||||
|
|
||||||
|
if (date.includes('/') && date.split('/')[0].length === 4) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dateObj = new Date(date)
|
||||||
|
if (isNaN(dateObj.getTime())) return '-'
|
||||||
|
|
||||||
|
const jMoment = moment(dateObj)
|
||||||
|
const persianYear = jMoment.jYear()
|
||||||
|
const persianMonth = jMoment.jMonth() + 1
|
||||||
|
const persianDay = jMoment.jDate()
|
||||||
|
|
||||||
|
return `${persianYear}/${persianMonth.toString().padStart(2, '0')}/${persianDay.toString().padStart(2, '0')}`
|
||||||
|
} catch (error) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-dialog {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -89,6 +89,8 @@ const en_lang = {
|
||||||
inquiry: "Inquiries",
|
inquiry: "Inquiries",
|
||||||
hrm: 'HR & Payroll',
|
hrm: 'HR & Payroll',
|
||||||
hrm_docs: 'Payroll Document',
|
hrm_docs: 'Payroll Document',
|
||||||
|
warranty_system: 'Warranty System',
|
||||||
|
warranty_serials: 'Warranty Serials',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export default en_lang
|
export default en_lang
|
||||||
|
|
|
@ -197,6 +197,8 @@ const fa_lang = {
|
||||||
inquiry: "استعلامات",
|
inquiry: "استعلامات",
|
||||||
hrm: 'منابع انسانی',
|
hrm: 'منابع انسانی',
|
||||||
hrm_docs: 'سند حقوق',
|
hrm_docs: 'سند حقوق',
|
||||||
|
warranty_system: 'مدیریت گارانتی',
|
||||||
|
warranty_serials: 'سریالهای گارانتی',
|
||||||
buysellByPerson: "گزارش خرید و فروش های اشخاص",
|
buysellByPerson: "گزارش خرید و فروش های اشخاص",
|
||||||
tax_system: "سامانه مودیان مالیاتی",
|
tax_system: "سامانه مودیان مالیاتی",
|
||||||
tax_invoices: "صورتحساب ها",
|
tax_invoices: "صورتحساب ها",
|
||||||
|
|
|
@ -864,6 +864,16 @@ const router = createRouter({
|
||||||
component: () =>
|
component: () =>
|
||||||
import('../views/acc/plugins/onlinestore/intro.vue'),
|
import('../views/acc/plugins/onlinestore/intro.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'plugins/warranty',
|
||||||
|
name: 'plugin_warranty',
|
||||||
|
component: () =>
|
||||||
|
import('../views/acc/plugins/warranty/WarrantyPlugin.vue'),
|
||||||
|
meta: {
|
||||||
|
'title': 'مدیریت گارانتی',
|
||||||
|
'login': true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'notifications/list',
|
path: 'notifications/list',
|
||||||
name: 'notification_list',
|
name: 'notification_list',
|
||||||
|
|
|
@ -193,6 +193,7 @@ export default {
|
||||||
{ path: '/acc/plugin-center/invoice', key: '`', label: this.$t('drawer.plugins_invoices'), ctrl: true, shift: true, permission: () => this.permissions.owner },
|
{ path: '/acc/plugin-center/invoice', key: '`', label: this.$t('drawer.plugins_invoices'), ctrl: true, shift: true, permission: () => this.permissions.owner },
|
||||||
{ path: '/acc/hrm/docs/list', key: 'H', label: this.$t('drawer.hrm_docs'), ctrl: true, shift: true, permission: () => this.isPluginActive('hrm') && this.permissions.plugHrmDocs },
|
{ path: '/acc/hrm/docs/list', key: 'H', label: this.$t('drawer.hrm_docs'), ctrl: true, shift: true, permission: () => this.isPluginActive('hrm') && this.permissions.plugHrmDocs },
|
||||||
{ path: '/acc/plugins/ghesta/list', key: 'G', label: this.$t('drawer.ghesta_invoices'), ctrl: true, shift: true, permission: () => this.isPluginActive('ghesta') && this.permissions.plugGhestaManager },
|
{ path: '/acc/plugins/ghesta/list', key: 'G', label: this.$t('drawer.ghesta_invoices'), ctrl: true, shift: true, permission: () => this.isPluginActive('ghesta') && this.permissions.plugGhestaManager },
|
||||||
|
{ path: '/acc/plugins/warranty', key: 'W', label: this.$t('drawer.warranty_serials'), ctrl: true, shift: true, permission: () => this.isPluginActive('warranty') && this.permissions.plugWarranty },
|
||||||
{ path: '/acc/plugins/tax/invoices/list', key: 'L', label: this.$t('drawer.tax_invoices'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
{ path: '/acc/plugins/tax/invoices/list', key: 'L', label: this.$t('drawer.tax_invoices'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
||||||
{ path: '/acc/plugins/tax/settings', key: 'T', label: this.$t('drawer.tax_settings'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
{ path: '/acc/plugins/tax/settings', key: 'T', label: this.$t('drawer.tax_settings'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
||||||
];
|
];
|
||||||
|
@ -859,6 +860,26 @@ export default {
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list-group>
|
</v-list-group>
|
||||||
|
<v-list-group v-show="isPluginActive('warranty') && permissions.plugWarranty">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item class="text-dark" v-bind="props" :title="$t('drawer.warranty_system')">
|
||||||
|
<template v-slot:prepend><v-icon icon="mdi-shield-check" color="primary"></v-icon></template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
<v-list-item to="/acc/plugins/warranty">
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ $t('drawer.warranty_serials') }}
|
||||||
|
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/plugins/warranty') }}</span>
|
||||||
|
</v-list-item-title>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-tooltip :text="$t('dialog.add_new')" location="end">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" icon="mdi-plus-box" variant="plain" />
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list-group>
|
||||||
<v-list-group v-show="isPluginActive('taxsettings') && permissions.plugTaxSettings">
|
<v-list-group v-show="isPluginActive('taxsettings') && permissions.plugTaxSettings">
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-list-item class="text-dark" v-bind="props" :title="$t('drawer.tax_system')">
|
<v-list-item class="text-dark" v-bind="props" :title="$t('drawer.tax_system')">
|
||||||
|
|
828
webUI/src/views/acc/plugins/warranty/WarrantyPlugin.vue
Normal file
828
webUI/src/views/acc/plugins/warranty/WarrantyPlugin.vue
Normal file
|
@ -0,0 +1,828 @@
|
||||||
|
<template>
|
||||||
|
<div class="warranty-plugin">
|
||||||
|
<v-container fluid>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="6" sm="6" md="3">
|
||||||
|
<div class="stats-card total-card">
|
||||||
|
<div class="stats-icon">
|
||||||
|
<v-icon size="24" color="white" class="d-sm-none">mdi-barcode-scan</v-icon>
|
||||||
|
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-barcode-scan</v-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-number">{{ stats.totalSerials || 0 }}</div>
|
||||||
|
<div class="stats-label">کل سریالها</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="6" md="3">
|
||||||
|
<div :class="getCardClasses('active')" @click="filterByStatus('active')" role="button" tabindex="0">
|
||||||
|
<div class="stats-icon">
|
||||||
|
<v-icon size="24" color="white" class="d-sm-none">mdi-check-circle</v-icon>
|
||||||
|
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-check-circle</v-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-number">
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="statsLoading"
|
||||||
|
indeterminate
|
||||||
|
size="20"
|
||||||
|
color="white"
|
||||||
|
class="me-2"
|
||||||
|
></v-progress-circular>
|
||||||
|
{{ stats.activeSerials || 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="stats-label">سریالهای فعال</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="6" md="3">
|
||||||
|
<div :class="getCardClasses('inactive')" @click="filterByStatus('inactive')" role="button" tabindex="0">
|
||||||
|
<div class="stats-icon">
|
||||||
|
<v-icon size="24" color="white" class="d-sm-none">mdi-pause-circle</v-icon>
|
||||||
|
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-pause-circle</v-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-number">
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="statsLoading"
|
||||||
|
indeterminate
|
||||||
|
size="20"
|
||||||
|
color="white"
|
||||||
|
class="me-2"
|
||||||
|
></v-progress-circular>
|
||||||
|
{{ stats.inactiveSerials || 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="stats-label">سریالهای غیرفعال</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="6" md="3">
|
||||||
|
<div :class="getCardClasses('expired')" @click="filterByStatus('expired')" role="button" tabindex="0">
|
||||||
|
<div class="stats-icon">
|
||||||
|
<v-icon size="24" color="white" class="d-sm-none">mdi-clock-alert</v-icon>
|
||||||
|
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-clock-alert</v-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-number">
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="statsLoading"
|
||||||
|
indeterminate
|
||||||
|
size="20"
|
||||||
|
color="white"
|
||||||
|
class="me-2"
|
||||||
|
></v-progress-circular>
|
||||||
|
{{ stats.expiredSerials || 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="stats-label">سریالهای منقضی شده</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Data Table -->
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<!-- <v-card-title>
|
||||||
|
لیست سریالهای گارانتی
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-card-title> -->
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="serials"
|
||||||
|
:loading="loading"
|
||||||
|
density="comfortable"
|
||||||
|
class="elevation-1"
|
||||||
|
:header-props="{ class: 'custom-header' }"
|
||||||
|
hover
|
||||||
|
>
|
||||||
|
<template v-slot:top>
|
||||||
|
<!-- موبایل -->
|
||||||
|
<div class="d-block d-md-none pa-4">
|
||||||
|
<div class="d-flex gap-2 flex-column mb-3">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="showAddDialog = true"
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
افزودن سریال
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="secondary"
|
||||||
|
prepend-icon="mdi-upload"
|
||||||
|
@click="showBulkImportDialog = true"
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
وارد کردن انبوه
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<v-text-field
|
||||||
|
v-model="filters.search"
|
||||||
|
label="جستجو"
|
||||||
|
prepend-icon="mdi-magnify"
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
class="mb-3"
|
||||||
|
@update:model-value="loadSerials"
|
||||||
|
/>
|
||||||
|
<div class="d-flex gap-2 mb-3">
|
||||||
|
<v-select
|
||||||
|
v-model="filters.status"
|
||||||
|
label="وضعیت"
|
||||||
|
:items="statusOptions"
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
style="flex: 1;"
|
||||||
|
@update:model-value="loadSerials"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="filters.commodity_id"
|
||||||
|
label="محصول"
|
||||||
|
:items="commodities"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
style="flex: 1;"
|
||||||
|
@update:model-value="loadSerials"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- دسکتاپ -->
|
||||||
|
<div class="d-none d-md-block">
|
||||||
|
<v-toolbar flat style="height: 70px !important; padding: 10px !important;">
|
||||||
|
<v-text-field
|
||||||
|
v-model="filters.search"
|
||||||
|
label="جستجو"
|
||||||
|
prepend-icon="mdi-magnify"
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
style="max-width: 250px;"
|
||||||
|
@update:model-value="loadSerials"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="filters.status"
|
||||||
|
label="وضعیت"
|
||||||
|
:items="statusOptions"
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
style="max-width: 200px;"
|
||||||
|
@update:model-value="loadSerials"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="filters.commodity_id"
|
||||||
|
label="محصول"
|
||||||
|
:items="commodities"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
style="max-width: 250px;"
|
||||||
|
@update:model-value="loadSerials"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="showAddDialog = true"
|
||||||
|
>
|
||||||
|
افزودن سریال
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="secondary"
|
||||||
|
prepend-icon="mdi-upload"
|
||||||
|
@click="showBulkImportDialog = true"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
وارد کردن انبوه
|
||||||
|
</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:item.status="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="getStatusColor((item as any).status)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getStatusText((item as any).status) }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.dateSubmit="{ item }">
|
||||||
|
{{ formatDate((item as any).dateSubmit) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.warrantyStartDate="{ item }">
|
||||||
|
{{ (item as any).warrantyStartDate ? formatDate((item as any).warrantyStartDate) : '-' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.warrantyEndDate="{ item }">
|
||||||
|
{{ (item as any).warrantyEndDate ? formatDate((item as any).warrantyEndDate) : '-' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.commodity="{ item }">
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
@click="viewCommodity((item as any).commodity)"
|
||||||
|
class="text-none"
|
||||||
|
>
|
||||||
|
{{ (item as any).commodity?.name || 'نامشخص' }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-menu>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
icon="mdi-menu"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item @click="viewSerial(item)">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="info">mdi-eye</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>مشاهده سریال</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="editSerial(item)">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="warning">mdi-pencil</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>ویرایش سریال</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="deleteSerial(item)">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="error">mdi-delete</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>حذف سریال</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<!-- Add/Edit Dialog -->
|
||||||
|
<SerialDialog
|
||||||
|
v-model="showAddDialog"
|
||||||
|
:serial="selectedSerial"
|
||||||
|
:commodities="commodities"
|
||||||
|
@save="saveSerial"
|
||||||
|
@close="closeDialog"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- View Dialog -->
|
||||||
|
<SerialViewDialog
|
||||||
|
v-model="showViewDialog"
|
||||||
|
:serial="selectedSerial"
|
||||||
|
@close="closeViewDialog"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Bulk Import Dialog -->
|
||||||
|
<BulkImportDialog
|
||||||
|
v-model="showBulkImportDialog"
|
||||||
|
:commodities="commodities"
|
||||||
|
@import="bulkImport"
|
||||||
|
@close="closeBulkImportDialog"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Confirmation Dialog -->
|
||||||
|
<v-dialog v-model="showDeleteDialog" max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>تأیید حذف</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
آیا از حذف سریال <strong>{{ selectedSerial?.serialNumber }}</strong> اطمینان دارید؟
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="grey" @click="showDeleteDialog = false">انصراف</v-btn>
|
||||||
|
<v-btn color="error" @click="confirmDelete" :loading="loading">حذف</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Snackbar for notifications -->
|
||||||
|
<v-snackbar
|
||||||
|
v-model="showSnackbar"
|
||||||
|
:color="snackbarColor"
|
||||||
|
:timeout="3000"
|
||||||
|
location="bottom"
|
||||||
|
class="rounded-lg"
|
||||||
|
elevation="2"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon :color="snackbarColor" class="me-2">
|
||||||
|
{{ snackbarColor === 'success' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
||||||
|
</v-icon>
|
||||||
|
{{ snackbarText }}
|
||||||
|
</div>
|
||||||
|
<template v-slot:actions>
|
||||||
|
<v-btn icon variant="text" @click="showSnackbar = false">
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-snackbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import SerialDialog from '@/components/plugins/warranty/SerialDialog.vue'
|
||||||
|
import SerialViewDialog from '@/components/plugins/warranty/SerialViewDialog.vue'
|
||||||
|
import BulkImportDialog from '@/components/plugins/warranty/BulkImportDialog.vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import moment from 'jalali-moment'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const statsLoading = ref(false)
|
||||||
|
const serials = ref<any[]>([])
|
||||||
|
const commodities = ref<any[]>([])
|
||||||
|
const stats = ref({
|
||||||
|
totalSerials: 0,
|
||||||
|
activeSerials: 0,
|
||||||
|
expiredSerials: 0,
|
||||||
|
inactiveSerials: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const showSnackbar = ref(false)
|
||||||
|
const snackbarText = ref('')
|
||||||
|
const snackbarColor = ref('success')
|
||||||
|
|
||||||
|
const showNotification = (text: string, color: string = 'success') => {
|
||||||
|
snackbarText.value = text
|
||||||
|
snackbarColor.value = color
|
||||||
|
showSnackbar.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
const showViewDialog = ref(false)
|
||||||
|
const showDeleteDialog = ref(false)
|
||||||
|
const showBulkImportDialog = ref(false)
|
||||||
|
const selectedSerial = ref<any>(null)
|
||||||
|
|
||||||
|
const filters = ref({
|
||||||
|
search: '',
|
||||||
|
status: '',
|
||||||
|
commodity_id: null as any,
|
||||||
|
page: undefined as number | undefined,
|
||||||
|
limit: undefined as number | undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: 'عملیات', key: 'actions', sortable: false },
|
||||||
|
{ title: 'شماره سریال', key: 'serialNumber', sortable: true },
|
||||||
|
{ title: 'محصول', key: 'commodity', sortable: true },
|
||||||
|
{ title: 'وضعیت', key: 'status', sortable: true },
|
||||||
|
{ title: 'تاریخ ثبت', key: 'dateSubmit', sortable: true },
|
||||||
|
{ title: 'شروع گارانتی', key: 'warrantyStartDate', sortable: true },
|
||||||
|
{ title: 'پایان گارانتی', key: 'warrantyEndDate', sortable: true },
|
||||||
|
{ title: 'توضیحات', key: 'description' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ title: 'فعال', value: 'active' },
|
||||||
|
{ title: 'غیرفعال', value: 'inactive' },
|
||||||
|
{ title: 'منقضی شده', value: 'expired' },
|
||||||
|
// { title: 'استفاده شده', value: 'used' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const loadSerials = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const params = { ...filters.value }
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
if (params.search) queryParams.append('search', params.search)
|
||||||
|
if (params.status) queryParams.append('status', params.status)
|
||||||
|
if (params.commodity_id) queryParams.append('commodity_id', params.commodity_id)
|
||||||
|
if (params.page) queryParams.append('page', params.page.toString())
|
||||||
|
if (params.limit) queryParams.append('limit', params.limit.toString())
|
||||||
|
|
||||||
|
const response = await axios.get(`/api/plugins/warranty/serials?${queryParams}`)
|
||||||
|
serials.value = Array.isArray(response.data) ? response.data : []
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.response?.data?.error) {
|
||||||
|
showNotification(error.response.data.error, 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showNotification('خطا در بارگذاری سریالها', 'error')
|
||||||
|
console.error(error)
|
||||||
|
serials.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadStats = async () => {
|
||||||
|
try {
|
||||||
|
statsLoading.value = true
|
||||||
|
const response = await axios.get('/api/plugins/warranty/stats')
|
||||||
|
stats.value = response.data
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('خطا در بارگذاری آمار:', error)
|
||||||
|
const totalSerials = serials.value.length
|
||||||
|
const activeSerials = serials.value.filter(serial => serial.status === 'active').length
|
||||||
|
const inactiveSerials = serials.value.filter(serial => serial.status === 'inactive').length
|
||||||
|
const expiredSerials = serials.value.filter(serial => serial.status === 'expired').length
|
||||||
|
|
||||||
|
stats.value = {
|
||||||
|
totalSerials,
|
||||||
|
activeSerials,
|
||||||
|
inactiveSerials,
|
||||||
|
expiredSerials
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
statsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCommodities = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/commodity/list')
|
||||||
|
commodities.value = response.data
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.response?.data?.error) {
|
||||||
|
showNotification(error.response.data.error, 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showNotification('خطا در بارگذاری محصولات', 'error')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSerial = async (serialData: any) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
if (selectedSerial.value) {
|
||||||
|
await axios.post(`/api/plugins/warranty/serials/edit/${selectedSerial.value.id}`, serialData)
|
||||||
|
showNotification('سریال با موفقیت ویرایش شد')
|
||||||
|
} else {
|
||||||
|
await axios.post('/api/plugins/warranty/serials/add', serialData)
|
||||||
|
showNotification('سریال با موفقیت اضافه شد')
|
||||||
|
}
|
||||||
|
await loadSerials()
|
||||||
|
closeDialog()
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.response?.data?.error) {
|
||||||
|
showNotification(error.response.data.error, 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showNotification('خطا در ذخیره سریال', 'error')
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewSerial = (serial: any) => {
|
||||||
|
selectedSerial.value = serial
|
||||||
|
showViewDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const editSerial = (serial: any) => {
|
||||||
|
selectedSerial.value = serial
|
||||||
|
showAddDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSerial = (serial: any) => {
|
||||||
|
selectedSerial.value = serial
|
||||||
|
showDeleteDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await axios.delete(`/api/plugins/warranty/serials/${selectedSerial.value.id}`)
|
||||||
|
showNotification('سریال با موفقیت حذف شد')
|
||||||
|
await loadSerials()
|
||||||
|
showDeleteDialog.value = false
|
||||||
|
} catch (error: any) {
|
||||||
|
showNotification('خطا در حذف سریال', 'error')
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bulkImport = async (serialsData: any) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await axios.post('/api/plugins/warranty/serials/bulk-import', serialsData)
|
||||||
|
showNotification(`عملیات وارد کردن انبوه تکمیل شد. ${response.data.successCount} موفق، ${response.data.errorCount} ناموفق`)
|
||||||
|
await loadSerials()
|
||||||
|
closeBulkImportDialog()
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.response?.data?.error) {
|
||||||
|
showNotification(error.response.data.error, 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showNotification('خطا در وارد کردن انبوه', 'error')
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewCommodity = (commodity: any) => {
|
||||||
|
if (commodity && commodity.id) {
|
||||||
|
router.push(`/acc/commodity/mod/${commodity.code}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
showAddDialog.value = false
|
||||||
|
selectedSerial.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeViewDialog = () => {
|
||||||
|
showViewDialog.value = false
|
||||||
|
selectedSerial.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeBulkImportDialog = () => {
|
||||||
|
showBulkImportDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
filters.value = {
|
||||||
|
search: '',
|
||||||
|
status: '',
|
||||||
|
commodity_id: null,
|
||||||
|
page: undefined,
|
||||||
|
limit: undefined
|
||||||
|
}
|
||||||
|
loadSerials()
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterByStatus = async (status: string) => {
|
||||||
|
if (filters.value.status === status) {
|
||||||
|
filters.value.status = ''
|
||||||
|
} else {
|
||||||
|
filters.value.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
filters.value.search = ''
|
||||||
|
filters.value.commodity_id = null
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
loadSerials(),
|
||||||
|
loadStats()
|
||||||
|
])
|
||||||
|
|
||||||
|
const statusText = getStatusText(status)
|
||||||
|
if (filters.value.status === status) {
|
||||||
|
showNotification(`فیلتر بر اساس وضعیت "${statusText}" اعمال شد`)
|
||||||
|
} else {
|
||||||
|
showNotification('فیلتر وضعیت پاک شد')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCardClasses = (status: string) => {
|
||||||
|
const baseClasses = 'stats-card'
|
||||||
|
const statusClasses = {
|
||||||
|
'active': 'active-card',
|
||||||
|
'inactive': 'inactive-card',
|
||||||
|
'expired': 'expired-card'
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = [baseClasses, statusClasses[status as keyof typeof statusClasses]]
|
||||||
|
|
||||||
|
if (filters.value.status === status) {
|
||||||
|
classes.push('active-filter')
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active':
|
||||||
|
return 'success'
|
||||||
|
case 'inactive':
|
||||||
|
return 'grey'
|
||||||
|
case 'expired':
|
||||||
|
return 'warning'
|
||||||
|
case 'used':
|
||||||
|
return 'info'
|
||||||
|
default:
|
||||||
|
return 'grey'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active':
|
||||||
|
return 'فعال'
|
||||||
|
case 'inactive':
|
||||||
|
return 'غیرفعال'
|
||||||
|
case 'expired':
|
||||||
|
return 'منقضی شده'
|
||||||
|
// case 'used':
|
||||||
|
// return 'استفاده شده'
|
||||||
|
default:
|
||||||
|
return 'نامشخص'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (date: string) => {
|
||||||
|
if (!date) return '-'
|
||||||
|
|
||||||
|
if (date.includes('/') && date.split('/')[0].length === 4) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dateObj = new Date(date)
|
||||||
|
if (isNaN(dateObj.getTime())) return '-'
|
||||||
|
|
||||||
|
const jMoment = moment(dateObj)
|
||||||
|
const persianYear = jMoment.jYear()
|
||||||
|
const persianMonth = jMoment.jMonth() + 1
|
||||||
|
const persianDay = jMoment.jDate()
|
||||||
|
|
||||||
|
return `${persianYear}/${persianMonth.toString().padStart(2, '0')}/${persianDay.toString().padStart(2, '0')}`
|
||||||
|
} catch (error) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
loadStats(),
|
||||||
|
loadSerials(),
|
||||||
|
loadCommodities()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.warranty-plugin {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-data-table {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table-header th) {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table td) {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-bottom: 1px solid #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table tr:hover) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-chip) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 120px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card:focus {
|
||||||
|
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.active-filter {
|
||||||
|
transform: translateY(-4px) scale(1.02);
|
||||||
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.active-filter::before {
|
||||||
|
background: linear-gradient(45deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(45deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card:hover {
|
||||||
|
transform: translateY(-8px) scale(1.01);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-icon {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.9;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Variants - Professional Colors */
|
||||||
|
.total-card {
|
||||||
|
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-card {
|
||||||
|
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-card {
|
||||||
|
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive-card {
|
||||||
|
background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-card {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in a new issue