update two-step ui/fix Warranty plugin bugs

This commit is contained in:
Gloomy 2025-08-18 19:53:48 +00:00
parent d3bd560e36
commit dd78e12a7a
4 changed files with 335 additions and 137 deletions

View file

@ -23,6 +23,7 @@ use App\Service\PluginService;
use App\Service\SMS;
use App\Service\registryMGR;
use Symfony\Component\Validator\Constraints as Assert;
use App\Entity\StoreroomTicket;
class PlugWarrantyController extends AbstractController
{
@ -39,6 +40,66 @@ class PlugWarrantyController extends AbstractController
}
}
#[Route('/api/plugins/warranty/serials/by-storeroom-ticket/{code}', name: 'plugin_warranty_serials_by_storeroom_ticket', methods: ['GET'])]
public function plugin_warranty_serials_by_storeroom_ticket(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, PluginService $pluginService): JsonResponse
{
$acc = $access->hasRole('store');
if (!$acc) {
throw $this->createAccessDeniedException();
}
if (!$pluginService->isActive('warranty', $acc['bid'])) {
return $this->json(['success' => false, 'message' => 'افزونه گارانتی فعال نیست'], 403);
}
/** @var StoreroomTicket|null $ticket */
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
]);
if (!$ticket) {
return $this->json(['success' => false, 'message' => 'حواله یافت نشد'], 404);
}
$doc = $ticket->getDoc();
if (!$doc) {
return $this->json([
'success' => true,
'ticketActivationCode' => $ticket->getActivationCode(),
'items' => []
]);
}
$serials = $entityManager->getRepository(PlugWarrantySerial::class)->createQueryBuilder('s')
->andWhere('s.business = :bid')
->andWhere('s.allocatedToDocumentId = :docId')
->setParameter('bid', $acc['bid'])
->setParameter('docId', $doc->getId())
->getQuery()
->getResult();
$items = array_map(function (PlugWarrantySerial $s) use ($entityManager) {
$commodity = $s->getCommodity();
return [
'serialNumber' => $s->getSerialNumber(),
'commodity' => $commodity ? [
'id' => $commodity->getId(),
'name' => $commodity->getName(),
'code' => $commodity->getCode(),
] : null,
'status' => $s->getStatus(),
'activation' => $s->getActivation(),
'activationTicketCode' => $s->getActivationTicketCode(),
'warrantyEndDate' => $s->getWarrantyEndDate()?->format('Y-m-d'),
];
}, $serials);
return $this->json([
'success' => true,
'ticketActivationCode' => $ticket->getActivationCode(),
'items' => $items
]);
}
private function expiredFlag(?\DateTimeImmutable $end): bool
{
return $end !== null && $end < new \DateTimeImmutable('today');
@ -938,16 +999,18 @@ class PlugWarrantyController extends AbstractController
}
#[Route('/api/plugins/warranty/settings/get', name: 'plugin_warranty_settings_get', methods: ['GET'])]
public function plugin_warranty_settings_get(Access $access, registryMGR $registryMGR): JsonResponse
public function plugin_warranty_settings_get(Access $access, registryMGR $registryMGR, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('plugWarrantyManager');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$require = filter_var($registryMGR->get('warranty', 'requireWarrantyOnDelivery'), FILTER_VALIDATE_BOOLEAN);
$grace = (int) ($registryMGR->get('warranty', 'activationGraceDays') ?? 7);
$match = filter_var($registryMGR->get('warranty', 'matchWarrantyToSerial'), FILTER_VALIDATE_BOOLEAN);
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
$require = filter_var($business->getRequireWarrantyOnDelivery(), FILTER_VALIDATE_BOOLEAN);
$grace = (int) ($business->getActivationGraceDays() ?? 7);
$match = filter_var($business->getMatchWarrantyToSerial(), FILTER_VALIDATE_BOOLEAN);
return $this->json([
'requireWarrantyOnDelivery' => (bool) $require,
'activationGraceDays' => max(0, $grace),
@ -956,22 +1019,27 @@ class PlugWarrantyController extends AbstractController
}
#[Route('/api/plugins/warranty/settings/save', name: 'plugin_warranty_settings_save', methods: ['POST'])]
public function plugin_warranty_settings_save(Request $request, Access $access, registryMGR $registryMGR): JsonResponse
public function plugin_warranty_settings_save(Request $request, Access $access, registryMGR $registryMGR, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('plugWarrantyManager');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
$params = json_decode($request->getContent() ?: '{}', true);
$require = isset($params['requireWarrantyOnDelivery']) && ($params['requireWarrantyOnDelivery'] === true || $params['requireWarrantyOnDelivery'] === '1' || $params['requireWarrantyOnDelivery'] === 1 || $params['requireWarrantyOnDelivery'] === 'true');
$graceDays = isset($params['activationGraceDays']) ? (int) $params['activationGraceDays'] : 7;
if ($graceDays < 0) { $graceDays = 0; }
$match = isset($params['matchWarrantyToSerial']) && ($params['matchWarrantyToSerial'] === true || $params['matchWarrantyToSerial'] === '1' || $params['matchWarrantyToSerial'] === 1 || $params['matchWarrantyToSerial'] === 'true');
$registryMGR->update('warranty', 'requireWarrantyOnDelivery', $require ? '1' : '0');
$registryMGR->update('warranty', 'activationGraceDays', (string) $graceDays);
$registryMGR->update('warranty', 'matchWarrantyToSerial', $match ? '1' : '0');
$business->setRequireWarrantyOnDelivery($require);
$business->setActivationGraceDays($graceDays);
$business->setMatchWarrantyToSerial($match);
$entityManager->flush();
return $this->json(['success' => true]);
}

View file

@ -325,6 +325,15 @@ class Business
#[ORM\Column(length: 255, nullable: true)]
private ?string $financialApprover = null;
#[ORM\Column(nullable: true)]
private ?bool $requireWarrantyOnDelivery = null;
#[ORM\Column(nullable: true)]
private ?int $activationGraceDays = null;
#[ORM\Column(nullable: true)]
private ?bool $matchWarrantyToSerial = null;
public function __construct()
{
$this->logs = new ArrayCollection();
@ -2254,4 +2263,37 @@ class Business
$this->financialApprover = $financialApprover;
return $this;
}
}
public function getRequireWarrantyOnDelivery(): ?bool
{
return $this->requireWarrantyOnDelivery;
}
public function setRequireWarrantyOnDelivery(?bool $requireWarrantyOnDelivery): static
{
$this->requireWarrantyOnDelivery = $requireWarrantyOnDelivery;
return $this;
}
public function getActivationGraceDays(): ?int
{
return $this->activationGraceDays;
}
public function setActivationGraceDays(?int $activationGraceDays): static
{
$this->activationGraceDays = $activationGraceDays;
return $this;
}
public function getMatchWarrantyToSerial(): ?bool
{
return $this->matchWarrantyToSerial;
}
public function setMatchWarrantyToSerial(?bool $matchWarrantyToSerial): static
{
$this->matchWarrantyToSerial = $matchWarrantyToSerial;
return $this;
}
}

View file

@ -310,142 +310,126 @@
</v-card-text>
</v-card>
<!-- بخش کالا و خدمات -->
<h3 class="text-primary mt-4" v-if="isPluginActive('accpro')">تایید دومرحله‌ای</h3>
<v-row v-if="isPluginActive('accpro')">
<v-col cols="12">
<!-- تایید دومرحلهای -->
<v-card v-if="isPluginActive('accpro')" variant="outlined" class="mb-6">
<v-card-title class="text-h6 text-primary">
<v-icon icon="mdi-shield-check" class="mr-2"></v-icon>
تایید دومرحلهای
</v-card-title>
<v-card-text>
<v-switch
v-model="content.requireTwoStepApproval"
label="فعال‌سازی تایید دومرحله‌ای برای اسناد"
color="primary"
hide-details
class="mb-2"
></v-switch>
<div class="text-caption text-medium-emphasis mt-1">
<div class="text-body-2 text-medium-emphasis mb-2">
با فعالسازی این گزینه، تمام فاکتورها، حوالههای انبار، دریافتها و پرداختها نیاز به تایید مدیر خواهند داشت.
</div>
</v-col>
</v-row>
<!-- تنظیمات تاییدکنندگان -->
<v-row v-if="content.requireTwoStepApproval && isPluginActive('accpro')">
<v-col cols="12">
<v-card variant="outlined" class="pa-4">
<h4 class="text-subtitle-1 mb-3">تعیین تاییدکنندگان</h4>
<!-- تاییدکننده فاکتور فروش -->
<div class="mb-4">
<v-select
v-model="content.invoiceApprover"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده فاکتور فروش"
variant="outlined"
density="compact"
clearable
hint="کاربری که می‌تواند فاکتورهای فروش را تایید کند"
persistent-hint
>
<template v-slot:item="{ item, props }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-icon
:color="item.owner ? 'success' : 'primary'"
size="small"
>
{{ item.owner ? 'mdi-crown' : 'mdi-account' }}
</v-icon>
<v-expand-transition>
<div v-if="content.requireTwoStepApproval">
<v-divider class="my-4"></v-divider>
<h4 class="text-subtitle-1 mb-3">تعیین تاییدکنندگان</h4>
<v-row>
<v-col cols="12">
<v-select
v-model="content.invoiceApprover"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده فاکتور فروش"
variant="outlined"
density="compact"
clearable
hint="کاربری که می‌تواند فاکتورهای فروش را تایید کند"
persistent-hint
>
<template v-slot:item="{ item, props }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-icon
:color="item.owner ? 'success' : 'primary'"
size="small"
>
{{ item.owner ? 'mdi-crown' : 'mdi-account' }}
</v-icon>
</template>
<v-list-item-title>{{ item.name }}</v-list-item-title>
<v-list-item-subtitle>{{ item.email }}</v-list-item-subtitle>
</v-list-item>
</template>
<v-list-item-title>{{ item.name }}</v-list-item-title>
<v-list-item-subtitle>{{ item.email }}</v-list-item-subtitle>
</v-list-item>
</template>
</v-select>
<div class="text-caption text-medium-emphasis mt-1">
این کاربر افزون بر مدیر کسب و کار میتواند فاکتورهای فروش را تایید کند
</v-select>
<div class="text-caption text-medium-emphasis mt-1">
این کاربر افزون بر مدیر کسب و کار میتواند فاکتورهای فروش را تایید کند
</div>
</v-col>
<v-col cols="12">
<v-select
v-model="content.warehouseApprover"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده حواله انبار"
variant="outlined"
density="compact"
clearable
hint="کاربری که می‌تواند حواله‌های انبار را تایید کند"
persistent-hint
>
<template v-slot:item="{ item, props }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-icon
:color="item.owner ? 'success' : 'primary'"
size="small"
>
{{ item.owner ? 'mdi-crown' : 'mdi-account' }}
</v-icon>
</template>
<v-list-item-title>{{ item.name }}</v-list-item-title>
<v-list-item-subtitle>{{ item.email }}</v-list-item-subtitle>
</v-list-item>
</template>
</v-select>
<div class="text-caption text-medium-emphasis mt-1">
این کاربر افزون بر مدیر کسب و کار میتواند حوالههای انبار را تایید کند
</div>
</v-col>
<!-- تاییدکننده دریافت و پرداخت مالی (غیرفعال فعلاً) -->
<!--
<v-col cols="12">
<v-select
v-model="content.financialApprover"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده دریافت و پرداخت مالی"
variant="outlined"
density="compact"
clearable
hint="کاربری که می‌تواند دریافت‌ها و پرداخت‌ها را تایید کند"
persistent-hint
/>
</v-col>
-->
</v-row>
<div class="text-caption text-medium-emphasis">
<v-icon size="small" color="info" class="me-1">mdi-information</v-icon>
در صورت عدم انتخاب تاییدکننده، فقط مدیر کسب و کار میتواند اسناد را تایید کند
</div>
<div class="text-caption text-medium-emphasis mt-2">
<v-icon size="small" color="success" class="me-1">mdi-check-circle</v-icon>
<strong>نکته:</strong> صاحب کسب و کار همیشه میتواند تمام اسناد را تأیید کند و نیازی به تعیین مجدد ندارد
</div>
</div>
<!-- تاییدکننده حواله انبار -->
<div class="mb-4">
<v-select
v-model="content.warehouseApprover"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده حواله انبار"
variant="outlined"
density="compact"
clearable
hint="کاربری که می‌تواند حواله‌های انبار را تایید کند"
persistent-hint
>
<template v-slot:item="{ item, props }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-icon
:color="item.owner ? 'success' : 'primary'"
size="small"
>
{{ item.owner ? 'mdi-crown' : 'mdi-account' }}
</v-icon>
</template>
<v-list-item-title>{{ item.name }}</v-list-item-title>
<v-list-item-subtitle>{{ item.email }}</v-list-item-subtitle>
</v-list-item>
</template>
</v-select>
<div class="text-caption text-medium-emphasis mt-1">
این کاربر افزون بر مدیر کسب و کار میتواند حوالههای انبار را تایید کند
</div>
</div>
<!-- تاییدکننده دریافت و پرداخت مالی -->
<!-- <div class="mb-4">
<v-select
v-model="content.financialApprover"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده دریافت و پرداخت مالی"
variant="outlined"
density="compact"
clearable
hint="کاربری که می‌تواند دریافت‌ها و پرداخت‌ها را تایید کند"
persistent-hint
>
<template v-slot:item="{ item, props }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-icon
:color="item.owner ? 'success' : 'primary'"
size="small"
>
{{ item.owner ? 'mdi-crown' : 'mdi-account' }}
</v-icon>
</template>
<v-list-item-title>{{ item.name }}</v-list-item-title>
<v-list-item-subtitle>{{ item.email }}</v-list-item-subtitle>
</v-list-item>
</template>
</v-select>
<div class="text-caption text-medium-emphasis mt-1">
این کاربر افزون بر مدیر کسب و کار میتواند دریافتها و پرداختها را تایید کند
</div>
</div> -->
<div class="text-caption text-medium-emphasis">
<v-icon size="small" color="info" class="me-1">mdi-information</v-icon>
در صورت عدم انتخاب تاییدکننده، فقط مدیر کسب و کار میتواند اسناد را تایید کند
</div>
<div class="text-caption text-medium-emphasis mt-2">
<v-icon size="small" color="success" class="me-1">mdi-check-circle</v-icon>
<strong>نکته:</strong> صاحب کسب و کار همیشه میتواند تمام اسناد را تأیید کند و نیازی به تعیین مجدد ندارد
</div>
</v-card>
</v-col>
</v-row>
</v-expand-transition>
</v-card-text>
</v-card>
<v-card variant="outlined">
<v-card-title class="text-h6 text-primary">

View file

@ -63,6 +63,8 @@ interface Item {
const router = useRouter()
const loading = ref(false)
const bid = ref<Business>({ legal_name: '' })
const warrantySerials = ref<any[]>([])
const ticketActivationCode = ref<string | null>(null)
const item = ref<Item>({
ticket: {
id: 0,
@ -103,9 +105,10 @@ const headers = [
const loadData = async () => {
loading.value = true
try {
const [ticketResponse, businessResponse] = await Promise.all([
const [ticketResponse, businessResponse, warrantyResponse] = await Promise.all([
axios.post(`/api/storeroom/tickets/info/${router.currentRoute.value.params.id}`),
axios.post(`/api/business/get/info/${localStorage.getItem('activeBid')}`)
axios.post(`/api/business/get/info/${localStorage.getItem('activeBid')}`),
axios.get(`/api/plugins/warranty/serials/by-storeroom-ticket/${router.currentRoute.value.params.id}`)
])
item.value.ticket = ticketResponse.data.ticket
@ -114,6 +117,10 @@ const loadData = async () => {
item.value.rows = ticketResponse.data.commodities
bid.value = businessResponse.data
// warranty data
warrantySerials.value = (warrantyResponse.data && warrantyResponse.data.items) ? warrantyResponse.data.items : []
ticketActivationCode.value = warrantyResponse.data ? (warrantyResponse.data.ticketActivationCode || null) : null
} catch (error) {
console.error('Error loading data:', error)
} finally {
@ -133,6 +140,50 @@ const printInvoice = async () => {
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'available': return 'success'
case 'allocated': return 'info'
case 'verified': return 'primary'
case 'bound': return 'warning'
case 'consumed': return 'teal'
case 'void': return 'grey'
case 'active': return 'success'
case 'deactive': return 'error'
default: return 'grey'
}
}
const getStatusText = (status: string) => {
switch (status) {
case 'available': return 'آزاد'
case 'allocated': return 'تخصیص یافته'
case 'verified': return 'تأیید شده'
case 'bound': return 'متصل'
case 'consumed': return 'مصرف شده'
case 'void': return 'باطل'
case 'active': return 'فعال'
case 'deactive': return 'فعال نشده'
default: return 'نامشخص'
}
}
const formatDate = (dateVal: any) => {
if (!dateVal) return '-'
try {
const d = typeof dateVal === 'string' ? new Date(dateVal) : dateVal
const valid = d instanceof Date && !isNaN(d.getTime())
if (!valid) return '-'
const j = moment(d)
const y = j.jYear()
const m = j.jMonth() + 1
const day = j.jDate()
return `${y}/${String(m).padStart(2, '0')}/${String(day).padStart(2, '0')}`
} catch {
return '-'
}
}
// Attachments
const attachments = ref<any[]>([])
const loadingAttachments = ref(false)
@ -417,8 +468,8 @@ onMounted(() => {
:headers="headers"
:items="item.rows"
:loading="loading"
class="elevation-1 text-center"
:header-props="{ class: 'custom-header' }"
class="elevation-1 text-center"
:header-props="{ class: 'custom-header' }"
density="compact"
show-expand
>
@ -442,6 +493,59 @@ onMounted(() => {
</v-card-text>
</v-card>
<!-- Warranty Section -->
<v-card variant="outlined" class="mt-4">
<v-card-title class="text-subtitle-1 font-weight-bold">
<v-icon start>mdi-shield-check</v-icon>
گارانتیهای حواله و کد فعالسازی
</v-card-title>
<v-card-text>
<v-row class="mb-2">
<v-col cols="12" md="6">
<v-text-field
:model-value="ticketActivationCode || '—'"
label="کد فعال‌سازی حواله"
variant="outlined"
density="compact"
readonly
prepend-inner-icon="mdi-key-variant"
/>
</v-col>
</v-row>
<v-data-table
:headers="[
{ title: 'سریال گارانتی', key: 'serialNumber' },
{ title: 'کالا', key: 'commodity' },
{ title: 'وضعیت', key: 'status' },
{ title: 'فعال‌سازی', key: 'activation' },
// { title: 'کد فعالسازی', key: 'activationTicketCode' },
{ title: 'اتمام گارانتی', key: 'warrantyEndDate' }
]"
:items="warrantySerials"
density="compact"
class="elevation-1"
:loading="loading"
>
<template #item.commodity="{ item }">
<span v-if="item.commodity">{{ item.commodity.code }} {{ item.commodity.name }}</span>
<span v-else></span>
</template>
<template #item.status="{ item }">
<v-chip :color="getStatusColor(item.status)" size="small">{{ getStatusText(item.status) }}</v-chip>
</template>
<template #item.activation="{ item }">
<v-chip :color="getStatusColor(item.activation)" size="small">{{ getStatusText(item.activation) }}</v-chip>
</template>
<template #item.warrantyEndDate="{ item }">
<span>{{ formatDate(item.warrantyEndDate) }}</span>
</template>
</v-data-table>
</v-card-text>
</v-card>
<v-card variant="outlined" class="mt-4" :class="{ 'opacity-50': isAttachmentsDisabled }">
<v-card-title class="text-subtitle-1 font-weight-bold">
<v-icon start>mdi-paperclip</v-icon>