feat: add withdraw request functionality and UI components
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"dev"
|
||||||
|
],
|
||||||
|
"port": 5173
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "serve",
|
||||||
|
"runtimeExecutable": "php",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"artisan",
|
||||||
|
"serve"
|
||||||
|
],
|
||||||
|
"port": 8000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -8,5 +8,13 @@ enum DocumentPriorityEnum: string
|
|||||||
|
|
||||||
case Medium = 'عادی';
|
case Medium = 'عادی';
|
||||||
case High = 'اورژانسی';
|
case High = 'اورژانسی';
|
||||||
|
|
||||||
|
public function processingHours(): int
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Medium => 24,
|
||||||
|
self::High => 3,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum InvoicePaymentMethodEnum: string
|
||||||
|
{
|
||||||
|
use EnumHasOptions;
|
||||||
|
|
||||||
|
case Wallet = 'کیف پول';
|
||||||
|
case Gateway = 'درگاه پرداخت';
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum InvoiceStatusEnum: string
|
||||||
|
{
|
||||||
|
use EnumHasOptions;
|
||||||
|
|
||||||
|
case Pending = 'در انتظار پرداخت';
|
||||||
|
case Paid = 'پرداخت شده';
|
||||||
|
case Canceled = 'لغو شده';
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum PaymentPurposeEnum: string
|
||||||
|
{
|
||||||
|
use EnumHasOptions;
|
||||||
|
|
||||||
|
case Invoice = 'پرداخت فاکتور';
|
||||||
|
case WalletTopUp = 'شارژ کیف پول';
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum PaymentStatusEnum: string
|
||||||
|
{
|
||||||
|
use EnumHasOptions;
|
||||||
|
|
||||||
|
case Pending = 'در انتظار پرداخت';
|
||||||
|
case Paid = 'پرداخت شده';
|
||||||
|
case Failed = 'ناموفق';
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum WalletTransactionReasonEnum: string
|
||||||
|
{
|
||||||
|
use EnumHasOptions;
|
||||||
|
|
||||||
|
case TopUp = 'شارژ کیف پول';
|
||||||
|
case Commission = 'کمیسیون پزشک';
|
||||||
|
case InvoicePayment = 'پرداخت فاکتور';
|
||||||
|
case Refund = 'بازگشت وجه';
|
||||||
|
case Withdraw = 'برداشت وجه';
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum WalletTransactionTypeEnum: string
|
||||||
|
{
|
||||||
|
use EnumHasOptions;
|
||||||
|
|
||||||
|
case Deposit = 'واریز';
|
||||||
|
case Withdrawal = 'برداشت';
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum WithdrawRequestStatusEnum: string
|
||||||
|
{
|
||||||
|
use EnumHasOptions;
|
||||||
|
|
||||||
|
case Pending = 'در انتظار بررسی';
|
||||||
|
case Approved = 'تایید شده';
|
||||||
|
case Rejected = 'رد شده';
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Enums\WithdrawRequestStatusEnum;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\WithdrawRejectRequest;
|
||||||
|
use App\Http\Resources\Admin\WithdrawRequestResource;
|
||||||
|
use App\Models\WithdrawRequest;
|
||||||
|
use App\Services\Wallet\WalletService;
|
||||||
|
use App\Services\Wallet\WithdrawRequestService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class WithdrawRequestController extends Controller
|
||||||
|
{
|
||||||
|
protected WithdrawRequestService $withdrawRequestService;
|
||||||
|
|
||||||
|
protected WalletService $walletService;
|
||||||
|
|
||||||
|
public function __construct(WithdrawRequestService $withdrawRequestService, WalletService $walletService)
|
||||||
|
{
|
||||||
|
$this->withdrawRequestService = $withdrawRequestService;
|
||||||
|
$this->walletService = $walletService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$lists = WithdrawRequest::with(['user', 'bankCard', 'reviewedBy'])
|
||||||
|
->when(isset($request->search), function ($query) use ($request) {
|
||||||
|
$query->whereHas('user', function ($userQuery) use ($request) {
|
||||||
|
$userQuery->where('fullname', 'LIKE', "%{$request->search}%")
|
||||||
|
->orWhere('phone', 'LIKE', "%{$request->search}%")
|
||||||
|
->orWhere('email', 'LIKE', "%{$request->search}%");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when(isset($request->status) && $request->status != 'همه وضعیت ها', function ($query) use ($request) {
|
||||||
|
$query->where('status', $request->status);
|
||||||
|
})
|
||||||
|
->latest()
|
||||||
|
->paginate(20)
|
||||||
|
->withQueryString()
|
||||||
|
->onEachSide(1);
|
||||||
|
|
||||||
|
$pending = WithdrawRequest::where('status', WithdrawRequestStatusEnum::Pending->value);
|
||||||
|
|
||||||
|
return Inertia::admin('WithdrawRequests/index', [
|
||||||
|
'withdrawRequests' => WithdrawRequestResource::collection($lists),
|
||||||
|
'stats' => [
|
||||||
|
'pending_count' => (clone $pending)->count(),
|
||||||
|
'pending_amount' => (int) (clone $pending)->sum('amount'),
|
||||||
|
'system_wallet_balance' => $this->walletService->system()->balance,
|
||||||
|
],
|
||||||
|
'search' => $request->search,
|
||||||
|
'status' => $request->status,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approve(WithdrawRequest $withdrawRequest)
|
||||||
|
{
|
||||||
|
$this->withdrawRequestService->approve($withdrawRequest, Auth::user());
|
||||||
|
|
||||||
|
session()->flash('success', 'درخواست برداشت تایید شد و مبلغ به ولت سایت منتقل شد.');
|
||||||
|
|
||||||
|
return response(null, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reject(WithdrawRejectRequest $request, WithdrawRequest $withdrawRequest)
|
||||||
|
{
|
||||||
|
$this->withdrawRequestService->reject($withdrawRequest, Auth::user(), $request->validated('note'));
|
||||||
|
|
||||||
|
session()->flash('success', 'درخواست برداشت رد شد.');
|
||||||
|
|
||||||
|
return response(null, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,10 @@
|
|||||||
namespace App\Http\Controllers\Client\Doctor;
|
namespace App\Http\Controllers\Client\Doctor;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Client\CaseRejectRequest;
|
||||||
|
use App\Http\Resources\Client\CaseDetailResource;
|
||||||
use App\Http\Resources\Client\CaseResource;
|
use App\Http\Resources\Client\CaseResource;
|
||||||
|
use App\Models\Document;
|
||||||
use App\Services\Document\CaseService;
|
use App\Services\Document\CaseService;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@@ -28,4 +31,25 @@ class CaseController extends Controller
|
|||||||
'cases' => CaseResource::collection($cases),
|
'cases' => CaseResource::collection($cases),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function show(Document $case)
|
||||||
|
{
|
||||||
|
abort_unless($this->caseService->canAccess($case, Auth::user()), 403);
|
||||||
|
|
||||||
|
$case->load(['pet', 'requester', 'media', 'reports']);
|
||||||
|
|
||||||
|
return Inertia::client('Dashboard/Cases/show', [
|
||||||
|
'case' => (new CaseDetailResource($case))->resolve(),
|
||||||
|
'canReport' => $this->caseService->canReport($case, Auth::user()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reject(CaseRejectRequest $request, Document $case)
|
||||||
|
{
|
||||||
|
$this->caseService->reject($case, Auth::user(), $request->validated('reason'));
|
||||||
|
|
||||||
|
session()->flash('success', 'درخواست رد شد و مبلغ فاکتور به کیف پول درخواستکننده بازگردانده شد.');
|
||||||
|
|
||||||
|
return redirect()->route('dashboard.cases.index');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class DashboardController extends Controller
|
|||||||
|
|
||||||
return Inertia::client('Dashboard/Dashboard', [
|
return Inertia::client('Dashboard/Dashboard', [
|
||||||
'cases' => CaseResource::collection($cases),
|
'cases' => CaseResource::collection($cases),
|
||||||
|
'stats' => $this->caseService->statsForUser(Auth::user()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ class DocumentController extends Controller
|
|||||||
|
|
||||||
public function show(Document $document)
|
public function show(Document $document)
|
||||||
{
|
{
|
||||||
return Inertia::client('Dashboard/Documents/show', [
|
return redirect()->route('dashboard.cases.show', $document);
|
||||||
'document' => $document,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,53 @@
|
|||||||
namespace App\Http\Controllers\Client\Doctor;
|
namespace App\Http\Controllers\Client\Doctor;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use App\Http\Requests\Client\ReportSubmitRequest;
|
||||||
|
use App\Http\Resources\Client\ReportResource;
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Models\Report;
|
||||||
|
use App\Services\Document\CaseService;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
class ReportController extends Controller
|
class ReportController extends Controller
|
||||||
{
|
{
|
||||||
|
protected CaseService $caseService;
|
||||||
|
|
||||||
|
public function __construct(CaseService $caseService)
|
||||||
|
{
|
||||||
|
$this->caseService = $caseService;
|
||||||
|
}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
return Inertia::client('Dashboard/Reports/Index');
|
$reports = Report::with(['document', 'doctor'])
|
||||||
|
->where(function ($query) {
|
||||||
|
$query->where('doctor_id', Auth::id())
|
||||||
|
->orWhereHas('document', fn ($documentQuery) => $documentQuery->where('requester_id', Auth::id()));
|
||||||
|
})
|
||||||
|
->latest('submitted_at')
|
||||||
|
->paginate(10)
|
||||||
|
->withQueryString()
|
||||||
|
->onEachSide(1);
|
||||||
|
|
||||||
|
return Inertia::client('Dashboard/Reports/Index', [
|
||||||
|
'reports' => ReportResource::collection($reports),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(ReportSubmitRequest $request)
|
||||||
|
{
|
||||||
|
$document = Document::findOrFail($request->validated('document_id'));
|
||||||
|
|
||||||
|
$this->caseService->submitReport(
|
||||||
|
$document,
|
||||||
|
Auth::user(),
|
||||||
|
$request->validated(),
|
||||||
|
$request->file('files', [])
|
||||||
|
);
|
||||||
|
|
||||||
|
session()->flash('success', 'گزارش با موفقیت ثبت شد.');
|
||||||
|
|
||||||
|
return redirect()->route('dashboard.cases.index');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class FastRequestController extends Controller
|
|||||||
);
|
);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'document' => new FastRequestResource($document->refresh()->load('pet')),
|
'document' => new FastRequestResource($document->refresh()->load(['pet', 'invoice'])),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Client;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Resources\Client\InvoiceResource;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Services\Wallet\WalletService;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class InvoiceController extends Controller
|
||||||
|
{
|
||||||
|
protected WalletService $walletService;
|
||||||
|
|
||||||
|
public function __construct(WalletService $walletService)
|
||||||
|
{
|
||||||
|
$this->walletService = $walletService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$invoices = Auth::user()->invoices()
|
||||||
|
->with('document')
|
||||||
|
->latest()
|
||||||
|
->paginate(10)
|
||||||
|
->withQueryString()
|
||||||
|
->onEachSide(1);
|
||||||
|
|
||||||
|
return Inertia::client('Dashboard/Invoices/Index', [
|
||||||
|
'invoices' => InvoiceResource::collection($invoices),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function payWithWallet(Invoice $invoice)
|
||||||
|
{
|
||||||
|
abort_unless($invoice->user_id === Auth::id(), 403);
|
||||||
|
|
||||||
|
$invoice = $this->walletService->payInvoice(Auth::user(), $invoice);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'invoice' => new InvoiceResource($invoice),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Client;
|
||||||
|
|
||||||
|
use App\Enums\InvoicePaymentMethodEnum;
|
||||||
|
use App\Enums\InvoiceStatusEnum;
|
||||||
|
use App\Enums\PaymentPurposeEnum;
|
||||||
|
use App\Enums\PaymentStatusEnum;
|
||||||
|
use App\Enums\WalletTransactionReasonEnum;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Client\PaymentRequest;
|
||||||
|
use App\Http\Resources\Client\PaymentResource;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Services\Invoice\InvoiceService;
|
||||||
|
use App\Services\Payment\ZibalPaymentService;
|
||||||
|
use App\Services\Wallet\WalletService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class PaymentController extends Controller
|
||||||
|
{
|
||||||
|
protected ZibalPaymentService $zibalPaymentService;
|
||||||
|
|
||||||
|
protected WalletService $walletService;
|
||||||
|
|
||||||
|
protected InvoiceService $invoiceService;
|
||||||
|
|
||||||
|
public function __construct(ZibalPaymentService $zibalPaymentService, WalletService $walletService, InvoiceService $invoiceService)
|
||||||
|
{
|
||||||
|
$this->zibalPaymentService = $zibalPaymentService;
|
||||||
|
$this->walletService = $walletService;
|
||||||
|
$this->invoiceService = $invoiceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(PaymentRequest $request)
|
||||||
|
{
|
||||||
|
$invoice = Invoice::where('id', $request->invoice_id)
|
||||||
|
->where('user_id', Auth::id())
|
||||||
|
->where('status', InvoiceStatusEnum::Pending->value)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$payment = $this->zibalPaymentService->request(Auth::user(), $invoice->amount, $invoice->number, [
|
||||||
|
'purpose' => PaymentPurposeEnum::Invoice->value,
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'payment' => new PaymentResource($payment),
|
||||||
|
'url' => $this->zibalPaymentService->paymentUrl($payment),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function callback(Request $request)
|
||||||
|
{
|
||||||
|
$payment = Payment::where('track_id', $request->trackId)->first();
|
||||||
|
|
||||||
|
if (!$payment) {
|
||||||
|
return redirect()->route('dashboard.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
$wasPending = $payment->status === PaymentStatusEnum::Pending;
|
||||||
|
|
||||||
|
if ($request->success == 1) {
|
||||||
|
$payment = $this->zibalPaymentService->verify($payment);
|
||||||
|
} else {
|
||||||
|
$payment = $this->zibalPaymentService->markFailed($payment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($wasPending && $payment->status === PaymentStatusEnum::Paid) {
|
||||||
|
$this->settle($payment);
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->flash('payment_result', [
|
||||||
|
'status' => $payment->status === PaymentStatusEnum::Paid ? 'paid' : 'failed',
|
||||||
|
'track_id' => $payment->track_id,
|
||||||
|
'purpose' => $payment->purpose->name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('dashboard.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function settle(Payment $payment): void
|
||||||
|
{
|
||||||
|
if ($payment->purpose === PaymentPurposeEnum::WalletTopUp) {
|
||||||
|
$this->walletService->deposit($payment->payer, $payment->amount, WalletTransactionReasonEnum::TopUp, $payment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoice = $payment->invoice;
|
||||||
|
|
||||||
|
if ($invoice && $invoice->status === InvoiceStatusEnum::Pending) {
|
||||||
|
$this->invoiceService->markPaid($invoice, InvoicePaymentMethodEnum::Gateway);
|
||||||
|
$this->walletService->recordGatewayInvoicePayment($invoice, $payment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Client;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Client\ProfileUpdateRequest;
|
||||||
|
use App\Models\Medium;
|
||||||
|
use App\Services\FileStorageService;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class ProfileController extends Controller
|
||||||
|
{
|
||||||
|
protected FileStorageService $fileStorageService;
|
||||||
|
|
||||||
|
public function __construct(FileStorageService $fileStorageService)
|
||||||
|
{
|
||||||
|
$this->fileStorageService = $fileStorageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(ProfileUpdateRequest $request)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
$data = $request->safe()->except('avatar');
|
||||||
|
|
||||||
|
if ($request->hasFile('avatar')) {
|
||||||
|
if (!empty($user->profile_url)) {
|
||||||
|
if ($medium = Medium::where('url', $user->profile_url)->first()) {
|
||||||
|
$this->fileStorageService->deleteMedium($medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$medium = $this->fileStorageService->storeFile($request->file('avatar'), 'public', 'profile');
|
||||||
|
$data['profile_url'] = $medium->url;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->update($data);
|
||||||
|
|
||||||
|
session()->flash('success', 'حساب کاربری شما با موفقیت ویرایش شد.');
|
||||||
|
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Client;
|
||||||
|
|
||||||
|
use App\Enums\PaymentPurposeEnum;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Client\BankCardStoreRequest;
|
||||||
|
use App\Http\Requests\Client\WalletDepositRequest;
|
||||||
|
use App\Http\Requests\Client\WithdrawRequestStoreRequest;
|
||||||
|
use App\Http\Resources\Client\BankCardResource;
|
||||||
|
use App\Http\Resources\Client\PaymentResource;
|
||||||
|
use App\Http\Resources\Client\WalletTransactionResource;
|
||||||
|
use App\Http\Resources\Client\WithdrawRequestResource;
|
||||||
|
use App\Models\BankCard;
|
||||||
|
use App\Services\Payment\ZibalPaymentService;
|
||||||
|
use App\Services\Wallet\WalletService;
|
||||||
|
use App\Services\Wallet\WithdrawRequestService;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class WalletController extends Controller
|
||||||
|
{
|
||||||
|
protected WalletService $walletService;
|
||||||
|
|
||||||
|
protected ZibalPaymentService $zibalPaymentService;
|
||||||
|
|
||||||
|
protected WithdrawRequestService $withdrawRequestService;
|
||||||
|
|
||||||
|
public function __construct(WalletService $walletService, ZibalPaymentService $zibalPaymentService, WithdrawRequestService $withdrawRequestService)
|
||||||
|
{
|
||||||
|
$this->walletService = $walletService;
|
||||||
|
$this->zibalPaymentService = $zibalPaymentService;
|
||||||
|
$this->withdrawRequestService = $withdrawRequestService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$wallet = $this->walletService->forUser(Auth::user());
|
||||||
|
|
||||||
|
$transactions = $wallet->transactions()
|
||||||
|
->with('invoice')
|
||||||
|
->latest()
|
||||||
|
->paginate(10)
|
||||||
|
->withQueryString()
|
||||||
|
->onEachSide(1);
|
||||||
|
|
||||||
|
$withdrawRequests = Auth::user()->withdrawRequests()
|
||||||
|
->with('bankCard')
|
||||||
|
->latest()
|
||||||
|
->take(10)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return Inertia::client('Dashboard/Wallet/Index', [
|
||||||
|
'transactions' => WalletTransactionResource::collection($transactions),
|
||||||
|
'withdrawRequests' => WithdrawRequestResource::collection($withdrawRequests)->resolve(),
|
||||||
|
'bankCards' => BankCardResource::collection(Auth::user()->bankCards()->latest()->get())->resolve(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deposit(WalletDepositRequest $request)
|
||||||
|
{
|
||||||
|
$payment = $this->zibalPaymentService->request(
|
||||||
|
Auth::user(),
|
||||||
|
$request->validated('amount'),
|
||||||
|
'wallet-' . Auth::id() . '-' . now()->timestamp,
|
||||||
|
['purpose' => PaymentPurposeEnum::WalletTopUp->value]
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'payment' => new PaymentResource($payment),
|
||||||
|
'url' => $this->zibalPaymentService->paymentUrl($payment),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeWithdrawRequest(WithdrawRequestStoreRequest $request)
|
||||||
|
{
|
||||||
|
$bankCard = BankCard::findOrFail($request->validated('bank_card_id'));
|
||||||
|
|
||||||
|
$this->withdrawRequestService->request(Auth::user(), $request->validated('amount'), $bankCard);
|
||||||
|
|
||||||
|
session()->flash('success', 'درخواست برداشت شما ثبت شد و در انتظار بررسی است.');
|
||||||
|
|
||||||
|
return redirect()->route('dashboard.wallet.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeBankCard(BankCardStoreRequest $request)
|
||||||
|
{
|
||||||
|
Auth::user()->bankCards()->create($request->validated());
|
||||||
|
|
||||||
|
session()->flash('success', 'کارت بانکی با موفقیت ثبت شد.');
|
||||||
|
|
||||||
|
return redirect()->route('dashboard.wallet.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyBankCard(BankCard $bankCard)
|
||||||
|
{
|
||||||
|
abort_unless($bankCard->user_id === Auth::id(), 403);
|
||||||
|
|
||||||
|
$bankCard->delete();
|
||||||
|
|
||||||
|
return redirect()->route('dashboard.wallet.index');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class WithdrawRejectRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'note' => ['nullable', 'string', 'max:1000'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Client;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class BankCardStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'card_number' => ['required', 'digits:16'],
|
||||||
|
'holder_name' => ['required', 'string', 'max:255'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Client;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class CaseRejectRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'reason' => ['required', 'string', 'max:1000'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Client;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class PaymentRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'invoice_id' => ['required', 'integer', 'exists:invoices,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Client;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class ProfileUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'fullname' => ['required', 'string', 'max:255'],
|
||||||
|
'birthdate' => ['nullable', 'date'],
|
||||||
|
'national_code' => ['required', 'string', 'max:20'],
|
||||||
|
'email' => ['nullable', 'email', 'max:255'],
|
||||||
|
'province' => ['nullable', 'string', 'max:255'],
|
||||||
|
'city' => ['nullable', 'string', 'max:255'],
|
||||||
|
'address' => ['nullable', 'string', 'max:500'],
|
||||||
|
'avatar' => ['nullable', 'image', 'mimes:png,jpg,jpeg', 'max:10240'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Client;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class ReportSubmitRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'document_id' => ['required', 'exists:documents,id'],
|
||||||
|
'technique' => ['nullable', 'string', 'max:10000'],
|
||||||
|
'finding' => ['required', 'string', 'max:10000'],
|
||||||
|
'diagnose' => ['nullable', 'string', 'max:10000'],
|
||||||
|
'recommendation' => ['nullable', 'string', 'max:10000'],
|
||||||
|
'files' => ['nullable', 'array'],
|
||||||
|
'files.*' => ['file', 'image', 'max:10240'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Client;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class WalletDepositRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => ['required', 'integer', 'min:10000', 'max:500000000'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Client;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class WithdrawRequestStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => ['required', 'integer', 'min:10000', 'max:500000000'],
|
||||||
|
'bank_card_id' => ['required', 'exists:bank_cards,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class WithdrawRequestResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'amount' => $this->amount,
|
||||||
|
'status' => $this->status,
|
||||||
|
'admin_note' => $this->admin_note,
|
||||||
|
'user' => $this->relationLoaded('user') ? $this->user->only(['id', 'fullname', 'level', 'phone']) : null,
|
||||||
|
'bank_card' => $this->relationLoaded('bankCard') && $this->bankCard
|
||||||
|
? $this->bankCard->only(['id', 'card_number', 'holder_name'])
|
||||||
|
: null,
|
||||||
|
'reviewed_by' => $this->relationLoaded('reviewedBy') && $this->reviewedBy ? $this->reviewedBy->only(['id', 'fullname']) : null,
|
||||||
|
'created_at' => verta($this->created_at)->format('%Y/%m/%d - H:i'),
|
||||||
|
'reviewed_at' => $this->reviewed_at ? verta($this->reviewed_at)->format('%Y/%m/%d - H:i') : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Client;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class BankCardResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'card_number' => $this->card_number,
|
||||||
|
'holder_name' => $this->holder_name,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Client;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class CaseDetailResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
$report = $this->reports->sortByDesc('submitted_at')->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'code' => $this->code,
|
||||||
|
'status' => $this->status,
|
||||||
|
'modality' => $this->modality,
|
||||||
|
'priority' => $this->priority,
|
||||||
|
'note' => $this->note,
|
||||||
|
'rejection_reason' => $this->rejection_reason,
|
||||||
|
'submitted_at' => $this->submitted_at ? verta($this->submitted_at)->format('%Y/%m/%d - H:i') : null,
|
||||||
|
'processing_deadline' => $this->processing_deadline?->toIso8601String(),
|
||||||
|
'pet' => $this->whenLoaded('pet', fn () => [
|
||||||
|
'name' => $this->pet->name,
|
||||||
|
'type' => $this->pet->type,
|
||||||
|
'gender' => $this->pet->gender,
|
||||||
|
'breed' => $this->pet->breed,
|
||||||
|
'age' => $this->pet->birthday ? (int) $this->pet->birthday->diffInYears(now()) : null,
|
||||||
|
'weight' => $this->pet->weight,
|
||||||
|
'sterilized' => $this->pet->sterilized,
|
||||||
|
]),
|
||||||
|
'requester' => $this->whenLoaded('requester', fn () => [
|
||||||
|
'level' => $this->requester->level,
|
||||||
|
'fullname' => $this->requester->fullname,
|
||||||
|
'phone' => $this->requester->phone,
|
||||||
|
'specialty' => $this->requester->specialty,
|
||||||
|
'clinic_name' => $this->requester->clinic_name,
|
||||||
|
'clinic_phone' => $this->requester->clinic_phone,
|
||||||
|
'province' => $this->requester->province,
|
||||||
|
'city' => $this->requester->city,
|
||||||
|
]),
|
||||||
|
'primary_files' => $this->mediaFiles('primary'),
|
||||||
|
'history_files' => $this->mediaFiles('history'),
|
||||||
|
'report' => $report ? [
|
||||||
|
'id' => $report->id,
|
||||||
|
'status' => $report->status,
|
||||||
|
'technique' => $report->technique,
|
||||||
|
'finding' => $report->finding,
|
||||||
|
'diagnose' => $report->diagnose,
|
||||||
|
'recommendation' => $report->recommendation,
|
||||||
|
'submitted_at' => $report->submitted_at ? verta($report->submitted_at)->format('%Y/%m/%d - H:i') : null,
|
||||||
|
'attachments' => $report->attachments->map(fn ($medium) => $this->mediumPayload($medium))->values(),
|
||||||
|
] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function mediaFiles(string $collection): array
|
||||||
|
{
|
||||||
|
return $this->media
|
||||||
|
->filter(fn ($medium) => $medium->pivot->collection === $collection)
|
||||||
|
->map(fn ($medium) => $this->mediumPayload($medium))
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function mediumPayload($medium): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $medium->id,
|
||||||
|
'filename' => $medium->filename,
|
||||||
|
'url' => $medium->url,
|
||||||
|
'mime_type' => $medium->mime_type,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Resources\Client;
|
namespace App\Http\Resources\Client;
|
||||||
|
|
||||||
use App\Enums\DocumentModalityEnum;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
@@ -17,10 +16,7 @@ class FastRequestResource extends JsonResource
|
|||||||
'status' => $this->status,
|
'status' => $this->status,
|
||||||
'priority' => $this->priority,
|
'priority' => $this->priority,
|
||||||
'pet' => new PetArchiveResource($this->whenLoaded('pet')),
|
'pet' => new PetArchiveResource($this->whenLoaded('pet')),
|
||||||
'invoice' => [
|
'invoice' => new InvoiceResource($this->whenLoaded('invoice')),
|
||||||
'number' => str_pad((string) $this->id, 5, '0', STR_PAD_LEFT),
|
|
||||||
'amount' => DocumentModalityEnum::from($this->modality)->price(),
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Client;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class InvoiceResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'number' => $this->number,
|
||||||
|
'amount' => $this->amount,
|
||||||
|
'status' => $this->status,
|
||||||
|
'payment_method' => $this->payment_method,
|
||||||
|
'paid_at' => $this->paid_at,
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'document' => $this->whenLoaded('document', fn () => [
|
||||||
|
'id' => $this->document->id,
|
||||||
|
'code' => $this->document->code,
|
||||||
|
'modality' => $this->document->modality,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Client;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class PaymentResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'track_id' => $this->track_id,
|
||||||
|
'amount' => $this->amount,
|
||||||
|
'status' => $this->status,
|
||||||
|
'ref_number' => $this->ref_number,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Client;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class ReportResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'status' => $this->status,
|
||||||
|
'submitted_at' => $this->submitted_at ? verta($this->submitted_at)->format('%Y/%m/%d - H:i') : null,
|
||||||
|
'document' => $this->whenLoaded('document', fn () => [
|
||||||
|
'id' => $this->document->id,
|
||||||
|
'code' => $this->document->code,
|
||||||
|
'modality' => $this->document->modality,
|
||||||
|
'status' => $this->document->status,
|
||||||
|
]),
|
||||||
|
'doctor' => $this->whenLoaded('doctor', fn () => optional($this->doctor)->fullname),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Client;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class WalletTransactionResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'type' => $this->type,
|
||||||
|
'reason' => $this->reason,
|
||||||
|
'amount' => $this->amount,
|
||||||
|
'balance_after' => $this->balance_after,
|
||||||
|
'description' => $this->description,
|
||||||
|
'via_gateway' => $this->payment_id !== null,
|
||||||
|
'invoice_number' => $this->whenLoaded('invoice', fn () => $this->invoice?->number),
|
||||||
|
'created_at' => verta($this->created_at)->format('%Y/%m/%d - H:i'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Client;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class WithdrawRequestResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'amount' => $this->amount,
|
||||||
|
'status' => $this->status,
|
||||||
|
'admin_note' => $this->admin_note,
|
||||||
|
'bank_card' => $this->relationLoaded('bankCard') && $this->bankCard
|
||||||
|
? $this->bankCard->only(['id', 'card_number', 'holder_name'])
|
||||||
|
: null,
|
||||||
|
'created_at' => verta($this->created_at)->format('%Y/%m/%d - H:i'),
|
||||||
|
'reviewed_at' => $this->reviewed_at ? verta($this->reviewed_at)->format('%Y/%m/%d - H:i') : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class BankCard extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'card_number',
|
||||||
|
'holder_name',
|
||||||
|
'user_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'user_id' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,11 @@ class Document extends Model
|
|||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function invoice()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Invoice::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function assignments()
|
public function assignments()
|
||||||
{
|
{
|
||||||
return $this->hasMany(DocumentAssignment::class);
|
return $this->hasMany(DocumentAssignment::class);
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\InvoicePaymentMethodEnum;
|
||||||
|
use App\Enums\InvoiceStatusEnum;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Invoice extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'number',
|
||||||
|
'amount',
|
||||||
|
'status',
|
||||||
|
'payment_method',
|
||||||
|
'paid_at',
|
||||||
|
'document_id',
|
||||||
|
'user_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'status' => InvoiceStatusEnum::class,
|
||||||
|
'payment_method' => InvoicePaymentMethodEnum::class,
|
||||||
|
'amount' => 'integer',
|
||||||
|
'paid_at' => 'datetime',
|
||||||
|
'document_id' => 'integer',
|
||||||
|
'user_id' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function document()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Document::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function payments()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Payment::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\PaymentPurposeEnum;
|
||||||
|
use App\Enums\PaymentStatusEnum;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Payment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'track_id',
|
||||||
|
'amount',
|
||||||
|
'status',
|
||||||
|
'purpose',
|
||||||
|
'ref_number',
|
||||||
|
'verified_at',
|
||||||
|
'invoice_id',
|
||||||
|
'payer_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'status' => PaymentStatusEnum::class,
|
||||||
|
'purpose' => PaymentPurposeEnum::class,
|
||||||
|
'amount' => 'integer',
|
||||||
|
'verified_at' => 'datetime',
|
||||||
|
'invoice_id' => 'integer',
|
||||||
|
'payer_id' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function invoice()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Invoice::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function payer()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'payer_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,10 @@ class Report extends Model
|
|||||||
'medium_id',
|
'medium_id',
|
||||||
'status',
|
'status',
|
||||||
'body',
|
'body',
|
||||||
|
'technique',
|
||||||
|
'finding',
|
||||||
|
'diagnose',
|
||||||
|
'recommendation',
|
||||||
'submitted_at',
|
'submitted_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -43,7 +47,7 @@ class Report extends Model
|
|||||||
'media_assignments',
|
'media_assignments',
|
||||||
'mediable_id',
|
'mediable_id',
|
||||||
'media_id'
|
'media_id'
|
||||||
)->withPivot('collection')->withTimestamps();
|
)->withPivot('collection');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function medium()
|
public function medium()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class User extends Authenticatable
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'username',
|
'username',
|
||||||
'fullname',
|
'fullname',
|
||||||
|
'birthdate',
|
||||||
'national_code',
|
'national_code',
|
||||||
'level',
|
'level',
|
||||||
'email',
|
'email',
|
||||||
@@ -56,6 +57,7 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
|
'birthdate' => 'date:Y-m-d',
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -76,4 +78,24 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
return $this->hasMany(Shift::class);
|
return $this->hasMany(Shift::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function wallet()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Wallet::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invoices()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Invoice::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bankCards()
|
||||||
|
{
|
||||||
|
return $this->hasMany(BankCard::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withdrawRequests()
|
||||||
|
{
|
||||||
|
return $this->hasMany(WithdrawRequest::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Wallet extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'balance',
|
||||||
|
'user_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'balance' => 'integer',
|
||||||
|
'user_id' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transactions()
|
||||||
|
{
|
||||||
|
return $this->hasMany(WalletTransaction::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\WalletTransactionReasonEnum;
|
||||||
|
use App\Enums\WalletTransactionTypeEnum;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class WalletTransaction extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'type',
|
||||||
|
'reason',
|
||||||
|
'amount',
|
||||||
|
'balance_after',
|
||||||
|
'description',
|
||||||
|
'wallet_id',
|
||||||
|
'invoice_id',
|
||||||
|
'payment_id',
|
||||||
|
'withdraw_request_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'type' => WalletTransactionTypeEnum::class,
|
||||||
|
'reason' => WalletTransactionReasonEnum::class,
|
||||||
|
'amount' => 'integer',
|
||||||
|
'balance_after' => 'integer',
|
||||||
|
'wallet_id' => 'integer',
|
||||||
|
'invoice_id' => 'integer',
|
||||||
|
'payment_id' => 'integer',
|
||||||
|
'withdraw_request_id' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function wallet()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Wallet::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invoice()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Invoice::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function payment()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Payment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withdrawRequest()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(WithdrawRequest::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\WithdrawRequestStatusEnum;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class WithdrawRequest extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'amount',
|
||||||
|
'status',
|
||||||
|
'admin_note',
|
||||||
|
'reviewed_at',
|
||||||
|
'reviewed_by',
|
||||||
|
'user_id',
|
||||||
|
'bank_card_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'status' => WithdrawRequestStatusEnum::class,
|
||||||
|
'amount' => 'integer',
|
||||||
|
'reviewed_at' => 'datetime',
|
||||||
|
'reviewed_by' => 'integer',
|
||||||
|
'user_id' => 'integer',
|
||||||
|
'bank_card_id' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bankCard()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(BankCard::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reviewedBy()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'reviewed_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
return [
|
return [
|
||||||
'User' => $user,
|
'User' => $user,
|
||||||
'permissions' => $permissions,
|
'permissions' => $permissions,
|
||||||
|
'wallet_balance' => (int) ($user?->wallet?->balance ?? 0),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -52,6 +53,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
return [
|
return [
|
||||||
'success' => Session::get('success'),
|
'success' => Session::get('success'),
|
||||||
'error' => Session::get('error'),
|
'error' => Session::get('error'),
|
||||||
|
'payment_result' => Session::get('payment_result'),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,167 @@
|
|||||||
namespace App\Services\Document;
|
namespace App\Services\Document;
|
||||||
|
|
||||||
use App\Enums\DocumentStatusEnum;
|
use App\Enums\DocumentStatusEnum;
|
||||||
|
use App\Enums\InvoiceStatusEnum;
|
||||||
|
use App\Enums\MediaCollectionEnum;
|
||||||
|
use App\Enums\ReportStatusEnum;
|
||||||
|
use App\Enums\UserEnum;
|
||||||
|
use App\Enums\WalletTransactionReasonEnum;
|
||||||
use App\Models\Document;
|
use App\Models\Document;
|
||||||
|
use App\Models\Report;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\FileStorageService;
|
||||||
|
use App\Services\Shift\ShiftScheduleService;
|
||||||
|
use App\Services\Wallet\WalletService;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class CaseService
|
class CaseService
|
||||||
{
|
{
|
||||||
|
protected FileStorageService $fileStorageService;
|
||||||
|
|
||||||
|
protected WalletService $walletService;
|
||||||
|
|
||||||
|
protected ShiftScheduleService $shiftScheduleService;
|
||||||
|
|
||||||
|
public function __construct(FileStorageService $fileStorageService, WalletService $walletService, ShiftScheduleService $shiftScheduleService)
|
||||||
|
{
|
||||||
|
$this->fileStorageService = $fileStorageService;
|
||||||
|
$this->walletService = $walletService;
|
||||||
|
$this->shiftScheduleService = $shiftScheduleService;
|
||||||
|
}
|
||||||
|
|
||||||
public function pendingForUser(User $user): Builder
|
public function pendingForUser(User $user): Builder
|
||||||
{
|
{
|
||||||
|
$statuses = $this->isDoctor($user)
|
||||||
|
? [DocumentStatusEnum::InProgress->value]
|
||||||
|
: [DocumentStatusEnum::Registered->value, DocumentStatusEnum::InProgress->value];
|
||||||
|
|
||||||
return Document::with(['pet', 'requester'])
|
return Document::with(['pet', 'requester'])
|
||||||
->whereIn('status', [
|
->whereIn('status', $statuses)
|
||||||
DocumentStatusEnum::Registered->value,
|
->where(fn ($query) => $this->scopeVisibleTo($query, $user))
|
||||||
DocumentStatusEnum::InProgress->value,
|
|
||||||
])
|
|
||||||
->where(function ($query) use ($user) {
|
|
||||||
$query->where('requester_id', $user->id)
|
|
||||||
->orWhereHas('assignments', function ($assignmentQuery) use ($user) {
|
|
||||||
$assignmentQuery->where('user_id', $user->id);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
->latest('submitted_at');
|
->latest('submitted_at');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function canAccess(Document $document, User $user): bool
|
||||||
|
{
|
||||||
|
return $document->requester_id === $user->id
|
||||||
|
|| $document->assignments()->where('user_id', $user->id)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canReport(Document $document, User $user): bool
|
||||||
|
{
|
||||||
|
return $this->isDoctor($user)
|
||||||
|
&& $document->status === DocumentStatusEnum::InProgress
|
||||||
|
&& $document->assignments()->where('user_id', $user->id)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function submitReport(Document $document, User $doctor, array $data, array $files = []): Report
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($document, $doctor, $data, $files) {
|
||||||
|
if (!$this->canReport($document, $doctor)) {
|
||||||
|
throw ValidationException::withMessages(['document' => 'امکان ثبت گزارش برای این پرونده وجود ندارد.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = $document->reports()->create([
|
||||||
|
'doctor_id' => $doctor->id,
|
||||||
|
'status' => ReportStatusEnum::Approved->value,
|
||||||
|
'technique' => $data['technique'] ?? null,
|
||||||
|
'finding' => $data['finding'] ?? null,
|
||||||
|
'diagnose' => $data['diagnose'] ?? null,
|
||||||
|
'recommendation' => $data['recommendation'] ?? null,
|
||||||
|
'submitted_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$medium = $this->fileStorageService->storeFile($file);
|
||||||
|
$report->media()->attach($medium->id, ['collection' => MediaCollectionEnum::Attachments->value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$document->update([
|
||||||
|
'status' => DocumentStatusEnum::Completed->value,
|
||||||
|
'finalized_at' => now(),
|
||||||
|
'finalized_by' => $doctor->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $report;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reject(Document $document, User $doctor, string $reason): Document
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($document, $doctor, $reason) {
|
||||||
|
if (!$this->canReport($document, $doctor)) {
|
||||||
|
throw ValidationException::withMessages(['document' => 'امکان رد این پرونده وجود ندارد.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$document->update([
|
||||||
|
'status' => DocumentStatusEnum::Rejected->value,
|
||||||
|
'finalized_at' => now(),
|
||||||
|
'finalized_by' => $doctor->id,
|
||||||
|
'rejection_reason' => $reason,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$invoice = $document->invoice;
|
||||||
|
|
||||||
|
if ($invoice && $invoice->status === InvoiceStatusEnum::Paid) {
|
||||||
|
$invoice->update(['status' => InvoiceStatusEnum::Canceled->value]);
|
||||||
|
$this->walletService->deposit($document->requester, $invoice->amount, WalletTransactionReasonEnum::Refund);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $document;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function statsForUser(User $user): array
|
||||||
|
{
|
||||||
|
$monthStart = now()->startOfMonth();
|
||||||
|
$isDoctor = $this->isDoctor($user);
|
||||||
|
|
||||||
|
$reviewedQuery = Document::where('status', DocumentStatusEnum::Completed->value)
|
||||||
|
->where('finalized_at', '>=', $monthStart);
|
||||||
|
|
||||||
|
if ($isDoctor) {
|
||||||
|
$reviewedQuery->where('finalized_by', $user->id);
|
||||||
|
} else {
|
||||||
|
$reviewedQuery->where('requester_id', $user->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$amountQuery = $isDoctor
|
||||||
|
? Document::where('documents.finalized_by', $user->id)
|
||||||
|
->where('documents.status', DocumentStatusEnum::Completed->value)
|
||||||
|
->where('documents.finalized_at', '>=', $monthStart)
|
||||||
|
->join('invoices', 'invoices.document_id', '=', 'documents.id')
|
||||||
|
->where('invoices.status', InvoiceStatusEnum::Paid->value)
|
||||||
|
: Document::where('documents.requester_id', $user->id)
|
||||||
|
->join('invoices', 'invoices.document_id', '=', 'documents.id')
|
||||||
|
->where('invoices.status', InvoiceStatusEnum::Paid->value)
|
||||||
|
->where('invoices.paid_at', '>=', $monthStart);
|
||||||
|
|
||||||
|
$nextShift = $isDoctor ? $this->shiftScheduleService->nextShiftStart($user, now()) : null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'pending_cases' => $this->pendingForUser($user)->count(),
|
||||||
|
'monthly_reviewed' => $reviewedQuery->count(),
|
||||||
|
'monthly_amount' => (int) $amountQuery->sum('invoices.amount'),
|
||||||
|
'next_shift_hours' => $nextShift ? (int) ceil(now()->diffInMinutes($nextShift, true) / 60) : null,
|
||||||
|
'is_doctor' => $isDoctor,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function scopeVisibleTo($query, User $user): void
|
||||||
|
{
|
||||||
|
if ($this->isDoctor($user)) {
|
||||||
|
$query->whereHas('assignments', fn ($assignmentQuery) => $assignmentQuery->where('user_id', $user->id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->where('requester_id', $user->id)
|
||||||
|
->orWhereHas('assignments', fn ($assignmentQuery) => $assignmentQuery->where('user_id', $user->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isDoctor(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->level === UserEnum::Doctor->value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Models\Document;
|
|||||||
use App\Models\Pet;
|
use App\Models\Pet;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\FileStorageService;
|
use App\Services\FileStorageService;
|
||||||
|
use App\Services\Invoice\InvoiceService;
|
||||||
use App\Services\Shift\ShiftScheduleService;
|
use App\Services\Shift\ShiftScheduleService;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
@@ -19,10 +20,13 @@ class FastRequestService
|
|||||||
|
|
||||||
protected ShiftScheduleService $shiftScheduleService;
|
protected ShiftScheduleService $shiftScheduleService;
|
||||||
|
|
||||||
public function __construct(FileStorageService $fileStorageService, ShiftScheduleService $shiftScheduleService)
|
protected InvoiceService $invoiceService;
|
||||||
|
|
||||||
|
public function __construct(FileStorageService $fileStorageService, ShiftScheduleService $shiftScheduleService, InvoiceService $invoiceService)
|
||||||
{
|
{
|
||||||
$this->fileStorageService = $fileStorageService;
|
$this->fileStorageService = $fileStorageService;
|
||||||
$this->shiftScheduleService = $shiftScheduleService;
|
$this->shiftScheduleService = $shiftScheduleService;
|
||||||
|
$this->invoiceService = $invoiceService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(User $requester, array $data, array $historyFiles = [], array $scanFiles = []): Document
|
public function create(User $requester, array $data, array $historyFiles = [], array $scanFiles = []): Document
|
||||||
@@ -39,6 +43,8 @@ class FastRequestService
|
|||||||
'requester_id' => $requester->id,
|
'requester_id' => $requester->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->invoiceService->createForDocument($document);
|
||||||
|
|
||||||
$this->attachFiles($document, $historyFiles, MediaCollectionEnum::History);
|
$this->attachFiles($document, $historyFiles, MediaCollectionEnum::History);
|
||||||
$this->attachFiles($document, $scanFiles, MediaCollectionEnum::Primary);
|
$this->attachFiles($document, $scanFiles, MediaCollectionEnum::Primary);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Invoice;
|
||||||
|
|
||||||
|
use App\Enums\DocumentModalityEnum;
|
||||||
|
use App\Enums\DocumentStatusEnum;
|
||||||
|
use App\Enums\InvoicePaymentMethodEnum;
|
||||||
|
use App\Enums\InvoiceStatusEnum;
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
|
||||||
|
class InvoiceService
|
||||||
|
{
|
||||||
|
public function createForDocument(Document $document): Invoice
|
||||||
|
{
|
||||||
|
$invoice = Invoice::create([
|
||||||
|
'number' => 'pending',
|
||||||
|
'amount' => DocumentModalityEnum::from($document->modality)->price(),
|
||||||
|
'status' => InvoiceStatusEnum::Pending->value,
|
||||||
|
'document_id' => $document->id,
|
||||||
|
'user_id' => $document->requester_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$invoice->update(['number' => str_pad((string) $invoice->id, 5, '0', STR_PAD_LEFT)]);
|
||||||
|
|
||||||
|
return $invoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markPaid(Invoice $invoice, InvoicePaymentMethodEnum $method): Invoice
|
||||||
|
{
|
||||||
|
$invoice->update([
|
||||||
|
'status' => InvoiceStatusEnum::Paid->value,
|
||||||
|
'payment_method' => $method->value,
|
||||||
|
'paid_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$document = $invoice->document;
|
||||||
|
|
||||||
|
if ($document && $document->status === DocumentStatusEnum::Registered) {
|
||||||
|
$document->update([
|
||||||
|
'status' => DocumentStatusEnum::InProgress->value,
|
||||||
|
'processing_deadline' => now()->addHours($document->priority->processingHours()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $invoice;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Payment;
|
||||||
|
|
||||||
|
use App\Enums\PaymentStatusEnum;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class ZibalPaymentService
|
||||||
|
{
|
||||||
|
protected const API_URL = 'https://gateway.zibal.ir/v1';
|
||||||
|
protected const START_URL = 'https://gateway.zibal.ir/start';
|
||||||
|
|
||||||
|
public function request(User $payer, int $amount, string $orderId, array $attributes = []): Payment
|
||||||
|
{
|
||||||
|
$response = Http::post(self::API_URL . '/request', [
|
||||||
|
'merchant' => $this->merchant(),
|
||||||
|
'amount' => $amount * 10,
|
||||||
|
'callbackUrl' => $this->callbackUrl(),
|
||||||
|
'orderId' => $orderId,
|
||||||
|
])->json();
|
||||||
|
|
||||||
|
if (($response['result'] ?? null) !== 100) {
|
||||||
|
throw new RuntimeException('خطا در ایجاد درخواست پرداخت: ' . ($response['message'] ?? 'پاسخ نامعتبر از درگاه'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Payment::create(array_merge([
|
||||||
|
'track_id' => (string) $response['trackId'],
|
||||||
|
'amount' => $amount,
|
||||||
|
'status' => PaymentStatusEnum::Pending->value,
|
||||||
|
'payer_id' => $payer->id,
|
||||||
|
], $attributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paymentUrl(Payment $payment): string
|
||||||
|
{
|
||||||
|
return self::START_URL . '/' . $payment->track_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verify(Payment $payment): Payment
|
||||||
|
{
|
||||||
|
if ($payment->status !== PaymentStatusEnum::Pending) {
|
||||||
|
return $payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = Http::post(self::API_URL . '/verify', [
|
||||||
|
'merchant' => $this->merchant(),
|
||||||
|
'trackId' => $payment->track_id,
|
||||||
|
])->json();
|
||||||
|
|
||||||
|
if (in_array($response['result'] ?? null, [100, 201], true)) {
|
||||||
|
$payment->update([
|
||||||
|
'status' => PaymentStatusEnum::Paid->value,
|
||||||
|
'ref_number' => $response['refNumber'] ?? null,
|
||||||
|
'verified_at' => now(),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$payment->update(['status' => PaymentStatusEnum::Failed->value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markFailed(Payment $payment): Payment
|
||||||
|
{
|
||||||
|
if ($payment->status === PaymentStatusEnum::Pending) {
|
||||||
|
$payment->update(['status' => PaymentStatusEnum::Failed->value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function merchant(): string
|
||||||
|
{
|
||||||
|
return config('services.zibal.sandbox') ? 'zibal' : config('services.zibal.merchant');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function callbackUrl(): string
|
||||||
|
{
|
||||||
|
return config('services.zibal.callback_url') ?: route('dashboard.payments.callback');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,6 +81,33 @@ class ShiftScheduleService
|
|||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function nextShiftStart(User $doctor, Carbon $from): ?Carbon
|
||||||
|
{
|
||||||
|
$shifts = Shift::active()->where('user_id', $doctor->id)->get();
|
||||||
|
|
||||||
|
return $shifts
|
||||||
|
->map(function (Shift $shift) use ($from) {
|
||||||
|
for ($offset = 0; $offset <= 7; $offset++) {
|
||||||
|
$day = $from->copy()->addDays($offset)->startOfDay();
|
||||||
|
|
||||||
|
if (!$this->coversDate($shift, $day)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = $day->copy()->addMinutes($this->toMinutes($shift->start_time));
|
||||||
|
|
||||||
|
if ($start->greaterThanOrEqualTo($from)) {
|
||||||
|
return $start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->sort()
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
public function hasConflict(array $data, ?int $ignoreId = null): bool
|
public function hasConflict(array $data, ?int $ignoreId = null): bool
|
||||||
{
|
{
|
||||||
if (empty($data['user_id'])) {
|
if (empty($data['user_id'])) {
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Wallet;
|
||||||
|
|
||||||
|
use App\Enums\InvoicePaymentMethodEnum;
|
||||||
|
use App\Enums\InvoiceStatusEnum;
|
||||||
|
use App\Enums\WalletTransactionReasonEnum;
|
||||||
|
use App\Enums\WalletTransactionTypeEnum;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Wallet;
|
||||||
|
use App\Models\WalletTransaction;
|
||||||
|
use App\Models\WithdrawRequest;
|
||||||
|
use App\Services\Invoice\InvoiceService;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class WalletService
|
||||||
|
{
|
||||||
|
protected InvoiceService $invoiceService;
|
||||||
|
|
||||||
|
public function __construct(InvoiceService $invoiceService)
|
||||||
|
{
|
||||||
|
$this->invoiceService = $invoiceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forUser(User $user): Wallet
|
||||||
|
{
|
||||||
|
return Wallet::firstOrCreate(['user_id' => $user->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function system(): Wallet
|
||||||
|
{
|
||||||
|
return Wallet::firstOrCreate(['user_id' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deposit(User $user, int $amount, WalletTransactionReasonEnum $reason, ?Payment $payment = null): WalletTransaction
|
||||||
|
{
|
||||||
|
return $this->depositToWallet($this->forUser($user), $amount, $reason, $payment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function depositToWallet(Wallet $wallet, int $amount, WalletTransactionReasonEnum $reason, ?Payment $payment = null, ?WithdrawRequest $withdrawRequest = null): WalletTransaction
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($wallet, $amount, $reason, $payment, $withdrawRequest) {
|
||||||
|
$wallet = $this->lockedWallet($wallet);
|
||||||
|
$wallet->update(['balance' => $wallet->balance + $amount]);
|
||||||
|
|
||||||
|
return $wallet->transactions()->create([
|
||||||
|
'type' => WalletTransactionTypeEnum::Deposit->value,
|
||||||
|
'reason' => $reason->value,
|
||||||
|
'amount' => $amount,
|
||||||
|
'balance_after' => $wallet->balance,
|
||||||
|
'payment_id' => $payment?->id,
|
||||||
|
'withdraw_request_id' => $withdrawRequest?->id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withdraw(User $user, int $amount, WalletTransactionReasonEnum $reason, ?Invoice $invoice = null, ?WithdrawRequest $withdrawRequest = null): WalletTransaction
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($user, $amount, $reason, $invoice, $withdrawRequest) {
|
||||||
|
$wallet = $this->lockedWallet($this->forUser($user));
|
||||||
|
|
||||||
|
if ($wallet->balance < $amount) {
|
||||||
|
throw ValidationException::withMessages(['amount' => 'موجودی کیف پول کافی نیست']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$wallet->update(['balance' => $wallet->balance - $amount]);
|
||||||
|
|
||||||
|
return $wallet->transactions()->create([
|
||||||
|
'type' => WalletTransactionTypeEnum::Withdrawal->value,
|
||||||
|
'reason' => $reason->value,
|
||||||
|
'amount' => $amount,
|
||||||
|
'balance_after' => $wallet->balance,
|
||||||
|
'invoice_id' => $invoice?->id,
|
||||||
|
'withdraw_request_id' => $withdrawRequest?->id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function payInvoice(User $user, Invoice $invoice): Invoice
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($user, $invoice) {
|
||||||
|
if ($invoice->status !== InvoiceStatusEnum::Pending) {
|
||||||
|
throw ValidationException::withMessages(['invoice' => 'این فاکتور قابل پرداخت نیست']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->withdraw($user, $invoice->amount, WalletTransactionReasonEnum::InvoicePayment, $invoice);
|
||||||
|
|
||||||
|
return $this->invoiceService->markPaid($invoice, InvoicePaymentMethodEnum::Wallet);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function recordGatewayInvoicePayment(Invoice $invoice, Payment $payment): WalletTransaction
|
||||||
|
{
|
||||||
|
$wallet = $this->forUser($invoice->user);
|
||||||
|
|
||||||
|
return $wallet->transactions()->create([
|
||||||
|
'type' => WalletTransactionTypeEnum::Withdrawal->value,
|
||||||
|
'reason' => WalletTransactionReasonEnum::InvoicePayment->value,
|
||||||
|
'amount' => $invoice->amount,
|
||||||
|
'balance_after' => $wallet->balance,
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'payment_id' => $payment->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function lockedWallet(Wallet $wallet): Wallet
|
||||||
|
{
|
||||||
|
return Wallet::whereKey($wallet->id)->lockForUpdate()->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Wallet;
|
||||||
|
|
||||||
|
use App\Enums\WalletTransactionReasonEnum;
|
||||||
|
use App\Enums\WithdrawRequestStatusEnum;
|
||||||
|
use App\Models\BankCard;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\WithdrawRequest;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class WithdrawRequestService
|
||||||
|
{
|
||||||
|
protected WalletService $walletService;
|
||||||
|
|
||||||
|
public function __construct(WalletService $walletService)
|
||||||
|
{
|
||||||
|
$this->walletService = $walletService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function request(User $user, int $amount, BankCard $bankCard): WithdrawRequest
|
||||||
|
{
|
||||||
|
if ($bankCard->user_id !== $user->id) {
|
||||||
|
throw ValidationException::withMessages(['bank_card_id' => 'کارت بانکی انتخابشده معتبر نیست.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$balance = $this->walletService->forUser($user)->balance;
|
||||||
|
|
||||||
|
$pendingAmount = WithdrawRequest::where('user_id', $user->id)
|
||||||
|
->where('status', WithdrawRequestStatusEnum::Pending->value)
|
||||||
|
->sum('amount');
|
||||||
|
|
||||||
|
if ($amount + $pendingAmount > $balance) {
|
||||||
|
throw ValidationException::withMessages(['amount' => 'مجموع مبلغ درخواستهای برداشت از موجودی کیف پول شما بیشتر است.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WithdrawRequest::create([
|
||||||
|
'amount' => $amount,
|
||||||
|
'status' => WithdrawRequestStatusEnum::Pending->value,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'bank_card_id' => $bankCard->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approve(WithdrawRequest $withdrawRequest, User $admin): WithdrawRequest
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($withdrawRequest, $admin) {
|
||||||
|
$this->ensurePending($withdrawRequest);
|
||||||
|
|
||||||
|
$this->walletService->withdraw(
|
||||||
|
$withdrawRequest->user,
|
||||||
|
$withdrawRequest->amount,
|
||||||
|
WalletTransactionReasonEnum::Withdraw,
|
||||||
|
null,
|
||||||
|
$withdrawRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->walletService->depositToWallet(
|
||||||
|
$this->walletService->system(),
|
||||||
|
$withdrawRequest->amount,
|
||||||
|
WalletTransactionReasonEnum::Withdraw,
|
||||||
|
null,
|
||||||
|
$withdrawRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
$withdrawRequest->update([
|
||||||
|
'status' => WithdrawRequestStatusEnum::Approved->value,
|
||||||
|
'reviewed_at' => now(),
|
||||||
|
'reviewed_by' => $admin->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $withdrawRequest;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reject(WithdrawRequest $withdrawRequest, User $admin, ?string $note = null): WithdrawRequest
|
||||||
|
{
|
||||||
|
$this->ensurePending($withdrawRequest);
|
||||||
|
|
||||||
|
$withdrawRequest->update([
|
||||||
|
'status' => WithdrawRequestStatusEnum::Rejected->value,
|
||||||
|
'admin_note' => $note,
|
||||||
|
'reviewed_at' => now(),
|
||||||
|
'reviewed_by' => $admin->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $withdrawRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function ensurePending(WithdrawRequest $withdrawRequest): void
|
||||||
|
{
|
||||||
|
if ($withdrawRequest->status !== WithdrawRequestStatusEnum::Pending) {
|
||||||
|
throw ValidationException::withMessages(['withdraw_request' => 'این درخواست قبلا بررسی شده است.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,12 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'zibal' => [
|
||||||
|
'merchant' => env('ZIBAL_MERCHANT', 'zibal'),
|
||||||
|
'sandbox' => env('ZIBAL_SANDBOX', true),
|
||||||
|
'callback_url' => env('ZIBAL_CALLBACK_URL'),
|
||||||
|
],
|
||||||
|
|
||||||
'iranpayamak' => [
|
'iranpayamak' => [
|
||||||
'api_url' => env('IRANPAYAMAK_SMS_API_URL', 'https://api.iranpayamak.com/ws/v1/sms/pattern'),
|
'api_url' => env('IRANPAYAMAK_SMS_API_URL', 'https://api.iranpayamak.com/ws/v1/sms/pattern'),
|
||||||
'line_number' => env('IRANPAYAMAK_SMS_API_URL', '50002178584000'),
|
'line_number' => env('IRANPAYAMAK_SMS_API_URL', '50002178584000'),
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\DocumentModalityEnum;
|
||||||
|
use App\Enums\InvoiceStatusEnum;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('invoices', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('number')->unique();
|
||||||
|
$table->unsignedBigInteger('amount');
|
||||||
|
$table->string('status')->default(InvoiceStatusEnum::Pending->value);
|
||||||
|
$table->string('payment_method')->nullable();
|
||||||
|
$table->timestamp('paid_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreignId('document_id')->nullable()->constrained('documents')->cascadeOnDelete();
|
||||||
|
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('documents')->orderBy('id')->each(function ($document) {
|
||||||
|
$modality = DocumentModalityEnum::tryFrom($document->modality);
|
||||||
|
|
||||||
|
if (!$modality) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = DB::table('invoices')->insertGetId([
|
||||||
|
'number' => 'pending',
|
||||||
|
'amount' => $modality->price(),
|
||||||
|
'status' => InvoiceStatusEnum::Pending->value,
|
||||||
|
'document_id' => $document->id,
|
||||||
|
'user_id' => $document->requester_id,
|
||||||
|
'created_at' => $document->created_at,
|
||||||
|
'updated_at' => $document->created_at,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('invoices')->where('id', $id)->update([
|
||||||
|
'number' => str_pad((string) $id, 5, '0', STR_PAD_LEFT),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('invoices');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\PaymentPurposeEnum;
|
||||||
|
use App\Enums\PaymentStatusEnum;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('payments', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('track_id')->unique();
|
||||||
|
$table->unsignedBigInteger('amount');
|
||||||
|
$table->string('status')->default(PaymentStatusEnum::Pending->value);
|
||||||
|
$table->string('purpose')->default(PaymentPurposeEnum::Invoice->value);
|
||||||
|
$table->string('ref_number')->nullable();
|
||||||
|
$table->timestamp('verified_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreignId('invoice_id')->nullable()->constrained('invoices')->cascadeOnDelete();
|
||||||
|
$table->foreignId('payer_id')->constrained('users')->cascadeOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('payments');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('wallets', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('balance')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreignId('user_id')->unique()->constrained('users')->cascadeOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('wallets');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->date('birthdate')->nullable()->after('fullname');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('birthdate');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('wallet_transactions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('type');
|
||||||
|
$table->string('reason');
|
||||||
|
$table->unsignedBigInteger('amount');
|
||||||
|
$table->unsignedBigInteger('balance_after');
|
||||||
|
$table->string('description')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreignId('wallet_id')->constrained('wallets')->cascadeOnDelete();
|
||||||
|
$table->foreignId('invoice_id')->nullable()->constrained('invoices')->nullOnDelete();
|
||||||
|
$table->foreignId('payment_id')->nullable()->constrained('payments')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('wallet_transactions');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('reports', function (Blueprint $table) {
|
||||||
|
$table->text('technique')->nullable()->after('body');
|
||||||
|
$table->text('finding')->nullable()->after('technique');
|
||||||
|
$table->text('diagnose')->nullable()->after('finding');
|
||||||
|
$table->text('recommendation')->nullable()->after('diagnose');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('reports', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['technique', 'finding', 'diagnose', 'recommendation']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('wallets', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['user_id']);
|
||||||
|
$table->foreignId('user_id')->nullable()->change();
|
||||||
|
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('wallets', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['user_id']);
|
||||||
|
$table->foreignId('user_id')->nullable(false)->change();
|
||||||
|
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('bank_cards', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('card_number', 16);
|
||||||
|
$table->string('holder_name');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('bank_cards');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\WithdrawRequestStatusEnum;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('withdraw_requests', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('amount');
|
||||||
|
$table->string('status')->default(WithdrawRequestStatusEnum::Pending->value)->index();
|
||||||
|
$table->text('admin_note')->nullable();
|
||||||
|
$table->timestamp('reviewed_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreignId('reviewed_by')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||||
|
$table->foreignId('bank_card_id')->nullable()->constrained('bank_cards')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('withdraw_requests');
|
||||||
|
}
|
||||||
|
};
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('wallet_transactions', function (Blueprint $table) {
|
||||||
|
$table->foreignId('withdraw_request_id')->nullable()->after('payment_id')->constrained('withdraw_requests')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('wallet_transactions', function (Blueprint $table) {
|
||||||
|
$table->dropConstrainedForeignId('withdraw_request_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -109,6 +109,12 @@ class PermissionSeeder extends Seeder
|
|||||||
'own' => 'دسترسی به شیفت های خود',
|
'own' => 'دسترسی به شیفت های خود',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'withdrawals' => [
|
||||||
|
'label' => 'دسترسی به درخواستهای برداشت',
|
||||||
|
'children' => [
|
||||||
|
'index' => 'دسترسی به لیست درخواستهای برداشت',
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
foreach ($permissions as $key => $data) {
|
foreach ($permissions as $key => $data) {
|
||||||
$parent = Permission::create([
|
$parent = Permission::create([
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@
|
|||||||
"@fullcalendar/core": "^6.1.21",
|
"@fullcalendar/core": "^6.1.21",
|
||||||
"@fullcalendar/interaction": "^6.1.21",
|
"@fullcalendar/interaction": "^6.1.21",
|
||||||
"@fullcalendar/timegrid": "^6.1.21",
|
"@fullcalendar/timegrid": "^6.1.21",
|
||||||
"@fullcalendar/vue3": "^7.0.2",
|
"@fullcalendar/vue3": "^6.1.21",
|
||||||
"@inertiajs/inertia": "^0.11.1",
|
"@inertiajs/inertia": "^0.11.1",
|
||||||
"@inertiajs/inertia-vue3": "^0.6.0",
|
"@inertiajs/inertia-vue3": "^0.6.0",
|
||||||
"@inertiajs/progress": "^0.2.7",
|
"@inertiajs/progress": "^0.2.7",
|
||||||
|
|||||||
@@ -1,3 +1,51 @@
|
|||||||
.p-formfield .p-select{
|
.p-inputtext,
|
||||||
@apply !py-0.5;
|
.p-select,
|
||||||
|
.p-multiselect,
|
||||||
|
.p-password,
|
||||||
|
.vpd-input-group {
|
||||||
|
@apply h-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-multiselect {
|
||||||
|
@apply h-auto min-h-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-select,
|
||||||
|
.p-multiselect {
|
||||||
|
@apply items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-password .p-inputtext {
|
||||||
|
@apply !h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-inputtext,
|
||||||
|
.p-textarea,
|
||||||
|
.p-select-label,
|
||||||
|
.p-multiselect-label,
|
||||||
|
.p-select-option,
|
||||||
|
.p-multiselect-option {
|
||||||
|
@apply !text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-inputtext::placeholder,
|
||||||
|
.p-textarea::placeholder {
|
||||||
|
@apply !text-sm font-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-select-label,
|
||||||
|
.p-multiselect-label {
|
||||||
|
@apply flex items-center !py-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-textarea {
|
||||||
|
@apply min-h-28;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-formfield select {
|
||||||
|
@apply w-full rounded-md border border-gray-300 bg-white p-2 text-sm text-client-Fontcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpd-input-group input.form-control {
|
||||||
|
@apply !h-full !w-full !rounded-md !border !border-gray-300 !bg-white !px-3 !text-sm !text-client-Fontcolor;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.ui-button--success.p-button {
|
||||||
|
@apply !bg-success !border-success font-normal !text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button--success.p-button:hover {
|
||||||
|
@apply !bg-success-active !border-success-active;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button--success.p-button:active {
|
||||||
|
@apply !bg-success-active !border-success-active;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button--success.p-button:focus {
|
||||||
|
box-shadow: 0 0 0 2px rgb(80 205 137 / 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
.ui-button-icon {
|
.ui-button-icon {
|
||||||
@apply transition-all duration-200;
|
@apply transition-all duration-200;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,4 @@
|
|||||||
/* Flag Icons using Emoji */
|
/* Flag Icons using Emoji */
|
||||||
.fi {
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
font-variant: normal;
|
|
||||||
text-transform: none;
|
|
||||||
line-height: 1;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fi:before {
|
|
||||||
content: attr(data-flag);
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Country Flags */
|
/* Country Flags */
|
||||||
.fi.fi-af:before { content: "🇦🇫"; }
|
.fi.fi-af:before { content: "🇦🇫"; }
|
||||||
.fi.fi-al:before { content: "🇦🇱"; }
|
.fi.fi-al:before { content: "🇦🇱"; }
|
||||||
|
|||||||
@@ -99,3 +99,8 @@
|
|||||||
.shift-calendar .shift-event--inactive {
|
.shift-calendar .shift-event--inactive {
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shift-calendar .fc-highlight {
|
||||||
|
background: rgba(87, 105, 137, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ body{
|
|||||||
@apply font-dana font-normal bg-client-gray/5;
|
@apply font-dana font-normal bg-client-gray/5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vpd-input-group{
|
|
||||||
@apply !h-9;
|
|
||||||
}
|
|
||||||
.ql-container {
|
.ql-container {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|||||||
@@ -118,17 +118,17 @@ const removeHistoryFile = (index) => {
|
|||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="submitted_at" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="submitted_at" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>تاریخ ثبت</label>
|
<label>تاریخ ثبت</label>
|
||||||
<CustomDatePicker v-model="documentStore.createForm.submitted_at" class="!text-xs" type="datetime" compact-time></CustomDatePicker>
|
<CustomDatePicker v-model="documentStore.createForm.submitted_at" type="datetime" compact-time></CustomDatePicker>
|
||||||
<Message v-if="documentStore.errorsCreateForm?.submitted_at" severity="error" size="small" variant="simple">{{ documentStore.errorsCreateForm?.submitted_at[0] }}</Message>
|
<Message v-if="documentStore.errorsCreateForm?.submitted_at" severity="error" size="small" variant="simple">{{ documentStore.errorsCreateForm?.submitted_at[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="processing_deadline" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="processing_deadline" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>مهلت پردازش</label>
|
<label>مهلت پردازش</label>
|
||||||
<CustomDatePicker v-model="documentStore.createForm.processing_deadline" class="!text-xs" type="datetime" compact-time></CustomDatePicker>
|
<CustomDatePicker v-model="documentStore.createForm.processing_deadline" type="datetime" compact-time></CustomDatePicker>
|
||||||
<Message v-if="documentStore.errorsCreateForm?.processing_deadline" severity="error" size="small" variant="simple">{{ documentStore.errorsCreateForm?.processing_deadline[0] }}</Message>
|
<Message v-if="documentStore.errorsCreateForm?.processing_deadline" severity="error" size="small" variant="simple">{{ documentStore.errorsCreateForm?.processing_deadline[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="note" initialValue="" class="flex flex-col col-span-2 gap-1">
|
<FormField v-slot="$field" name="note" initialValue="" class="flex flex-col col-span-2 gap-1">
|
||||||
<label>متن درخواست</label>
|
<label>متن درخواست</label>
|
||||||
<Textarea type="text" class="placeholder:!text-sm" v-model="documentStore.createForm.note" placeholder="متن درخواست را وارد کنید" />
|
<Textarea type="text" v-model="documentStore.createForm.note" placeholder="متن درخواست را وارد کنید" />
|
||||||
</FormField>
|
</FormField>
|
||||||
<div class="grid grid-cols-2 col-span-2 gap-4">
|
<div class="grid grid-cols-2 col-span-2 gap-4">
|
||||||
<Card class="mt-4">
|
<Card class="mt-4">
|
||||||
@@ -155,7 +155,7 @@ const removeHistoryFile = (index) => {
|
|||||||
<div class="p-4 grid grid-cols-1 gap-4 rounded-2xl">
|
<div class="p-4 grid grid-cols-1 gap-4 rounded-2xl">
|
||||||
<div class="grid grid-cols-1 justify-between items-center">
|
<div class="grid grid-cols-1 justify-between items-center">
|
||||||
<span>زمان انتشار:</span>
|
<span>زمان انتشار:</span>
|
||||||
<CustomDatePicker v-model="documentStore.createForm.datetime" class="!text-xs mt-2" type="datetime" compact-time></CustomDatePicker>
|
<CustomDatePicker v-model="documentStore.createForm.datetime" class="mt-2" type="datetime" compact-time></CustomDatePicker>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 justify-start">
|
<div class="grid grid-cols-1 justify-start">
|
||||||
<Button :loading="documentStore.loaderCreateForm" @click="documentStore.update_and_store" type="button" severity="secondary" :label="documentStore.buttonLabel" />
|
<Button :loading="documentStore.loaderCreateForm" @click="documentStore.update_and_store" type="button" severity="secondary" :label="documentStore.buttonLabel" />
|
||||||
@@ -166,29 +166,3 @@ const removeHistoryFile = (index) => {
|
|||||||
</section>
|
</section>
|
||||||
</master>
|
</master>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
.p-multiselect {
|
|
||||||
height: auto;
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect-label {
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect .p-multiselect-header {
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect .p-multiselect-filter {
|
|
||||||
max-height: calc(100px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect .p-multiselect-item {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -56,24 +56,18 @@ onUnmounted(() => {
|
|||||||
<Form ref="formRef" v-slot="$form" class="flex flex-col gap-4 !w-full">
|
<Form ref="formRef" v-slot="$form" class="flex flex-col gap-4 !w-full">
|
||||||
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نام سطح</label>
|
<label>نام سطح</label>
|
||||||
<InputText type="text" :class="[
|
<InputText type="text" :class="permissionStore.errorsCreateForm?.name && !permissionStore.createForm.name ? 'p-invalid' : ''" v-model="permissionStore.createForm.name" placeholder="نام سطح را وارد کنید" />
|
||||||
'placeholder:!text-sm',
|
|
||||||
permissionStore.errorsCreateForm?.name && !permissionStore.createForm.name ? 'p-invalid' : ''
|
|
||||||
]" v-model="permissionStore.createForm.name" placeholder="نام سطح را وارد کنید" />
|
|
||||||
<Message v-if="permissionStore.errorsCreateForm?.name"
|
<Message v-if="permissionStore.errorsCreateForm?.name"
|
||||||
severity="error" size="small" variant="simple">{{ permissionStore.errorsCreateForm?.name[0] }}</Message>
|
severity="error" size="small" variant="simple">{{ permissionStore.errorsCreateForm?.name[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="label" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="label" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نام فارسی سطح</label>
|
<label>نام فارسی سطح</label>
|
||||||
<InputText type="text" :class="[
|
<InputText type="text" :class="permissionStore.errorsCreateForm?.label && !permissionStore.createForm.label ? 'p-invalid' : ''" v-model="permissionStore.createForm.label" placeholder="نام فارسی سطح را وارد کنید" />
|
||||||
'placeholder:!text-sm',
|
|
||||||
permissionStore.errorsCreateForm?.label && !permissionStore.createForm.label ? 'p-invalid' : ''
|
|
||||||
]" v-model="permissionStore.createForm.label" placeholder="نام فارسی سطح را وارد کنید" />
|
|
||||||
<Message v-if="permissionStore.errorsCreateForm?.label" severity="error" size="small" variant="simple">{{ permissionStore.errorsCreateForm?.label[0] }}</Message>
|
<Message v-if="permissionStore.errorsCreateForm?.label" severity="error" size="small" variant="simple">{{ permissionStore.errorsCreateForm?.label[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="parent_id" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="parent_id" initialValue="" class="flex flex-col gap-1">
|
||||||
<label> والد (مرتبط با) </label>
|
<label> والد (مرتبط با) </label>
|
||||||
<Select v-model="permissionStore.createForm.parent_id" optionLabel="label" optionValue="id" :pt="{option : '!text-sm'}"
|
<Select v-model="permissionStore.createForm.parent_id" optionLabel="label" optionValue="id"
|
||||||
:options="permissions.data" filter
|
:options="permissions.data" filter
|
||||||
placeholder="انتخاب سطح" class="w-full">
|
placeholder="انتخاب سطح" class="w-full">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
|
|||||||
@@ -54,41 +54,38 @@ onUnmounted(() => {
|
|||||||
<Form ref="formRef" v-slot="$form" class=" p-4 grid grid-cols-2 gap-4 !w-full">
|
<Form ref="formRef" v-slot="$form" class=" p-4 grid grid-cols-2 gap-4 !w-full">
|
||||||
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نام کیس</label>
|
<label>نام کیس</label>
|
||||||
<InputText type="text" :class="[
|
<InputText type="text" :class="petStore.errorsCreateForm?.name && !petStore.createForm.name ? 'p-invalid' : ''" v-model="petStore.createForm.name" placeholder="نام کیس خود را وارد کنید" />
|
||||||
'placeholder:!text-sm',
|
|
||||||
petStore.errorsCreateForm?.name && !petStore.createForm.name ? 'p-invalid' : ''
|
|
||||||
]" v-model="petStore.createForm.name" placeholder="نام کیس خود را وارد کنید" />
|
|
||||||
<Message v-if="petStore.errorsCreateForm?.name"
|
<Message v-if="petStore.errorsCreateForm?.name"
|
||||||
severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.name[0] }}</Message>
|
severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.name[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="email" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="email" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نوع کیس</label>
|
<label>نوع کیس</label>
|
||||||
<Select class="!text-xs !h-full" placeholder="انتخاب نوع کیس" v-model="petStore.createForm.type" :options="petStore.typeOptions" optionValue="value" optionLabel="label" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
|
<Select placeholder="انتخاب نوع کیس" v-model="petStore.createForm.type" :options="petStore.typeOptions" optionValue="value" optionLabel="label" />
|
||||||
<Message v-if="petStore.errorsCreateForm?.type" severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.type[0] }}</Message>
|
<Message v-if="petStore.errorsCreateForm?.type" severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.type[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="breed" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="breed" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نژاد</label>
|
<label>نژاد</label>
|
||||||
<InputText type="text" class="placeholder:!text-sm" v-model="petStore.createForm.breed" placeholder="نژاد را وارد کنید" />
|
<InputText type="text" v-model="petStore.createForm.breed" placeholder="نژاد را وارد کنید" />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="weight" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="weight" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>وزن</label>
|
<label>وزن</label>
|
||||||
<InputText type="text" class="placeholder:!text-sm" v-model="petStore.createForm.weight" placeholder="وزن را وارد کنید" />
|
<InputText type="text" v-model="petStore.createForm.weight" placeholder="وزن را وارد کنید" />
|
||||||
<Message v-if="$field?.invalid" severity="error" size="small" variant="simple">{{ $field.error?.message }}</Message>
|
<Message v-if="$field?.invalid" severity="error" size="small" variant="simple">{{ $field.error?.message }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="sterilized" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="sterilized" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>عقیم سازی</label>
|
<label>عقیم سازی</label>
|
||||||
<Select class="!text-xs !h-full" placeholder="انتخاب عقیم سازی" v-model="petStore.createForm.sterilized" optionLabel="label" optionValue="value" :options="petStore.sterilizedOptions" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
|
<Select placeholder="انتخاب عقیم سازی" v-model="petStore.createForm.sterilized" optionLabel="label" optionValue="value" :options="petStore.sterilizedOptions" />
|
||||||
<Message v-if="petStore.errorsCreateForm?.sterilized"
|
<Message v-if="petStore.errorsCreateForm?.sterilized"
|
||||||
severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.sterilized[0] }}</Message>
|
severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.sterilized[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="gender" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="gender" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>جنسیت</label>
|
<label>جنسیت</label>
|
||||||
<Select class="!text-xs !h-full" placeholder="انتخاب جنسیت" v-model="petStore.createForm.gender" optionLabel="label" optionValue="value" :options="petStore.genderOptions" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
|
<Select placeholder="انتخاب جنسیت" v-model="petStore.createForm.gender" optionLabel="label" optionValue="value" :options="petStore.genderOptions" />
|
||||||
<Message v-if="petStore.errorsCreateForm?.gender" severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.gender[0] }}</Message>
|
<Message v-if="petStore.errorsCreateForm?.gender" severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.gender[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="birthday" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="birthday" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>تاریخ تولد</label>
|
<label>تاریخ تولد</label>
|
||||||
<CustomDatePicker v-model="petStore.createForm.birthday" class="!text-xs mt-2" type="datetime" compact-time></CustomDatePicker>
|
<CustomDatePicker v-model="petStore.createForm.birthday" type="datetime" compact-time></CustomDatePicker>
|
||||||
<Message v-if="petStore.errorsCreateForm?.birthday" severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.birthday[0] }}</Message>
|
<Message v-if="petStore.errorsCreateForm?.birthday" severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.birthday[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -105,7 +102,7 @@ onUnmounted(() => {
|
|||||||
<div class="p-4 grid grid-cols-1 gap-4 rounded-2xl">
|
<div class="p-4 grid grid-cols-1 gap-4 rounded-2xl">
|
||||||
<div class="grid grid-cols-1 justify-between items-center">
|
<div class="grid grid-cols-1 justify-between items-center">
|
||||||
<span>زمان انتشار:</span>
|
<span>زمان انتشار:</span>
|
||||||
<CustomDatePicker v-model="petStore.createForm.datetime" class="!text-xs mt-2" type="datetime" compact-time></CustomDatePicker>
|
<CustomDatePicker v-model="petStore.createForm.datetime" class="mt-2" type="datetime" compact-time></CustomDatePicker>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 justify-start">
|
<div class="grid grid-cols-1 justify-start">
|
||||||
<Button :loading="petStore.loaderCreateForm" @click="petStore.update_and_store" type="button" severity="secondary" :label="petStore.buttonLabel" />
|
<Button :loading="petStore.loaderCreateForm" @click="petStore.update_and_store" type="button" severity="secondary" :label="petStore.buttonLabel" />
|
||||||
@@ -116,29 +113,3 @@ onUnmounted(() => {
|
|||||||
</section>
|
</section>
|
||||||
</master>
|
</master>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
.p-multiselect {
|
|
||||||
height: auto;
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect-label {
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect .p-multiselect-header {
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect .p-multiselect-filter {
|
|
||||||
max-height: calc(100px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect .p-multiselect-item {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -74,19 +74,13 @@ const selectAllWithOwn = () => {
|
|||||||
<Form ref="formRef" v-slot="$form" class="bg-white p-4 rounded-2xl flex flex-col gap-4 !w-full">
|
<Form ref="formRef" v-slot="$form" class="bg-white p-4 rounded-2xl flex flex-col gap-4 !w-full">
|
||||||
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نام مقام</label>
|
<label>نام مقام</label>
|
||||||
<InputText type="text" :class="[
|
<InputText type="text" :class="roleStore.errorsCreateForm?.name && !roleStore.createForm.name ? 'p-invalid' : ''" v-model="roleStore.createForm.name" placeholder="نام مقام را وارد کنید" />
|
||||||
'placeholder:!text-sm',
|
|
||||||
roleStore.errorsCreateForm?.name && !roleStore.createForm.name ? 'p-invalid' : ''
|
|
||||||
]" v-model="roleStore.createForm.name" placeholder="نام مقام را وارد کنید" />
|
|
||||||
<Message v-if="roleStore.errorsCreateForm?.name"
|
<Message v-if="roleStore.errorsCreateForm?.name"
|
||||||
severity="error" size="small" variant="simple">{{ roleStore.errorsCreateForm?.name[0] }}</Message>
|
severity="error" size="small" variant="simple">{{ roleStore.errorsCreateForm?.name[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="label" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="label" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نام فارسی مقام</label>
|
<label>نام فارسی مقام</label>
|
||||||
<InputText type="text" :class="[
|
<InputText type="text" :class="roleStore.errorsCreateForm?.label && !roleStore.createForm.label ? 'p-invalid' : ''" v-model="roleStore.createForm.label" placeholder="نام فارسی مقام را وارد کنید" />
|
||||||
'placeholder:!text-sm',
|
|
||||||
roleStore.errorsCreateForm?.label && !roleStore.createForm.label ? 'p-invalid' : ''
|
|
||||||
]" v-model="roleStore.createForm.label" placeholder="نام فارسی مقام را وارد کنید" />
|
|
||||||
<Message v-if="roleStore.errorsCreateForm?.label" severity="error" size="small" variant="simple">{{ roleStore.errorsCreateForm?.label[0] }}</Message>
|
<Message v-if="roleStore.errorsCreateForm?.label" severity="error" size="small" variant="simple">{{ roleStore.errorsCreateForm?.label[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="permissions_parent" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="permissions_parent" initialValue="" class="flex flex-col gap-1">
|
||||||
@@ -109,7 +103,7 @@ const selectAllWithOwn = () => {
|
|||||||
<div class="p-4 bg-white grid grid-cols-1 gap-4 rounded-2xl">
|
<div class="p-4 bg-white grid grid-cols-1 gap-4 rounded-2xl">
|
||||||
<div class="grid grid-cols-1 justify-between items-center">
|
<div class="grid grid-cols-1 justify-between items-center">
|
||||||
<span>زمان انتشار:</span>
|
<span>زمان انتشار:</span>
|
||||||
<CustomDatePicker v-model="roleStore.createForm.datetime" class="!text-xs mt-2" type="datetime" compact-time></CustomDatePicker>
|
<CustomDatePicker v-model="roleStore.createForm.datetime" class="mt-2" type="datetime" compact-time></CustomDatePicker>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-x-3 mt-1 justify-between items-center">
|
<div class="grid grid-cols-2 gap-x-3 mt-1 justify-between items-center">
|
||||||
<span class="col-span-2 mb-2">عملیات سریع:</span>
|
<span class="col-span-2 mb-2">عملیات سریع:</span>
|
||||||
|
|||||||
@@ -98,34 +98,25 @@ const uploadProfile = () => {
|
|||||||
<Form @submit="onFormSubmit" class="grid grid-cols-1 gap-4 w-full !border-0 !text-sm">
|
<Form @submit="onFormSubmit" class="grid grid-cols-1 gap-4 w-full !border-0 !text-sm">
|
||||||
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نام کاربری</label>
|
<label>نام کاربری</label>
|
||||||
<InputText type="text" :class="[
|
<InputText type="text" :class="errors?.name && !errors.name ? 'p-invalid' : ''" v-model="user_form.name" placeholder="نام کاربری را وارد کنید" />
|
||||||
'placeholder:!text-sm !text-sm',
|
|
||||||
errors?.name && !errors.name ? 'p-invalid' : ''
|
|
||||||
]" v-model="user_form.name" placeholder="نام کاربری را وارد کنید" />
|
|
||||||
<Message v-if="errors?.name"
|
<Message v-if="errors?.name"
|
||||||
severity="error" size="small" variant="simple">{{ errors?.name[0] }}</Message>
|
severity="error" size="small" variant="simple">{{ errors?.name[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="first_name" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="first_name" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نام کوچک</label>
|
<label>نام کوچک</label>
|
||||||
<InputText type="text" :class="[
|
<InputText type="text" :class="errors?.first_name && !errors.first_name ? 'p-invalid' : ''" v-model="user_form.first_name" placeholder="نام کوچک خود را وارد کنید" />
|
||||||
'placeholder:!text-sm !text-sm',
|
|
||||||
errors?.first_name && !errors.first_name ? 'p-invalid' : ''
|
|
||||||
]" v-model="user_form.first_name" placeholder="نام کوچک خود را وارد کنید" />
|
|
||||||
<Message v-if="errors?.first_name"
|
<Message v-if="errors?.first_name"
|
||||||
severity="error" size="small" variant="simple">{{ errors?.first_name[0] }}</Message>
|
severity="error" size="small" variant="simple">{{ errors?.first_name[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="last_name" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="last_name" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نام خانوادگی</label>
|
<label>نام خانوادگی</label>
|
||||||
<InputText type="text" :class="[
|
<InputText type="text" :class="errors?.last_name && !errors.last_name ? 'p-invalid' : ''" v-model="user_form.last_name" placeholder="نام کوچک خود را وارد کنید" />
|
||||||
'placeholder:!text-sm !text-sm',
|
|
||||||
errors?.last_name && !errors.last_name ? 'p-invalid' : ''
|
|
||||||
]" v-model="user_form.last_name" placeholder="نام کوچک خود را وارد کنید" />
|
|
||||||
<Message v-if="errors?.last_name"
|
<Message v-if="errors?.last_name"
|
||||||
severity="error" size="small" variant="simple">{{ errors?.last_name[0] }}</Message>
|
severity="error" size="small" variant="simple">{{ errors?.last_name[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" asChild name="email" :initialValue="globalStore.current_user.email">
|
<FormField v-slot="$field" asChild name="email" :initialValue="globalStore.current_user.email">
|
||||||
<section class="flex flex-col gap-2">
|
<section class="flex flex-col gap-2">
|
||||||
<InputText type="email" class="flex pt-2 !text-sm !font-light" disabled id="email" fluid />
|
<InputText type="email" class="!font-light" disabled id="email" fluid />
|
||||||
</section>
|
</section>
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button type="submit" :loading="loading" severity="secondary" label="ذخیره اطلاعات" />
|
<Button type="submit" :loading="loading" severity="secondary" label="ذخیره اطلاعات" />
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const ChangePasswordHandler = () => {
|
|||||||
<Password id="password" type="password" :feedback="false"
|
<Password id="password" type="password" :feedback="false"
|
||||||
appendTo="self" toggleMask
|
appendTo="self" toggleMask
|
||||||
v-model="ChangePasswordForm.password"
|
v-model="ChangePasswordForm.password"
|
||||||
class="placeholder:!font-light !text-sm !bg-white h-12 !border-gray-500/20 !text-client-Fontcolor"/>
|
class="placeholder:!font-light !bg-white !border-gray-500/20 !text-client-Fontcolor"/>
|
||||||
<label for="on_label" class="!text-client-Fontcolor !bg-white !text-sm font-light">کلمه عبور فعلی</label>
|
<label for="on_label" class="!text-client-Fontcolor !bg-white !text-sm font-light">کلمه عبور فعلی</label>
|
||||||
</FloatLabel>
|
</FloatLabel>
|
||||||
<Message
|
<Message
|
||||||
@@ -74,7 +74,7 @@ const ChangePasswordHandler = () => {
|
|||||||
<Password id="password" type="password" :feedback="false"
|
<Password id="password" type="password" :feedback="false"
|
||||||
appendTo="self" toggleMask
|
appendTo="self" toggleMask
|
||||||
v-model="ChangePasswordForm.newPassword"
|
v-model="ChangePasswordForm.newPassword"
|
||||||
class="placeholder:!font-light !text-sm !bg-white h-12 !border-gray-500/20 !text-client-Fontcolor"/>
|
class="placeholder:!font-light !bg-white !border-gray-500/20 !text-client-Fontcolor"/>
|
||||||
<label for="on_label" class="!text-client-Fontcolor !bg-white !text-sm font-light"> کلمه عبور جدید</label>
|
<label for="on_label" class="!text-client-Fontcolor !bg-white !text-sm font-light"> کلمه عبور جدید</label>
|
||||||
</FloatLabel>
|
</FloatLabel>
|
||||||
<Message
|
<Message
|
||||||
@@ -88,7 +88,7 @@ const ChangePasswordHandler = () => {
|
|||||||
<Password id="password" type="password" :feedback="false"
|
<Password id="password" type="password" :feedback="false"
|
||||||
appendTo="self" toggleMask
|
appendTo="self" toggleMask
|
||||||
v-model="ChangePasswordForm.newPassword_confirmation"
|
v-model="ChangePasswordForm.newPassword_confirmation"
|
||||||
class="placeholder:!font-light !text-sm !bg-white h-12 !border-gray-500/20 !text-client-Fontcolor"/>
|
class="placeholder:!font-light !bg-white !border-gray-500/20 !text-client-Fontcolor"/>
|
||||||
<label for="on_label" class="!text-client-Fontcolor !bg-white !text-sm font-light">تکرار کلمه عبور جدید</label>
|
<label for="on_label" class="!text-client-Fontcolor !bg-white !text-sm font-light">تکرار کلمه عبور جدید</label>
|
||||||
</FloatLabel>
|
</FloatLabel>
|
||||||
<Message
|
<Message
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {onMounted , onUnmounted} from 'vue';
|
|||||||
import {useShiftStore} from "../../store/Shift.js";
|
import {useShiftStore} from "../../store/Shift.js";
|
||||||
import {Link} from "@inertiajs/inertia-vue3";
|
import {Link} from "@inertiajs/inertia-vue3";
|
||||||
import Card from "../../components/ui/Card.vue";
|
import Card from "../../components/ui/Card.vue";
|
||||||
|
import ShiftCalendar from "../../components/shifts/ShiftCalendar.vue";
|
||||||
|
import { weekdayName } from "../../../Shared/shiftCalendarEvents";
|
||||||
|
|
||||||
const shiftStore = useShiftStore();
|
const shiftStore = useShiftStore();
|
||||||
|
|
||||||
@@ -18,8 +20,22 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pad = (value) => String(value).padStart(2, '0');
|
||||||
|
|
||||||
|
const onRangeSelect = ({ start, end }) => {
|
||||||
|
shiftStore.createForm.start_time = `${pad(start.getHours())}:${pad(start.getMinutes())}`;
|
||||||
|
shiftStore.createForm.end_time = `${pad(end.getHours())}:${pad(end.getMinutes())}`;
|
||||||
|
|
||||||
|
if (shiftStore.createForm.schedule_type === 'date') {
|
||||||
|
shiftStore.createForm.date = `${start.getFullYear()}-${pad(start.getMonth() + 1)}-${pad(start.getDate())}`;
|
||||||
|
} else {
|
||||||
|
shiftStore.createForm.day_of_week = weekdayName(start.getDay());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
shiftStore.fetchDoctorOptions();
|
shiftStore.fetchDoctorOptions();
|
||||||
|
shiftStore.fetchCalendarData();
|
||||||
if (props.is_edit) {
|
if (props.is_edit) {
|
||||||
shiftStore.setEditData(props.shift);
|
shiftStore.setEditData(props.shift);
|
||||||
}
|
}
|
||||||
@@ -52,36 +68,36 @@ onUnmounted(() => {
|
|||||||
<Form ref="formRef" v-slot="$form" class=" p-4 grid grid-cols-2 gap-4 !w-full">
|
<Form ref="formRef" v-slot="$form" class=" p-4 grid grid-cols-2 gap-4 !w-full">
|
||||||
<FormField v-slot="$field" name="user_id" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="user_id" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>پزشک</label>
|
<label>پزشک</label>
|
||||||
<Select class="!text-xs !h-full" placeholder="انتخاب پزشک (خالی = شیفت آزاد)" v-model="shiftStore.createForm.user_id" :options="shiftStore.doctorOptions" optionValue="value" optionLabel="label" showClear :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
|
<Select placeholder="انتخاب پزشک (خالی = شیفت آزاد)" v-model="shiftStore.createForm.user_id" :options="shiftStore.doctorOptions" optionValue="value" optionLabel="label" showClear />
|
||||||
<Message v-if="shiftStore.errorsCreateForm?.user_id" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.user_id[0] }}</Message>
|
<Message v-if="shiftStore.errorsCreateForm?.user_id" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.user_id[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="schedule_type" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="schedule_type" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نوع زمانبندی</label>
|
<label>نوع زمانبندی</label>
|
||||||
<Select class="!text-xs !h-full" placeholder="انتخاب نوع زمانبندی" v-model="shiftStore.createForm.schedule_type" :options="shiftStore.scheduleTypeOptions" optionValue="value" optionLabel="label" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
|
<Select placeholder="انتخاب نوع زمانبندی" v-model="shiftStore.createForm.schedule_type" :options="shiftStore.scheduleTypeOptions" optionValue="value" optionLabel="label" />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-if="shiftStore.createForm.schedule_type === 'weekly'" v-slot="$field" name="day_of_week" initialValue="" class="flex flex-col gap-1">
|
<FormField v-if="shiftStore.createForm.schedule_type === 'weekly'" v-slot="$field" name="day_of_week" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>روز هفته</label>
|
<label>روز هفته</label>
|
||||||
<Select class="!text-xs !h-full" placeholder="انتخاب روز هفته" v-model="shiftStore.createForm.day_of_week" :options="shiftStore.dayOptions" optionValue="value" optionLabel="label" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
|
<Select placeholder="انتخاب روز هفته" v-model="shiftStore.createForm.day_of_week" :options="shiftStore.dayOptions" optionValue="value" optionLabel="label" />
|
||||||
<Message v-if="shiftStore.errorsCreateForm?.day_of_week" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.day_of_week[0] }}</Message>
|
<Message v-if="shiftStore.errorsCreateForm?.day_of_week" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.day_of_week[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-else v-slot="$field" name="date" initialValue="" class="flex flex-col gap-1">
|
<FormField v-else v-slot="$field" name="date" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>تاریخ</label>
|
<label>تاریخ</label>
|
||||||
<CustomDatePicker v-model="shiftStore.createForm.date" class="!text-xs mt-2" type="date" format="YYYY-MM-DD" placeholder="تاریخ شیفت را انتخاب کنید"></CustomDatePicker>
|
<CustomDatePicker v-model="shiftStore.createForm.date" type="date" format="YYYY-MM-DD" placeholder="تاریخ شیفت را انتخاب کنید"></CustomDatePicker>
|
||||||
<Message v-if="shiftStore.errorsCreateForm?.date" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.date[0] }}</Message>
|
<Message v-if="shiftStore.errorsCreateForm?.date" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.date[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="start_time" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="start_time" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>ساعت شروع</label>
|
<label>ساعت شروع</label>
|
||||||
<CustomDatePicker v-model="shiftStore.createForm.start_time" class="!text-xs mt-2" type="time" format="HH:mm" display-format="HH:mm" placeholder="ساعت شروع را انتخاب کنید"></CustomDatePicker>
|
<CustomDatePicker v-model="shiftStore.createForm.start_time" type="time" format="HH:mm" display-format="HH:mm" placeholder="ساعت شروع را انتخاب کنید"></CustomDatePicker>
|
||||||
<Message v-if="shiftStore.errorsCreateForm?.start_time" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.start_time[0] }}</Message>
|
<Message v-if="shiftStore.errorsCreateForm?.start_time" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.start_time[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="end_time" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="end_time" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>ساعت پایان</label>
|
<label>ساعت پایان</label>
|
||||||
<CustomDatePicker v-model="shiftStore.createForm.end_time" class="!text-xs mt-2" type="time" format="HH:mm" display-format="HH:mm" placeholder="ساعت پایان را انتخاب کنید"></CustomDatePicker>
|
<CustomDatePicker v-model="shiftStore.createForm.end_time" type="time" format="HH:mm" display-format="HH:mm" placeholder="ساعت پایان را انتخاب کنید"></CustomDatePicker>
|
||||||
<Message v-if="shiftStore.errorsCreateForm?.end_time" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.end_time[0] }}</Message>
|
<Message v-if="shiftStore.errorsCreateForm?.end_time" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.end_time[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="is_active" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="is_active" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>وضعیت</label>
|
<label>وضعیت</label>
|
||||||
<Select class="!text-xs !h-full" placeholder="انتخاب وضعیت" v-model="shiftStore.createForm.is_active" :options="shiftStore.statusOptions" optionValue="value" optionLabel="label" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
|
<Select placeholder="انتخاب وضعیت" v-model="shiftStore.createForm.is_active" :options="shiftStore.statusOptions" optionValue="value" optionLabel="label" />
|
||||||
<Message v-if="shiftStore.errorsCreateForm?.is_active" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.is_active[0] }}</Message>
|
<Message v-if="shiftStore.errorsCreateForm?.is_active" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.is_active[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -97,5 +113,18 @@ onUnmounted(() => {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<Card class="mt-4">
|
||||||
|
<div class="p-4 rounded-2xl">
|
||||||
|
<div class="flex items-center justify-between pb-2">
|
||||||
|
<h5 class="font-bold text-client-Fontcolor">برنامه فعلی شیفتها</h5>
|
||||||
|
<span class="text-xs text-client-Fontcolor/60">برای تنظیم سریع زمان شیفت، بازه مورد نظر را روی تقویم بکشید</span>
|
||||||
|
</div>
|
||||||
|
<ShiftCalendar
|
||||||
|
:shifts="shiftStore.calendarShifts"
|
||||||
|
selectable
|
||||||
|
@select-range="onRangeSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
</master>
|
</master>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -58,41 +58,32 @@ onUnmounted(() => {
|
|||||||
<Form ref="formRef" v-slot="$form" class=" p-4 flex flex-col gap-4 !w-full">
|
<Form ref="formRef" v-slot="$form" class=" p-4 flex flex-col gap-4 !w-full">
|
||||||
<FormField v-slot="$field" name="username" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="username" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نام کاربری</label>
|
<label>نام کاربری</label>
|
||||||
<InputText type="text" :class="[
|
<InputText type="text" :class="userStore.errorsCreateForm?.username && !userStore.createForm.username ? 'p-invalid' : ''" v-model="userStore.createForm.username" placeholder="نام کاربری خود را وارد کنید" />
|
||||||
'placeholder:!text-sm',
|
|
||||||
userStore.errorsCreateForm?.username && !userStore.createForm.username ? 'p-invalid' : ''
|
|
||||||
]" v-model="userStore.createForm.username" placeholder="نام کاربری خود را وارد کنید" />
|
|
||||||
<Message v-if="userStore.errorsCreateForm?.username"
|
<Message v-if="userStore.errorsCreateForm?.username"
|
||||||
severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.username[0] }}</Message>
|
severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.username[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="email" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="email" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>ایمیل کاربری</label>
|
<label>ایمیل کاربری</label>
|
||||||
<InputText type="text" :class="[
|
<InputText type="text" :class="userStore.errorsCreateForm?.email && !userStore.createForm.email ? 'p-invalid' : ''" v-model="userStore.createForm.email" placeholder="ایمیل خود را وارد کنید" />
|
||||||
'placeholder:!text-sm',
|
|
||||||
userStore.errorsCreateForm?.email && !userStore.createForm.email ? 'p-invalid' : ''
|
|
||||||
]" v-model="userStore.createForm.email" placeholder="ایمیل خود را وارد کنید" />
|
|
||||||
<Message v-if="userStore.errorsCreateForm?.email" severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.email[0] }}</Message>
|
<Message v-if="userStore.errorsCreateForm?.email" severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.email[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="national_code" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="national_code" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>کد ملی</label>
|
<label>کد ملی</label>
|
||||||
<InputText type="text" :class="[
|
<InputText type="text" :class="userStore.errorsCreateForm?.national_code && !userStore.createForm.national_code ? 'p-invalid' : ''" v-model="userStore.createForm.national_code" placeholder="کد ملی خود را وارد کنید" />
|
||||||
'placeholder:!text-sm',
|
|
||||||
userStore.errorsCreateForm?.national_code && !userStore.createForm.national_code ? 'p-invalid' : ''
|
|
||||||
]" v-model="userStore.createForm.national_code" placeholder="کد ملی خود را وارد کنید" />
|
|
||||||
<Message v-if="userStore.errorsCreateForm?.national_code" severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.national_code[0] }}</Message>
|
<Message v-if="userStore.errorsCreateForm?.national_code" severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.national_code[0] }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="phone" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="phone" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>شماره تماس</label>
|
<label>شماره تماس</label>
|
||||||
<InputText type="text" class="placeholder:!text-sm" v-model="userStore.createForm.phone" placeholder="شماره تماس خود را وارد کنید" />
|
<InputText type="text" v-model="userStore.createForm.phone" placeholder="شماره تماس خود را وارد کنید" />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="fullname" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="fullname" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>نام خانوادگی</label>
|
<label>نام خانوادگی</label>
|
||||||
<InputText type="text" class="placeholder:!text-sm" v-model="userStore.createForm.fullname" placeholder="نام خانوادگی خود را وارد کنید" />
|
<InputText type="text" v-model="userStore.createForm.fullname" placeholder="نام خانوادگی خود را وارد کنید" />
|
||||||
<Message v-if="$field?.invalid" severity="error" size="small" variant="simple">{{ $field.error?.message }}</Message>
|
<Message v-if="$field?.invalid" severity="error" size="small" variant="simple">{{ $field.error?.message }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="password" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="password" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>کلمه عبور</label>
|
<label>کلمه عبور</label>
|
||||||
<Password type="text" :inputClass="['placeholder:!text-sm' , { 'p-invalid' : userStore.errorsCreateForm?.password && !userStore.createForm.password}]"
|
<Password type="text" :inputClass="{ 'p-invalid' : userStore.errorsCreateForm?.password && !userStore.createForm.password}"
|
||||||
v-model="userStore.createForm.password" placeholder="کلمه عبور خود را وارد کنید" :feedback="false" toggleMask fluid />
|
v-model="userStore.createForm.password" placeholder="کلمه عبور خود را وارد کنید" :feedback="false" toggleMask fluid />
|
||||||
<Message v-if="userStore.errorsCreateForm?.password"
|
<Message v-if="userStore.errorsCreateForm?.password"
|
||||||
severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.password[0] }}</Message>
|
severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.password[0] }}</Message>
|
||||||
@@ -100,7 +91,7 @@ onUnmounted(() => {
|
|||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="$field" name="password_confirmation" initialValue="" class="flex flex-col gap-1">
|
<FormField v-slot="$field" name="password_confirmation" initialValue="" class="flex flex-col gap-1">
|
||||||
<label>تکرار کلمه عبور</label>
|
<label>تکرار کلمه عبور</label>
|
||||||
<Password type="text" inputClass="placeholder:!text-sm" v-model="userStore.createForm.password_confirmation" placeholder="تکرار کلمه عبور خود را وارد کنید" :feedback="false" toggleMask fluid />
|
<Password type="text" v-model="userStore.createForm.password_confirmation" placeholder="تکرار کلمه عبور خود را وارد کنید" :feedback="false" toggleMask fluid />
|
||||||
<Message v-if="$field?.invalid" severity="error" size="small" variant="simple">{{ $field.error?.message }}</Message>
|
<Message v-if="$field?.invalid" severity="error" size="small" variant="simple">{{ $field.error?.message }}</Message>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -117,7 +108,7 @@ onUnmounted(() => {
|
|||||||
<div class="mt-4 p-4 grid grid-cols-1 gap-4 rounded-2xl">
|
<div class="mt-4 p-4 grid grid-cols-1 gap-4 rounded-2xl">
|
||||||
<div class="grid grid-cols-1 justify-between items-center">
|
<div class="grid grid-cols-1 justify-between items-center">
|
||||||
<span>زمان انتشار:</span>
|
<span>زمان انتشار:</span>
|
||||||
<CustomDatePicker v-model="userStore.createForm.datetime" class="!text-xs mt-2" type="datetime" compact-time></CustomDatePicker>
|
<CustomDatePicker v-model="userStore.createForm.datetime" class="mt-2" type="datetime" compact-time></CustomDatePicker>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2a justify-between items-center">
|
<div class="grid grid-cols-2a justify-between items-center">
|
||||||
<span class="flex">سطح:</span>
|
<span class="flex">سطح:</span>
|
||||||
@@ -125,9 +116,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2a justify-between items-center">
|
<div class="grid grid-cols-2a justify-between items-center">
|
||||||
<span class="flex">مقام:</span>
|
<span class="flex">مقام:</span>
|
||||||
<MultiSelect class="!text-xs"
|
<MultiSelect v-model="userStore.createForm.role"
|
||||||
v-model="userStore.createForm.role"
|
|
||||||
:pt="{ option : '!text-sm' }"
|
|
||||||
placeholder="انتخاب مقام"
|
placeholder="انتخاب مقام"
|
||||||
optionLabel="label"
|
optionLabel="label"
|
||||||
optionValue="id"
|
optionValue="id"
|
||||||
@@ -142,29 +131,3 @@ onUnmounted(() => {
|
|||||||
</section>
|
</section>
|
||||||
</master>
|
</master>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
.p-multiselect {
|
|
||||||
height: auto;
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect-label {
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect .p-multiselect-header {
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect .p-multiselect-filter {
|
|
||||||
max-height: calc(100px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-multiselect .p-multiselect-item {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
<script setup>
|
||||||
|
import Master from "../../layouts/master.vue";
|
||||||
|
import { ref, reactive, watch } from 'vue';
|
||||||
|
import TableGeneric from "../../components/TableGeneric.vue";
|
||||||
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { useWithdrawRequestStore } from "../../store/WithdrawRequest.js";
|
||||||
|
import UIModal from "../../components/ui/UIModal.vue";
|
||||||
|
|
||||||
|
const withdrawRequestStore = useWithdrawRequestStore();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
withdrawRequests: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const withdrawRequests = ref(props.withdrawRequests ?? []);
|
||||||
|
const searchLoader = ref(false);
|
||||||
|
const actionLoader = ref(false);
|
||||||
|
|
||||||
|
const columns = ref(withdrawRequestStore.columns)
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
'همه وضعیت ها',
|
||||||
|
'در انتظار بررسی',
|
||||||
|
'تایید شده',
|
||||||
|
'رد شده',
|
||||||
|
];
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
search: props.search ?? '',
|
||||||
|
status: props.status ?? 'همه وضعیت ها',
|
||||||
|
})
|
||||||
|
|
||||||
|
const confirm = useConfirm();
|
||||||
|
|
||||||
|
watch(() => props.withdrawRequests, (newRequests) => {
|
||||||
|
if (newRequests) {
|
||||||
|
withdrawRequests.value = newRequests;
|
||||||
|
}
|
||||||
|
}, { deep: true, immediate: true });
|
||||||
|
|
||||||
|
const onPageChange = (event) => {
|
||||||
|
withdrawRequestStore.navigateToPage(event.page + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchData = () => {
|
||||||
|
searchLoader.value = true;
|
||||||
|
withdrawRequestStore.search(queryParams).then(() => {
|
||||||
|
searchLoader.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSearchChange = (value) => {
|
||||||
|
queryParams.search = value;
|
||||||
|
searchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCardNumber = (cardNumber) => cardNumber.replace(/(\d{4})(?=\d)/g, '$1-');
|
||||||
|
|
||||||
|
const showApproveConfirm = (data) => {
|
||||||
|
confirm.require({
|
||||||
|
group: 'deleteTemplate',
|
||||||
|
header: 'تایید درخواست برداشت',
|
||||||
|
message: `مبلغ ${Number(data.amount).toLocaleString('fa-IR')} تومان از کیف پول «${data.user?.fullname}» کسر و به ولت سایت منتقل میشود.`,
|
||||||
|
icon: 'fi fi-rr-info',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'انصراف',
|
||||||
|
icon: 'fi fi-rr-ban',
|
||||||
|
outlined: true,
|
||||||
|
size: 'small',
|
||||||
|
class: '!text-xs !flex pt-1 !item-center',
|
||||||
|
iconClass: '!flex !text-xs',
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'تایید برداشت',
|
||||||
|
icon: 'fi fi-rr-check',
|
||||||
|
size: 'small',
|
||||||
|
class: '!text-xs !flex pt-1 !item-center',
|
||||||
|
iconClass: '!flex !text-xs',
|
||||||
|
},
|
||||||
|
accept: () => {
|
||||||
|
actionLoader.value = true;
|
||||||
|
withdrawRequestStore.approve(data.id)
|
||||||
|
.catch((error) => {
|
||||||
|
toast.add({ severity: 'error', summary: 'خطا', detail: error.response?.data?.message || 'خطا در تایید درخواست', life: 4000 });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
actionLoader.value = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rejectModalVisible = ref(false);
|
||||||
|
const rejectingRequest = ref(null);
|
||||||
|
const rejectNote = ref('');
|
||||||
|
|
||||||
|
const openRejectModal = (data) => {
|
||||||
|
rejectingRequest.value = data;
|
||||||
|
rejectNote.value = '';
|
||||||
|
rejectModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitReject = () => {
|
||||||
|
actionLoader.value = true;
|
||||||
|
withdrawRequestStore.reject(rejectingRequest.value.id, rejectNote.value)
|
||||||
|
.then(() => {
|
||||||
|
rejectModalVisible.value = false;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.add({ severity: 'error', summary: 'خطا', detail: error.response?.data?.message || 'خطا در رد درخواست', life: 4000 });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
actionLoader.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<master>
|
||||||
|
<div class="mb-4 grid grid-cols-2a justify-between items-center bg-white border-[1px] border-client-gray/20 py-4 px-4 rounded-2xl">
|
||||||
|
<h2 class="font-bold text-client-Fontcolor">{{ withdrawRequestStore.indexPageTitle }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 grid grid-cols-3 max-lg:grid-cols-1 gap-4">
|
||||||
|
<div class="bg-white border-[1px] border-client-gray/20 rounded-2xl p-4 flex flex-col gap-2">
|
||||||
|
<span class="text-xs text-gray-500">درخواستهای در انتظار بررسی</span>
|
||||||
|
<strong class="text-lg text-client-Fontcolor">{{ Number(stats.pending_count).toLocaleString('fa-IR') }} درخواست</strong>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white border-[1px] border-client-gray/20 rounded-2xl p-4 flex flex-col gap-2">
|
||||||
|
<span class="text-xs text-gray-500">مجموع مبلغ در انتظار برداشت</span>
|
||||||
|
<strong class="text-lg text-client-Fontcolor">{{ Number(stats.pending_amount).toLocaleString('fa-IR') }} تومان</strong>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white border-[1px] border-client-gray/20 rounded-2xl p-4 flex flex-col gap-2">
|
||||||
|
<span class="text-xs text-gray-500">موجودی ولت سایت</span>
|
||||||
|
<strong class="text-lg text-client-Fontcolor">{{ Number(stats.system_wallet_balance).toLocaleString('fa-IR') }} تومان</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-[1px] border-client-gray/20 bg-white rounded-2xl">
|
||||||
|
<TableGeneric
|
||||||
|
:data="withdrawRequests.data"
|
||||||
|
:columns="columns"
|
||||||
|
:selectable="false"
|
||||||
|
:pagination="withdrawRequests.meta"
|
||||||
|
:searchable="true"
|
||||||
|
:searchValue="queryParams.search"
|
||||||
|
:model-value="[]"
|
||||||
|
@page-change="onPageChange"
|
||||||
|
@search-change="onSearchChange"
|
||||||
|
>
|
||||||
|
<template #toolbar-actions>
|
||||||
|
<div class="grid grid-cols-a gap-4 justify-start">
|
||||||
|
<Dropdown
|
||||||
|
v-model="queryParams.status"
|
||||||
|
:options="statusOptions"
|
||||||
|
placeholder="انتخاب وضعیت..."
|
||||||
|
class="!text-xs"
|
||||||
|
@change="searchData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #column-user="{ data }">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm">{{ data.user?.fullname }}</span>
|
||||||
|
<span class="text-xs text-gray-500">{{ data.user?.level }} - {{ data.user?.phone }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #column-bank_card="{ data }">
|
||||||
|
<div v-if="data.bank_card" class="flex flex-col gap-1">
|
||||||
|
<pre dir="ltr" class="text-right text-xs">{{ formatCardNumber(data.bank_card.card_number) }}</pre>
|
||||||
|
<span class="text-xs text-gray-500">{{ data.bank_card.holder_name }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-xs text-gray-500">حذف شده</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #column-status="{ data }">
|
||||||
|
<div class="flex flex-col gap-1 items-start">
|
||||||
|
<Tag
|
||||||
|
class="!text-xs"
|
||||||
|
:value="data.status"
|
||||||
|
:severity="withdrawRequestStore.getStatusSeverity(data.status)"
|
||||||
|
/>
|
||||||
|
<span v-if="data.admin_note" class="text-xs text-gray-500">{{ data.admin_note }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #column-actions="{ data }">
|
||||||
|
<div v-if="data.status === 'در انتظار بررسی'" class="grid grid-cols-2a justify-start gap-2">
|
||||||
|
<Button
|
||||||
|
label="تایید"
|
||||||
|
size="small"
|
||||||
|
severity="success"
|
||||||
|
class="!text-xs"
|
||||||
|
:disabled="actionLoader"
|
||||||
|
@click="showApproveConfirm(data)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="رد"
|
||||||
|
size="small"
|
||||||
|
severity="danger"
|
||||||
|
variant="outlined"
|
||||||
|
class="!text-xs"
|
||||||
|
:disabled="actionLoader"
|
||||||
|
@click="openRejectModal(data)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TableGeneric>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UIModal
|
||||||
|
v-model:visible="rejectModalVisible"
|
||||||
|
title="رد درخواست برداشت"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
درخواست برداشت «{{ rejectingRequest ? Number(rejectingRequest.amount).toLocaleString('fa-IR') : '' }} تومان» کاربر «{{ rejectingRequest?.user?.fullname }}» رد میشود.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
v-model="rejectNote"
|
||||||
|
rows="4"
|
||||||
|
placeholder="توضیح برای کاربر (اختیاری)"
|
||||||
|
class="w-full border border-gray-200 rounded-xl p-3 text-sm outline-none focus:border-gray-400 resize-y"
|
||||||
|
></textarea>
|
||||||
|
<div class="flex items-center justify-start gap-2">
|
||||||
|
<Button
|
||||||
|
label="رد درخواست"
|
||||||
|
size="small"
|
||||||
|
severity="danger"
|
||||||
|
class="!text-xs"
|
||||||
|
:loading="actionLoader"
|
||||||
|
:disabled="actionLoader"
|
||||||
|
@click="submitReject"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="انصراف"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
variant="outlined"
|
||||||
|
class="!text-xs"
|
||||||
|
:disabled="actionLoader"
|
||||||
|
@click="rejectModalVisible = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UIModal>
|
||||||
|
</master>
|
||||||
|
</template>
|
||||||
@@ -86,6 +86,14 @@ const items: Ref<SidebarItem[]> = ref([
|
|||||||
permission: "reports.index",
|
permission: "reports.index",
|
||||||
expanded: false,
|
expanded: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "درخواستهای برداشت",
|
||||||
|
icon: "fi fi-rr-wallet",
|
||||||
|
to: "/admin/withdraw-requests",
|
||||||
|
key: 'withdrawals',
|
||||||
|
permission: "withdrawals.index",
|
||||||
|
expanded: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "مدیریت شیفت ها",
|
label: "مدیریت شیفت ها",
|
||||||
icon: "fi fi-rr-calendar",
|
icon: "fi fi-rr-calendar",
|
||||||
|
|||||||
@@ -251,7 +251,6 @@ const getColumnStyle = (column) => {
|
|||||||
<div class="grid grid-cols-1 items-center justify-start">
|
<div class="grid grid-cols-1 items-center justify-start">
|
||||||
<InputText
|
<InputText
|
||||||
size="small"
|
size="small"
|
||||||
class="placeholder:!font-light placeholder:!text-xs !h-10"
|
|
||||||
placeholder="جستجو..."
|
placeholder="جستجو..."
|
||||||
type="text"
|
type="text"
|
||||||
v-model="localSearchValue"
|
v-model="localSearchValue"
|
||||||
@@ -274,8 +273,7 @@ const getColumnStyle = (column) => {
|
|||||||
optionLabel="label"
|
optionLabel="label"
|
||||||
optionValue="value"
|
optionValue="value"
|
||||||
size="small"
|
size="small"
|
||||||
:pt="{label : '!text-[12px] !pt-2.5' , option : '!text-[12px] !font-normal'}"
|
class="min-w-[130px] max-sm:!w-full max-w-[200px]"
|
||||||
class="min-w-[130px] max-sm:!w-full max-w-[200px] !text-[12px] !h-10"
|
|
||||||
:loading="renderFilter(column).loading"
|
:loading="renderFilter(column).loading"
|
||||||
:disabled="renderFilter(column).loading"
|
:disabled="renderFilter(column).loading"
|
||||||
:filter="renderFilter(column).search"
|
:filter="renderFilter(column).search"
|
||||||
@@ -297,8 +295,7 @@ const getColumnStyle = (column) => {
|
|||||||
optionLabel="label"
|
optionLabel="label"
|
||||||
optionValue="value"
|
optionValue="value"
|
||||||
size="small"
|
size="small"
|
||||||
:pt="{label : '!text-[12px] !pt-2.5' , option : '!text-[12px] !font-normal'}"
|
class="min-w-[150px] max-sm:!w-full max-w-[200px]"
|
||||||
class="min-w-[150px] max-sm:!w-full max-w-[200px] !text-[12px] !h-10"
|
|
||||||
:loading="renderFilter(column).loading"
|
:loading="renderFilter(column).loading"
|
||||||
:disabled="renderFilter(column).loading"
|
:disabled="renderFilter(column).loading"
|
||||||
:filter="renderFilter(column).search"
|
:filter="renderFilter(column).search"
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ const props = defineProps({
|
|||||||
shifts: {
|
shifts: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
selectable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['select-shift']);
|
const emit = defineEmits(['select-shift', 'select-range']);
|
||||||
|
|
||||||
const doctorPalette = ['#576989', '#800E13', '#50CD89', '#7239EA', '#FFC700', '#F1416C', '#009EF7', '#965457'];
|
const doctorPalette = ['#576989', '#800E13', '#50CD89', '#7239EA', '#FFC700', '#F1416C', '#009EF7', '#965457'];
|
||||||
|
|
||||||
@@ -71,6 +75,11 @@ const calendarOptions = computed(() => ({
|
|||||||
eventClick: (info) => {
|
eventClick: (info) => {
|
||||||
emit('select-shift', info.event.extendedProps.shift);
|
emit('select-shift', info.event.extendedProps.shift);
|
||||||
},
|
},
|
||||||
|
selectable: props.selectable,
|
||||||
|
selectMirror: true,
|
||||||
|
select: (info) => {
|
||||||
|
emit('select-range', { start: info.start, end: info.end });
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const goPrev = () => calendarRef.value?.getApi().prev();
|
const goPrev = () => calendarRef.value?.getApi().prev();
|
||||||
|
|||||||
@@ -45,15 +45,11 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
class: {
|
class: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '!text-xs !h-full'
|
default: 'w-full'
|
||||||
},
|
},
|
||||||
pt: {
|
pt: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({})
|
||||||
option: '!text-sm',
|
|
||||||
placeholder: '!text-sm',
|
|
||||||
label: '!pt-3'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import axios from "axios";
|
||||||
|
import { Inertia } from '@inertiajs/inertia'
|
||||||
|
|
||||||
|
export const useWithdrawRequestStore = defineStore('WithdrawRequestStore', {
|
||||||
|
state: () => ({
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: 'id',
|
||||||
|
title: 'شناسه',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'user',
|
||||||
|
title: 'درخواستکننده',
|
||||||
|
slot: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'amount',
|
||||||
|
title: 'مبلغ',
|
||||||
|
render: (data) => `${Number(data.amount).toLocaleString('fa-IR')} تومان`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bank_card',
|
||||||
|
title: 'کارت مقصد',
|
||||||
|
slot: 'bank_card',
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
title: 'وضعیت',
|
||||||
|
slot: 'status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'created_at',
|
||||||
|
title: 'تاریخ درخواست',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'reviewed_by',
|
||||||
|
title: 'بررسیکننده',
|
||||||
|
render: (data) => data.reviewed_by ? `${data.reviewed_by.fullname} - ${data.reviewed_at}` : 'بررسی نشده',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
title: 'عملیات',
|
||||||
|
hasSort: false,
|
||||||
|
slot: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
endpoints: {
|
||||||
|
index: '/admin/withdraw-requests',
|
||||||
|
approve: (id) => `/admin/withdraw-requests/${id}/approve`,
|
||||||
|
reject: (id) => `/admin/withdraw-requests/${id}/reject`,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
indexPageTitle: () => {
|
||||||
|
return 'درخواستهای برداشت';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
approve(id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.post(this.endpoints.approve(id))
|
||||||
|
.then(response => {
|
||||||
|
Inertia.get(this.endpoints.index);
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
reject(id, note) {
|
||||||
|
return new Promise((resolve, rejectPromise) => {
|
||||||
|
axios.post(this.endpoints.reject(id), { note })
|
||||||
|
.then(response => {
|
||||||
|
Inertia.get(this.endpoints.index);
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
rejectPromise(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
search(params) {
|
||||||
|
return Inertia.get(this.endpoints.index, params, {
|
||||||
|
preserveState: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
navigateToPage(page) {
|
||||||
|
return Inertia.get(this.endpoints.index, { page }, { preserveState: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatusSeverity(status) {
|
||||||
|
const statusSeverities = {
|
||||||
|
'در انتظار بررسی': 'warning',
|
||||||
|
'تایید شده': 'success',
|
||||||
|
'رد شده': 'danger',
|
||||||
|
};
|
||||||
|
return statusSeverities[status] || 'secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import { useForm } from '@inertiajs/inertia-vue3';
|
||||||
|
import { Inertia } from '@inertiajs/inertia';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import { toast } from 'vue3-toastify';
|
||||||
|
import ClientLayout from '../../../layouts/ClientLayout.vue';
|
||||||
|
import UiButton from '../../../components/ui/UiButton.vue';
|
||||||
|
import CaseSpecsCard from '../../../components/dashboard/cases/CaseSpecsCard.vue';
|
||||||
|
import ReportSectionCard from '../../../components/dashboard/cases/ReportSectionCard.vue';
|
||||||
|
import FileUploadBox from '../../../components/dashboard/fastRequest/FileUploadBox.vue';
|
||||||
|
import { modalityTitle } from '../../../data/fastRequestOptions';
|
||||||
|
import { convertEnglishToPersian } from '../../../utils/numberUtils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
case: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
canReport: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const petRows = computed(() => [
|
||||||
|
{ label: 'نام', value: props.case.pet?.name },
|
||||||
|
{ label: 'نوع', value: props.case.pet?.type },
|
||||||
|
{ label: 'جنسیت', value: props.case.pet?.gender },
|
||||||
|
{ label: 'نژاد', value: props.case.pet?.breed },
|
||||||
|
{ label: 'سن', value: props.case.pet?.age !== null && props.case.pet?.age !== undefined ? `${convertEnglishToPersian(props.case.pet.age)} سال` : null },
|
||||||
|
{ label: 'وزن', value: props.case.pet?.weight ? `${convertEnglishToPersian(props.case.pet.weight)} کیلو گرم` : null },
|
||||||
|
{ label: 'وضعیت عقیمسازی', value: props.case.pet?.sterilized === null ? null : (props.case.pet?.sterilized ? 'عقیم شده' : 'عقیم نشده') },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const requesterRows = computed(() => {
|
||||||
|
const requester = props.case.requester || {};
|
||||||
|
return [
|
||||||
|
{ label: 'نوع', value: requester.level },
|
||||||
|
{ label: 'نام و نام خانوادگی', value: requester.fullname },
|
||||||
|
{ label: 'شماره موبایل', value: requester.phone ? convertEnglishToPersian(requester.phone) : null },
|
||||||
|
{ label: 'تخصص', value: requester.specialty },
|
||||||
|
{ label: 'کلینیک', value: requester.clinic_name },
|
||||||
|
{ label: 'تلفن کلینیک', value: requester.clinic_phone ? convertEnglishToPersian(requester.clinic_phone) : null },
|
||||||
|
{ label: 'محل اقامت', value: [requester.province, requester.city].filter(Boolean).join('، ') || null },
|
||||||
|
].filter((row) => row.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const remainingLabel = ref(null);
|
||||||
|
let timerId = null;
|
||||||
|
|
||||||
|
const updateRemaining = () => {
|
||||||
|
if (!props.case.processing_deadline) {
|
||||||
|
remainingLabel.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = Math.floor((new Date(props.case.processing_deadline).getTime() - Date.now()) / 1000);
|
||||||
|
|
||||||
|
if (diff <= 0) {
|
||||||
|
remainingLabel.value = 'اتمام فرصت رسیدگی';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pad = (value) => String(value).padStart(2, '0');
|
||||||
|
remainingLabel.value = convertEnglishToPersian(`${pad(Math.floor(diff / 3600))}:${pad(Math.floor((diff % 3600) / 60))}:${pad(diff % 60)}`) + ' تا پایان زمان ثبت گزارش';
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateRemaining();
|
||||||
|
timerId = setInterval(updateRemaining, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => clearInterval(timerId));
|
||||||
|
|
||||||
|
const openFiles = (files) => {
|
||||||
|
files.forEach((file) => window.open(file.url, '_blank'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const reportForm = useForm({
|
||||||
|
document_id: props.case.id,
|
||||||
|
technique: props.case.report?.technique || '',
|
||||||
|
finding: props.case.report?.finding || '',
|
||||||
|
diagnose: props.case.report?.diagnose || '',
|
||||||
|
recommendation: props.case.report?.recommendation || '',
|
||||||
|
files: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitReport = () => {
|
||||||
|
reportForm.post('/dashboard/reports', {
|
||||||
|
forceFormData: true,
|
||||||
|
onError: () => toast.error('لطفا موارد مشخصشده را بررسی و اصلاح کنید.'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectModalVisible = ref(false);
|
||||||
|
const rejectReason = ref('');
|
||||||
|
const rejecting = ref(false);
|
||||||
|
|
||||||
|
const submitReject = () => {
|
||||||
|
if (!rejectReason.value.trim()) {
|
||||||
|
toast.error('لطفا دلیل رد درخواست را وارد کنید.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rejecting.value = true;
|
||||||
|
Inertia.post(`/dashboard/cases/${props.case.id}/reject`, { reason: rejectReason.value }, {
|
||||||
|
onFinish: () => {
|
||||||
|
rejecting.value = false;
|
||||||
|
rejectModalVisible.value = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ClientLayout>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<header class="bg-white border border-complement-50 rounded-2xl px-4 py-3 flex items-center justify-between flex-wrap gap-2">
|
||||||
|
<h1 class="text-[13px] font-bold text-[#3E4C63]">پرونده شماره {{ convertEnglishToPersian(props.case.code) }}</h1>
|
||||||
|
<div class="flex items-center gap-3 text-[11px]">
|
||||||
|
<span v-if="canReport && remainingLabel" class="font-semibold text-[#7A1419]">{{ remainingLabel }}</span>
|
||||||
|
<span v-else class="font-normal text-[#576989]">وضعیت فعلی</span>
|
||||||
|
<span class="font-semibold text-[#3E4C63] border-r border-[#DEE3EB] pr-3">{{ props.case.status }}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[16rem_1fr] max-lg:grid-cols-1 gap-4 items-start">
|
||||||
|
<aside class="flex flex-col gap-4">
|
||||||
|
<CaseSpecsCard title="مشخصات کیس" :rows="petRows" />
|
||||||
|
<CaseSpecsCard v-if="canReport && requesterRows.length" title="مشخصات درخواستدهنده" :rows="requesterRows" />
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<section class="bg-white border border-complement-50 rounded-2xl p-4 flex flex-col gap-3">
|
||||||
|
<h2 class="flex items-center gap-2 text-[12px] font-bold text-[#3E4C63]">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-[#7A1419]"></span>
|
||||||
|
جزئیات درخواست
|
||||||
|
</h2>
|
||||||
|
<div class="flex items-center gap-3 flex-wrap text-[11px]">
|
||||||
|
<p><span class="font-normal text-[#576989]">گزارش درخواستشده:</span> <span class="font-semibold text-[#7A1419]">{{ modalityTitle(props.case.modality) }}</span></p>
|
||||||
|
<span class="text-[#DEE3EB]">|</span>
|
||||||
|
<p><span class="font-normal text-[#576989]">نوع درخواست:</span> <span class="font-semibold text-[#7A1419]">{{ props.case.priority }}</span></p>
|
||||||
|
<span class="text-[#DEE3EB]">|</span>
|
||||||
|
<p><span class="font-normal text-[#576989]">زمان درخواست:</span> <span class="font-semibold text-[#3E4C63]">{{ props.case.submitted_at ? convertEnglishToPersian(props.case.submitted_at) : '-' }}</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="text-[12px] leading-6">
|
||||||
|
<p class="font-normal text-[#576989]">تاریخچه بیماری:</p>
|
||||||
|
<p class="font-normal text-[#3E4C63] whitespace-pre-line">{{ props.case.note || 'توضیحی ثبت نشده است.' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 flex-wrap pt-2 border-t border-[#EBEBEB]">
|
||||||
|
<UiButton label="دانلود فایل درخواست" severity="secondary" size="small" :disabled="!props.case.primary_files.length" @click="openFiles(props.case.primary_files)" />
|
||||||
|
<UiButton label="دانلود سوابق پزشکی قبلی" severity="secondary" size="small" :disabled="!props.case.history_files.length" @click="openFiles(props.case.history_files)" />
|
||||||
|
<UiButton v-if="!canReport && props.case.report" label="دانلود فایل گزارش" severity="secondary" size="small" :disabled="!props.case.report.attachments.length" @click="openFiles(props.case.report.attachments)" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p v-if="props.case.rejection_reason" class="bg-[#F8E9EA] border border-[#E7C3C5] rounded-2xl p-4 text-[12px] font-normal text-[#7A1419] leading-6">
|
||||||
|
دلیل رد درخواست: {{ props.case.rejection_reason }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<template v-if="canReport">
|
||||||
|
<ReportSectionCard v-model="reportForm.technique" title="تکنیک (Technique)" editable placeholder="شرح تکنیک را وارد کنید" :error="reportForm.errors.technique" />
|
||||||
|
<ReportSectionCard v-model="reportForm.finding" title="یافتهها (Finding)" editable placeholder="یافتهها را وارد کنید" :error="reportForm.errors.finding" />
|
||||||
|
<ReportSectionCard v-model="reportForm.diagnose" title="تشخیص (Diagnose)" editable placeholder="تشخیص را وارد کنید" :error="reportForm.errors.diagnose" />
|
||||||
|
<ReportSectionCard v-model="reportForm.recommendation" title="پیشنهادات (Recommendation)" editable placeholder="پیشنهادات را وارد کنید" :error="reportForm.errors.recommendation" />
|
||||||
|
|
||||||
|
<section class="bg-white border border-complement-50 rounded-2xl p-4 flex flex-col gap-3">
|
||||||
|
<h2 class="flex items-center gap-2 text-[12px] font-bold text-[#3E4C63]">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-[#7A1419]"></span>
|
||||||
|
آپلود فایل و مستندات
|
||||||
|
</h2>
|
||||||
|
<FileUploadBox v-model="reportForm.files" title="آپلود فایل و مستندات گزارش" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="bg-white border border-complement-50 rounded-2xl p-4 flex items-center justify-between flex-wrap gap-3">
|
||||||
|
<p class="text-[11px] font-normal text-[#576989]">با تایید گزارش و یا رد درخواست دیگر امکان ویرایش آن را نخواهید داشت.</p>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button type="button" class="text-[11px] font-semibold text-[#7A1419]" @click="rejectModalVisible = true">رد درخواست</button>
|
||||||
|
<UiButton label="تایید و ثبت گزارش" severity="success" size="small" :loading="reportForm.processing" :disabled="reportForm.processing" @click="submitReport" />
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<ReportSectionCard :model-value="props.case.report?.technique || ''" title="تکنیک (Technique)" />
|
||||||
|
<ReportSectionCard :model-value="props.case.report?.finding || ''" title="یافتهها (Finding)" />
|
||||||
|
<ReportSectionCard :model-value="props.case.report?.diagnose || ''" title="تشخیص (Diagnose)" />
|
||||||
|
<ReportSectionCard :model-value="props.case.report?.recommendation || ''" title="پیشنهادات (Recommendation)" />
|
||||||
|
|
||||||
|
<section class="bg-white border border-complement-50 rounded-2xl p-4 flex flex-col gap-3">
|
||||||
|
<h2 class="flex items-center gap-2 text-[12px] font-bold text-[#3E4C63]">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-[#7A1419]"></span>
|
||||||
|
آپلود فایل و مستندات
|
||||||
|
</h2>
|
||||||
|
<div v-if="props.case.report?.attachments?.length" class="flex flex-wrap gap-4">
|
||||||
|
<a v-for="file in props.case.report.attachments" :key="file.id" :href="file.url" target="_blank" class="relative w-[5rem] h-[5rem] rounded-lg border border-gray-200 overflow-hidden">
|
||||||
|
<img :alt="file.filename" :src="file.url" class="absolute top-0 left-0 w-full h-full object-cover" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p v-else class="w-full min-h-[5rem] bg-[#F8FAFC] border border-[#DEE3EB] rounded-xl p-4 text-[12px] font-normal text-[#576989] leading-6 flex items-center justify-center text-center">
|
||||||
|
عکسی برای این پرونده آپلود نشده است.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="rejectModalVisible" :modal="true" :closable="true" :dismissableMask="false" :style="{ width: '480px' }" class="fast-request-dialog" :pt="{ header: '!bg-client-primaryBlue' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="w-full flex items-center justify-start">
|
||||||
|
<h2 class="text-[14px] font-[700] text-[#576989]">رد درخواست</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="w-full flex flex-col gap-3 border-t border-[#EBEBEB] pt-4">
|
||||||
|
<p class="text-[12px] font-normal text-[#3E4C63] leading-6">پس از رد درخواست، مبلغ فاکتور به کیف پول درخواستکننده بازگردانده میشود.</p>
|
||||||
|
<textarea
|
||||||
|
v-model="rejectReason"
|
||||||
|
rows="4"
|
||||||
|
placeholder="دلیل رد درخواست را وارد کنید"
|
||||||
|
class="w-full bg-[#F8FAFC] border border-[#DEE3EB] rounded-xl p-4 text-[12px] font-normal text-[#3E4C63] leading-6 outline-none focus:border-[#C1D0E8] resize-y"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="w-full pt-3 flex items-center justify-start gap-2 border-t border-[#EBEBEB]">
|
||||||
|
<UiButton label="رد درخواست" severity="danger" size="small" :loading="rejecting" :disabled="rejecting" @click="submitReject" />
|
||||||
|
<UiButton label="انصراف" severity="secondary" size="small" :disabled="rejecting" @click="rejectModalVisible = false" />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</ClientLayout>
|
||||||
|
</template>
|
||||||
@@ -7,9 +7,10 @@ import UiButton from '../../components/ui/UiButton.vue';
|
|||||||
import EyeSvg from '../../components/svg/eye.svg';
|
import EyeSvg from '../../components/svg/eye.svg';
|
||||||
import UiCard from '../../components/ui/UiCard.vue';
|
import UiCard from '../../components/ui/UiCard.vue';
|
||||||
import RequestCard from '../../components/dashboard/RequestCard.vue';
|
import RequestCard from '../../components/dashboard/RequestCard.vue';
|
||||||
import { ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { Link } from '@inertiajs/inertia-vue3';
|
import { Link, usePage } from '@inertiajs/inertia-vue3';
|
||||||
import FastRequestModal from '../../components/dashboard/FastRequestModal.vue';
|
import FastRequestModal from '../../components/dashboard/FastRequestModal.vue';
|
||||||
|
import PaymentResultModal from '../../components/dashboard/PaymentResultModal.vue';
|
||||||
import { useFastRequestStore } from '../../store/FastRequest';
|
import { useFastRequestStore } from '../../store/FastRequest';
|
||||||
import { modalities } from '../../data/fastRequestOptions';
|
import { modalities } from '../../data/fastRequestOptions';
|
||||||
import { caseColumns } from '../../data/casesTableColumns';
|
import { caseColumns } from '../../data/casesTableColumns';
|
||||||
@@ -22,6 +23,17 @@ const openFastRequestModal = (type) => {
|
|||||||
fastRequestStore.requestType = type;
|
fastRequestStore.requestType = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paymentResultVisible = ref(false);
|
||||||
|
const paymentResult = ref(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const result = usePage().props.value.flash?.payment_result;
|
||||||
|
if (result) {
|
||||||
|
paymentResult.value = result;
|
||||||
|
paymentResultVisible.value = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
user: {
|
user: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -30,13 +42,17 @@ defineProps({
|
|||||||
cases: {
|
cases: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientLayout>
|
<ClientLayout>
|
||||||
<ActivitiesWidget />
|
<ActivitiesWidget :stats="stats" />
|
||||||
<UiCard title="ثبت آنی درخواست جدید" variant="default" class="mt-4">
|
<UiCard title="ثبت آنی درخواست جدید" variant="default" class="mt-4">
|
||||||
<p class="w-full flex items-center justify-start font-[400] text-[11px]">فقط چند قدم ساده برای ثبت درخواست کافیه.</p>
|
<p class="w-full flex items-center justify-start font-[400] text-[11px]">فقط چند قدم ساده برای ثبت درخواست کافیه.</p>
|
||||||
<div class="w-full h-[179px] grid grid-cols-5 gap-3 justify-start pt-[10px]">
|
<div class="w-full h-[179px] grid grid-cols-5 gap-3 justify-start pt-[10px]">
|
||||||
@@ -77,5 +93,6 @@ defineProps({
|
|||||||
<ActivitiesChart />
|
<ActivitiesChart />
|
||||||
</UiCard>
|
</UiCard>
|
||||||
<fast-request-modal v-model:visible="fastRequestModalVisible"></fast-request-modal>
|
<fast-request-modal v-model:visible="fastRequestModalVisible"></fast-request-modal>
|
||||||
|
<payment-result-modal v-model:visible="paymentResultVisible" :result="paymentResult"></payment-result-modal>
|
||||||
</ClientLayout>
|
</ClientLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import ClientLayout from '../../../layouts/ClientLayout.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
document: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ClientLayout>
|
|
||||||
<h1>{{ document.title }}</h1>
|
|
||||||
<p>{{ document.description }}</p>
|
|
||||||
<p>{{ document.created_at }}</p>
|
|
||||||
<p>{{ document.updated_at }}</p>
|
|
||||||
<p>{{ document.status }}</p>
|
|
||||||
<p>{{ document.priority }}</p>
|
|
||||||
<p>{{ document.note }}</p>
|
|
||||||
<p>{{ document.modality }}</p>
|
|
||||||
<p>{{ document.pet_id }}</p>
|
|
||||||
<p>{{ document.requester_id }}</p>
|
|
||||||
</ClientLayout>
|
|
||||||
</template>
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import ClientLayout from '../../../layouts/ClientLayout.vue';
|
||||||
|
import TableGeneric from '../../../components/ui/TableGeneric.vue';
|
||||||
|
import UiCard from '../../../components/ui/UiCard.vue';
|
||||||
|
import UiButton from '../../../components/ui/UiButton.vue';
|
||||||
|
import InvoiceModal from '../../../components/dashboard/fastRequest/InvoiceModal.vue';
|
||||||
|
import { invoiceColumns } from '../../../data/invoicesTableColumns';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
invoices: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedInvoice = ref(null);
|
||||||
|
const invoiceModalVisible = ref(false);
|
||||||
|
|
||||||
|
const openInvoice = (invoice) => {
|
||||||
|
selectedInvoice.value = invoice;
|
||||||
|
invoiceModalVisible.value = true;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ClientLayout>
|
||||||
|
<UiCard title="لیست فاکتورها" variant="default">
|
||||||
|
<TableGeneric
|
||||||
|
class="bg-white !rounded-2xl"
|
||||||
|
:data="invoices.data"
|
||||||
|
customStyle="!text-[11px] !font-normal"
|
||||||
|
:searchable="false"
|
||||||
|
:columns="invoiceColumns"
|
||||||
|
:selectable="false"
|
||||||
|
:pagination="invoices.meta"
|
||||||
|
:model-value="[]"
|
||||||
|
>
|
||||||
|
<template #column-actions="{ data }">
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<UiButton
|
||||||
|
v-if="data.status === 'در انتظار پرداخت'"
|
||||||
|
label="پرداخت"
|
||||||
|
severity="success"
|
||||||
|
size="small"
|
||||||
|
@click="openInvoice(data)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TableGeneric>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<InvoiceModal v-model:visible="invoiceModalVisible" :invoice="selectedInvoice" />
|
||||||
|
</ClientLayout>
|
||||||
|
</template>
|
||||||
@@ -1,11 +1,42 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ClientLayout from '../../../layouts/ClientLayout.vue'
|
import ClientLayout from '../../../layouts/ClientLayout.vue';
|
||||||
|
import TableGeneric from '../../../components/ui/TableGeneric.vue';
|
||||||
|
import UiCard from '../../../components/ui/UiCard.vue';
|
||||||
|
import EyeSvg from '../../../components/svg/eye.svg';
|
||||||
|
import { Link } from '@inertiajs/inertia-vue3';
|
||||||
|
import { reportColumns } from '../../../data/reportsTableColumns';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
reports: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientLayout>
|
<ClientLayout>
|
||||||
<h1 class="text-2xl font-bold">
|
<UiCard title="گزارشات" variant="default">
|
||||||
Reports
|
<TableGeneric
|
||||||
</h1>
|
class="bg-white !rounded-2xl"
|
||||||
|
:data="reports.data"
|
||||||
|
customStyle="!text-[11px] !font-normal"
|
||||||
|
:searchable="false"
|
||||||
|
:columns="reportColumns"
|
||||||
|
:selectable="false"
|
||||||
|
:pagination="reports.meta"
|
||||||
|
:model-value="[]"
|
||||||
|
>
|
||||||
|
<template #column-actions="{ data }">
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<Link :href="`/dashboard/cases/${data.document?.id}`"
|
||||||
|
class="text-primary-500 bg-complement-100 flex items-center justify-end p-1.5 rounded-md"
|
||||||
|
>
|
||||||
|
<EyeSvg class="w-4 h-4 text-primary-500" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TableGeneric>
|
||||||
|
</UiCard>
|
||||||
</ClientLayout>
|
</ClientLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { usePage } from '@inertiajs/inertia-vue3';
|
||||||
|
import { toast } from 'vue3-toastify';
|
||||||
|
import ClientLayout from '../../../layouts/ClientLayout.vue';
|
||||||
|
import TableGeneric from '../../../components/ui/TableGeneric.vue';
|
||||||
|
import UiCard from '../../../components/ui/UiCard.vue';
|
||||||
|
import UiInput from '../../../components/ui/UiInput.vue';
|
||||||
|
import UiButton from '../../../components/ui/UiButton.vue';
|
||||||
|
import WalletSvg from '../../../components/svg/wallet.svg';
|
||||||
|
import WithdrawRequestCard from '../../../components/dashboard/wallet/WithdrawRequestCard.vue';
|
||||||
|
import { walletTransactionColumns } from '../../../data/walletTransactionsTableColumns';
|
||||||
|
import { withdrawRequestColumns } from '../../../data/withdrawRequestsTableColumns';
|
||||||
|
import { convertEnglishToPersian, convertPersianToEnglish, formatPrice } from '../../../utils/numberUtils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
transactions: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
withdrawRequests: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
bankCards: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { props: pageProps } = usePage();
|
||||||
|
|
||||||
|
const quickAmounts = [100000, 200000, 500000];
|
||||||
|
|
||||||
|
const amount = ref('');
|
||||||
|
const depositing = ref(false);
|
||||||
|
|
||||||
|
const numericAmount = computed(() => Number(convertPersianToEnglish(amount.value).replace(/\D/g, '')) || 0);
|
||||||
|
|
||||||
|
const formattedBalance = computed(() => formatPrice(pageProps.value.auth?.wallet_balance || 0));
|
||||||
|
|
||||||
|
const pendingWithdrawAmount = computed(() => props.withdrawRequests
|
||||||
|
.filter((request) => request.status === 'در انتظار بررسی')
|
||||||
|
.reduce((sum, request) => sum + request.amount, 0));
|
||||||
|
|
||||||
|
const withdrawStatusClasses = (status) => {
|
||||||
|
if (status === 'تایید شده') {
|
||||||
|
return 'text-[#1F7A4D] bg-[#E6F4EC]';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'رد شده') {
|
||||||
|
return 'text-[#7A1419] bg-[#F8E9EA]';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'text-[#576989] bg-[#EEF2F8]';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCardNumber = (cardNumber) => convertEnglishToPersian(cardNumber.replace(/(\d{4})(?=\d)/g, '$1 - '));
|
||||||
|
|
||||||
|
const selectQuickAmount = (value) => {
|
||||||
|
amount.value = String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeposit = async () => {
|
||||||
|
if (numericAmount.value < 10000) {
|
||||||
|
toast.error('حداقل مبلغ شارژ ۱۰٬۰۰۰ تومان است.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
depositing.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await window.axios.post('/dashboard/wallet/deposit', { amount: numericAmount.value });
|
||||||
|
window.location.href = data.url;
|
||||||
|
} catch (error) {
|
||||||
|
depositing.value = false;
|
||||||
|
toast.error(error.response?.data?.message || 'خطا در اتصال به درگاه پرداخت. لطفا دوباره تلاش کنید.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ClientLayout>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<UiCard title="کیف پول" variant="default">
|
||||||
|
<div class="grid grid-cols-2 max-sm:grid-cols-1 gap-4">
|
||||||
|
<div class="flex items-center gap-3 bg-[#EEF2F8] border border-[#DEE3EB] rounded-[12px] px-5 py-4">
|
||||||
|
<WalletSvg class="w-6 h-6" />
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-[11px] font-normal text-[#576989]">موجودی کیف پول</span>
|
||||||
|
<strong class="text-[16px] font-bold text-[#3E4C63]">{{ formattedBalance }} تومان</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 bg-[#F8FAFC] border border-[#DEE3EB] rounded-[12px] px-5 py-4">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-[11px] font-normal text-[#576989]">مبلغ در انتظار برداشت</span>
|
||||||
|
<strong class="text-[16px] font-bold text-[#3E4C63]">{{ formatPrice(pendingWithdrawAmount) }} تومان</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard title="شارژ کیف پول" variant="default">
|
||||||
|
<form class="flex flex-col gap-4" @submit.prevent="handleDeposit">
|
||||||
|
<p class="text-[11px] font-normal text-[#576989] leading-6">
|
||||||
|
مبلغ مورد نظر را وارد کنید؛ پس از پرداخت از طریق درگاه بانکی، موجودی کیف پول شما بلافاصله افزایش مییابد.
|
||||||
|
</p>
|
||||||
|
<div class="flex items-end gap-3 max-sm:flex-col max-sm:items-stretch">
|
||||||
|
<UiInput
|
||||||
|
v-model="amount"
|
||||||
|
label="مبلغ شارژ"
|
||||||
|
placeholder="مبلغ مورد نظر را به تومان وارد کنید"
|
||||||
|
prefixEnd="تومان"
|
||||||
|
prefixClasses="!border-dominant-50"
|
||||||
|
class="!w-[22rem] max-sm:!w-full"
|
||||||
|
customClass="!border-dominant-50 !font-normal !text-[12px]"
|
||||||
|
/>
|
||||||
|
<UiButton
|
||||||
|
label="افزایش موجودی"
|
||||||
|
severity="success"
|
||||||
|
size="small"
|
||||||
|
:loading="depositing"
|
||||||
|
:disabled="depositing"
|
||||||
|
@click="handleDeposit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<button
|
||||||
|
v-for="value in quickAmounts"
|
||||||
|
:key="value"
|
||||||
|
type="button"
|
||||||
|
class="text-[11px] font-normal text-[#576989] bg-[#F5F7FA] border border-[#DEE3EB] rounded-lg px-3 py-1.5 hover:bg-[#EEF2F8]"
|
||||||
|
@click="selectQuickAmount(value)"
|
||||||
|
>
|
||||||
|
{{ formatPrice(value) }} تومان
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<WithdrawRequestCard :bank-cards="bankCards" />
|
||||||
|
|
||||||
|
<UiCard title="درخواستهای برداشت" variant="default">
|
||||||
|
<TableGeneric
|
||||||
|
class="bg-white !rounded-2xl"
|
||||||
|
:data="withdrawRequests"
|
||||||
|
customStyle="!text-[11px] !font-normal"
|
||||||
|
:searchable="false"
|
||||||
|
:columns="withdrawRequestColumns"
|
||||||
|
:selectable="false"
|
||||||
|
:pagination="null"
|
||||||
|
:model-value="[]"
|
||||||
|
>
|
||||||
|
<template #column-bank_card="{ data }">
|
||||||
|
<span v-if="data.bank_card" dir="ltr">{{ formatCardNumber(data.bank_card.card_number) }}</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
<template #column-status="{ data }">
|
||||||
|
<span class="text-[11px] font-normal rounded-md px-2 py-1" :class="withdrawStatusClasses(data.status)">
|
||||||
|
{{ data.status }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</TableGeneric>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard title="تراکنشها" variant="default">
|
||||||
|
<TableGeneric
|
||||||
|
class="bg-white !rounded-2xl"
|
||||||
|
:data="transactions.data"
|
||||||
|
customStyle="!text-[11px] !font-normal"
|
||||||
|
:searchable="false"
|
||||||
|
:columns="walletTransactionColumns"
|
||||||
|
:selectable="false"
|
||||||
|
:pagination="transactions.meta"
|
||||||
|
:model-value="[]"
|
||||||
|
>
|
||||||
|
<template #column-type="{ data }">
|
||||||
|
<span
|
||||||
|
class="text-[11px] font-normal rounded-md px-2 py-1"
|
||||||
|
:class="data.type === 'واریز' ? 'text-[#1F7A4D] bg-[#E6F4EC]' : 'text-[#7A1419] bg-[#F8E9EA]'"
|
||||||
|
>
|
||||||
|
{{ data.type }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</TableGeneric>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
</ClientLayout>
|
||||||
|
</template>
|
||||||
@@ -1,18 +1,24 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Link } from '@inertiajs/inertia-vue3'
|
|
||||||
import ActivityCard from './ActivityCard.vue'
|
import ActivityCard from './ActivityCard.vue'
|
||||||
import { formatPrice } from '../../utils/numberUtils'
|
import { formatPrice } from '../../utils/numberUtils'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
stats: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="grid grid-cols-4 max-lg:grid-cols-2 max-sm:grid-cols-1 gap-4">
|
<section class="grid grid-cols-4 max-lg:grid-cols-2 max-sm:grid-cols-1 gap-4">
|
||||||
<ActivityCard title="کیس در انتظار بررسی"
|
<ActivityCard title="کیس در انتظار بررسی"
|
||||||
:count="5" link="/dashboard/doctor/cases" label-link="مشاهده و بررسی" />
|
:count="stats.pending_cases" link="/dashboard/cases" label-link="مشاهده و بررسی" />
|
||||||
<ActivityCard title="کیس بررسی شده در ماه"
|
<ActivityCard title="کیس بررسی شده در ماه"
|
||||||
:count="999" link="/dashboard/doctor/cases" label-link="مشاهده گزارشات کیسها" />
|
:count="stats.monthly_reviewed" link="/dashboard/reports" label-link="مشاهده گزارشات کیسها" />
|
||||||
<ActivityCard title="تومان درآمد"
|
<ActivityCard :title="stats.is_doctor ? 'تومان درآمد' : 'تومان هزینه در ماه'"
|
||||||
:count="formatPrice(50000000)" link="/dashboard/doctor/cases" label-link="مشاهده گزارشات مالی " />
|
:count="formatPrice(stats.monthly_amount)" :link="stats.is_doctor ? '/dashboard/wallet' : '/dashboard/invoices'" label-link="مشاهده گزارشات مالی" />
|
||||||
<ActivityCard title="ساعت تا شیت بعدی"
|
<ActivityCard v-if="stats.next_shift_hours !== null" title="ساعت تا شیفت بعدی"
|
||||||
:count="4" link="/dashboard/doctor/cases" label-link="مشاهده شیفتها" />
|
:count="stats.next_shift_hours" link="/dashboard/shifts" label-link="مشاهده شیفتها" />
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { Inertia } from '@inertiajs/inertia';
|
||||||
|
import { usePage } from '@inertiajs/inertia-vue3';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import UiButton from '../ui/UiButton.vue';
|
||||||
|
import UiInput from '../ui/UiInput.vue';
|
||||||
|
import { useGlobalStore } from '../../store/Global';
|
||||||
|
|
||||||
|
const visible = defineModel('visible');
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
const { props } = usePage();
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
fullname: '',
|
||||||
|
birthdate: '',
|
||||||
|
national_code: '',
|
||||||
|
email: '',
|
||||||
|
province: '',
|
||||||
|
city: '',
|
||||||
|
address: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatarFile = ref(null);
|
||||||
|
const avatarPreview = ref('');
|
||||||
|
const avatarInput = ref(null);
|
||||||
|
const errors = ref({});
|
||||||
|
const submitting = ref(false);
|
||||||
|
|
||||||
|
const avatarUrl = computed(() => avatarPreview.value || globalStore.current_user.profile_url || '');
|
||||||
|
|
||||||
|
watch(visible, (isOpen) => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
const user = props.value.auth?.User || {};
|
||||||
|
form.value = {
|
||||||
|
fullname: user.fullname || '',
|
||||||
|
birthdate: user.birthdate || '',
|
||||||
|
national_code: user.national_code || '',
|
||||||
|
email: user.email || '',
|
||||||
|
province: user.province || '',
|
||||||
|
city: user.city || '',
|
||||||
|
address: user.address || '',
|
||||||
|
};
|
||||||
|
avatarFile.value = null;
|
||||||
|
avatarPreview.value = '';
|
||||||
|
errors.value = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorFor = (field) => {
|
||||||
|
const error = errors.value[field];
|
||||||
|
return Array.isArray(error) ? error[0] : (error || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAvatarPicker = () => {
|
||||||
|
avatarInput.value?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarChange = (event) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
avatarFile.value = file;
|
||||||
|
avatarPreview.value = URL.createObjectURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
submitting.value = true;
|
||||||
|
Inertia.post('/dashboard/profile', {
|
||||||
|
...form.value,
|
||||||
|
avatar: avatarFile.value,
|
||||||
|
}, {
|
||||||
|
forceFormData: true,
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
globalStore.current_user = props.value.auth.User;
|
||||||
|
visible.value = false;
|
||||||
|
},
|
||||||
|
onError: (formErrors) => {
|
||||||
|
errors.value = formErrors;
|
||||||
|
},
|
||||||
|
onFinish: () => {
|
||||||
|
submitting.value = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="false" :maximizable="false" :style="{ width: '688px' , minHeight: '376px' }" class="fast-request-dialog" :pt="{header : '!bg-client-primaryBlue'}">
|
||||||
|
<template #header>
|
||||||
|
<div class="w-full flex items-center justify-start">
|
||||||
|
<h2 class="text-[14px] font-[700] text-[#576989]">ویرایش حساب کاربری</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<form class="w-full flex flex-col gap-5 border-t border-[#EBEBEB] pt-5" @submit.prevent="submit">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="relative">
|
||||||
|
<span class="flex items-center justify-center w-20 h-20 rounded-full bg-[#F6F7F9] border border-[#DEE3EB] overflow-hidden">
|
||||||
|
<img v-if="avatarUrl" :src="avatarUrl" alt="تصویر پروفایل" class="w-full h-full object-cover" />
|
||||||
|
<i v-else class="fi fi-rr-user flex text-2xl text-[#576989]"></i>
|
||||||
|
</span>
|
||||||
|
<button type="button" class="absolute -bottom-1 -left-1 flex items-center justify-center w-7 h-7 rounded-full bg-dominant text-white border-2 border-white cursor-pointer" @click="openAvatarPicker">
|
||||||
|
<i class="fi fi-rr-camera flex text-[11px]"></i>
|
||||||
|
</button>
|
||||||
|
<input ref="avatarInput" type="file" accept="image/png,image/jpeg" class="hidden" @change="handleAvatarChange" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-[13px] font-[700] text-[#3E4C63]">تصویر پروفایل</span>
|
||||||
|
<span class="text-[11px] font-[400] text-[#576989]">فرمتهای مجاز: png ،jpg — حداکثر ۱۰ مگابایت</span>
|
||||||
|
<span v-if="errorFor('avatar')" class="text-[11px] text-[#7A1419]">{{ errorFor('avatar') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 max-sm:grid-cols-1 gap-4">
|
||||||
|
<UiInput v-model="form.fullname" label="نام و نام خانوادگی" placeholder="نام و نام خانوادگی خود را وارد کنید" :error="errorFor('fullname')" />
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="ui-input-label">تاریخ تولد</label>
|
||||||
|
<CustomDatePicker v-model="form.birthdate" type="date" format="YYYY-MM-DD" display-format="jYYYY/jMM/jDD" class="w-full" />
|
||||||
|
<span v-if="errorFor('birthdate')" class="text-[11px] text-[#7A1419]">{{ errorFor('birthdate') }}</span>
|
||||||
|
</div>
|
||||||
|
<UiInput v-model="form.national_code" label="کد ملی" placeholder="کد ملی خود را وارد کنید" convertPersianNumbers :error="errorFor('national_code')" />
|
||||||
|
<UiInput v-model="form.email" label="ایمیل" type="email" placeholder="ایمیل خود را وارد کنید" direction="ltr" :error="errorFor('email')" />
|
||||||
|
<UiInput v-model="form.province" label="استان" placeholder="استان محل سکونت" :error="errorFor('province')" />
|
||||||
|
<UiInput v-model="form.city" label="شهر" placeholder="شهر محل سکونت" :error="errorFor('city')" />
|
||||||
|
</div>
|
||||||
|
<UiInput v-model="form.address" label="آدرس" placeholder="آدرس خود را وارد کنید" :error="errorFor('address')" />
|
||||||
|
<div class="w-full pt-3 flex items-center justify-start gap-2 border-t border-[#EBEBEB]">
|
||||||
|
<UiButton label="ذخیره تغییرات" severity="success" size="small" :loading="submitting" :disabled="submitting" @click="submit" />
|
||||||
|
<UiButton label="انصراف" severity="secondary" size="small" :disabled="submitting" @click="visible = false" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import UiButton from '../../components/ui/UiButton.vue';
|
import UiButton from '../../components/ui/UiButton.vue';
|
||||||
import { computed, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import Dialog from 'primevue/dialog';
|
import Dialog from 'primevue/dialog';
|
||||||
import ProgressBar from 'primevue/progressbar';
|
import ProgressBar from 'primevue/progressbar';
|
||||||
import { toast } from 'vue3-toastify';
|
import { toast } from 'vue3-toastify';
|
||||||
@@ -9,11 +9,15 @@ import RequestCase from './fastRequest/RequestCase.vue';
|
|||||||
import RequestDetails from './fastRequest/RequestDetails.vue';
|
import RequestDetails from './fastRequest/RequestDetails.vue';
|
||||||
import RequestDoctor from './fastRequest/RequestDoctor.vue';
|
import RequestDoctor from './fastRequest/RequestDoctor.vue';
|
||||||
import RequestReview from './fastRequest/RequestReview.vue';
|
import RequestReview from './fastRequest/RequestReview.vue';
|
||||||
|
import InvoiceModal from './fastRequest/InvoiceModal.vue';
|
||||||
import { useFastRequestStore } from '../../store/FastRequest';
|
import { useFastRequestStore } from '../../store/FastRequest';
|
||||||
|
|
||||||
const visible = defineModel('visible');
|
const visible = defineModel('visible');
|
||||||
const fastRequestStore = useFastRequestStore();
|
const fastRequestStore = useFastRequestStore();
|
||||||
|
|
||||||
|
const invoiceVisible = ref(false);
|
||||||
|
const invoiceDocument = ref(null);
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{ label: 'نوع درخواست', component: RequestType },
|
{ label: 'نوع درخواست', component: RequestType },
|
||||||
{ label: 'انتخاب کیس', component: RequestCase },
|
{ label: 'انتخاب کیس', component: RequestCase },
|
||||||
@@ -49,8 +53,9 @@ const handleNext = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const document = await fastRequestStore.submit();
|
const document = await fastRequestStore.submit();
|
||||||
toast.success(`درخواست شما با شماره پرونده ${document.code} با موفقیت ثبت شد.`);
|
invoiceDocument.value = document;
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
|
invoiceVisible.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error.response?.data?.message || 'خطا در ثبت درخواست. لطفا دوباره تلاش کنید.');
|
toast.error(error.response?.data?.message || 'خطا در ثبت درخواست. لطفا دوباره تلاش کنید.');
|
||||||
}
|
}
|
||||||
@@ -93,4 +98,5 @@ const handleNext = async () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<invoice-modal v-model:visible="invoiceVisible" :document="invoiceDocument"></invoice-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import UiButton from '../ui/UiButton.vue';
|
||||||
|
import PaymentSuccessSvg from '../svg/paymentSuccess.svg';
|
||||||
|
import PaymentFailedSvg from '../svg/paymentFailed.svg';
|
||||||
|
|
||||||
|
const visible = defineModel('visible');
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
result: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPaid = computed(() => props.result?.status === 'paid');
|
||||||
|
|
||||||
|
const isWalletTopUp = computed(() => props.result?.purpose === 'WalletTopUp');
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="false" :maximizable="false" :style="{ width: '688px' , minHeight: '376px' }" class="fast-request-dialog">
|
||||||
|
<div v-if="result" class="w-full flex flex-col items-center justify-center gap-2 py-6">
|
||||||
|
<component :is="isPaid ? PaymentSuccessSvg : PaymentFailedSvg" class="w-[180px] h-[150px]" />
|
||||||
|
<h2 class="text-[15px] font-[700] text-[#3E4C63] mt-2">
|
||||||
|
<template v-if="isPaid">{{ isWalletTopUp ? 'کیف پول شما با موفقیت شارژ شد.' : 'پرداخت شما با موفقیت انجام شد.' }}</template>
|
||||||
|
<template v-else>متاسفانه پرداخت شما با خطا روبهرو شده است.</template>
|
||||||
|
</h2>
|
||||||
|
<p class="max-w-[480px] text-center text-[13px] font-[400] text-[#576989] leading-7">
|
||||||
|
<template v-if="isPaid && isWalletTopUp">مبلغ پرداختی به موجودی کیف پول شما اضافه شد و میتوانید از آن برای پرداخت فاکتورها استفاده کنید.</template>
|
||||||
|
<template v-else-if="isPaid">کارشناسان پرداخت و درخواست شما را به زودی بررسی خواهند کرد.</template>
|
||||||
|
<template v-else>لطفا دوباره نسبت به پرداخت اقدام نمایید. در صورتیکه مبلغ از حساب شما کسر شده و یا فکر میکنید اشتباهی رخ داده است، با کارشناسان ما در تماس باشید.</template>
|
||||||
|
</p>
|
||||||
|
<p class="text-[13px] font-[700] text-[#3E4C63] mt-2">شماره پیگیری: {{ result.track_id }}</p>
|
||||||
|
<div class="flex items-center justify-center gap-2 mt-4">
|
||||||
|
<ui-button label="بازگشت به داشبورد" severity="success" size="small" @click="closeModal" />
|
||||||
|
<ui-button v-if="!isPaid" label="تماس با پشتیبانی" severity="secondary" size="small" @click="closeModal" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
rows: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="bg-white border border-complement-50 rounded-2xl p-4">
|
||||||
|
<h2 class="flex items-center gap-2 text-[12px] font-bold text-[#3E4C63]">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-[#7A1419]"></span>
|
||||||
|
{{ title }}
|
||||||
|
</h2>
|
||||||
|
<ul class="mt-3 flex flex-col gap-2">
|
||||||
|
<li v-for="row in rows" :key="row.label" class="flex items-start gap-2 text-[11px]">
|
||||||
|
<span class="font-normal text-[#576989]">{{ row.label }}:</span>
|
||||||
|
<span class="font-semibold text-[#3E4C63]">{{ row.value ?? '-' }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup>
|
||||||
|
const value = defineModel({ type: String, default: '' })
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="bg-white border border-complement-50 rounded-2xl p-4 flex flex-col gap-3">
|
||||||
|
<h2 class="flex items-center gap-2 text-[12px] font-bold text-[#3E4C63]">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-[#7A1419]"></span>
|
||||||
|
{{ title }}
|
||||||
|
</h2>
|
||||||
|
<textarea
|
||||||
|
v-if="editable"
|
||||||
|
v-model="value"
|
||||||
|
rows="6"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
class="w-full bg-[#F8FAFC] border border-[#DEE3EB] rounded-xl p-4 text-[12px] font-normal text-[#3E4C63] leading-6 outline-none focus:border-[#C1D0E8] resize-y"
|
||||||
|
></textarea>
|
||||||
|
<div v-else class="w-full min-h-[7rem] bg-[#F8FAFC] border border-[#DEE3EB] rounded-xl p-4 text-[12px] font-normal text-[#3E4C63] leading-6 whitespace-pre-line">
|
||||||
|
{{ value || 'موردی ثبت نشده است.' }}
|
||||||
|
</div>
|
||||||
|
<p v-if="error" class="text-[11px] text-[#7A1419]">{{ error }}</p>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import Dialog from 'primevue/dialog';
|
import Dialog from 'primevue/dialog';
|
||||||
|
import { usePage } from '@inertiajs/inertia-vue3';
|
||||||
|
import { Inertia } from '@inertiajs/inertia';
|
||||||
import UiButton from '../../ui/UiButton.vue';
|
import UiButton from '../../ui/UiButton.vue';
|
||||||
|
import { toast } from 'vue3-toastify';
|
||||||
import { modalityTitle } from '../../../data/fastRequestOptions';
|
import { modalityTitle } from '../../../data/fastRequestOptions';
|
||||||
|
import { formatPrice } from '../../../utils/numberUtils';
|
||||||
|
|
||||||
const visible = defineModel('visible');
|
const visible = defineModel('visible');
|
||||||
|
|
||||||
@@ -10,31 +14,69 @@ const props = defineProps({
|
|||||||
document: {
|
document: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
invoice: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const formattedAmount = computed(() => {
|
const { props: pageProps } = usePage();
|
||||||
return Number(props.document?.invoice?.amount || 0).toLocaleString('fa-IR');
|
|
||||||
});
|
|
||||||
|
|
||||||
const handlePay = () => {
|
const paying = ref(false);
|
||||||
|
const payingWithWallet = ref(false);
|
||||||
|
|
||||||
|
const currentInvoice = computed(() => props.invoice ?? props.document?.invoice ?? null);
|
||||||
|
|
||||||
|
const formattedAmount = computed(() => formatPrice(currentInvoice.value?.amount || 0));
|
||||||
|
|
||||||
|
const walletBalance = computed(() => Number(pageProps.value.auth?.wallet_balance || 0));
|
||||||
|
|
||||||
|
const formattedWalletBalance = computed(() => formatPrice(walletBalance.value));
|
||||||
|
|
||||||
|
const hasEnoughBalance = computed(() => walletBalance.value >= Number(currentInvoice.value?.amount || 0));
|
||||||
|
|
||||||
|
const handlePay = async () => {
|
||||||
|
paying.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await window.axios.post('/dashboard/payments', { invoice_id: currentInvoice.value.id });
|
||||||
|
window.location.href = data.url;
|
||||||
|
} catch (error) {
|
||||||
|
paying.value = false;
|
||||||
|
toast.error(error.response?.data?.message || 'خطا در اتصال به درگاه پرداخت. لطفا دوباره تلاش کنید.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWalletPay = async () => {
|
||||||
|
payingWithWallet.value = true;
|
||||||
|
try {
|
||||||
|
await window.axios.post(`/dashboard/invoices/${currentInvoice.value.id}/pay-wallet`);
|
||||||
|
toast.success('فاکتور با موفقیت از کیف پول پرداخت شد.');
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
|
Inertia.reload();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error.response?.data?.message || 'خطا در پرداخت از کیف پول. لطفا دوباره تلاش کنید.');
|
||||||
|
} finally {
|
||||||
|
payingWithWallet.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="false" :maximizable="false" :style="{ width: '1032px' }">
|
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="false" :maximizable="false" :style="{ width: '688px' , minHeight: '376px' }" class="fast-request-dialog" :pt="{header : '!bg-client-primaryBlue'}">
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2 class="text-[14px] font-[700] text-[#576989]">فاکتور پرداخت «{{ document?.invoice?.number }}»</h2>
|
<div class="w-full flex items-center justify-start">
|
||||||
|
<h2 class="text-[14px] font-[700] text-[#576989]">فاکتور پرداخت «{{ currentInvoice?.number }}»</h2>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="document" class="w-full flex flex-col gap-4 border-t border-[#EBEBEB] pt-4">
|
<div v-if="currentInvoice" class="w-full flex flex-col gap-4 border-t border-[#EBEBEB] pt-4">
|
||||||
<div class="w-full flex items-center gap-3 bg-[#EEF2F8] border border-[#DEE3EB] rounded-[12px] px-4 py-4">
|
<div class="w-full flex items-center gap-3 bg-[#EEF2F8] border border-[#DEE3EB] rounded-[12px] px-4 py-3">
|
||||||
<i class="fi-rr-info text-[20px] text-[#576989]"></i>
|
<i class="fi-rr-info text-[18px] text-[#576989]"></i>
|
||||||
<p class="text-[13px] font-[400] text-[#3E4C63]">
|
<p class="text-[12px] font-[400] text-[#3E4C63] leading-6">
|
||||||
یک فاکتور به مبلغ « <span class="font-[700]">{{ formattedAmount }} تومان</span> » برای شما ایجاد شد. لطفا جهت ادامه مراحل درخواست نسبت به پرداخت اقدام نمایید.
|
یک فاکتور به مبلغ « <span class="font-[700]">{{ formattedAmount }} تومان</span> » برای شما ایجاد شد. لطفا جهت ادامه مراحل درخواست نسبت به پرداخت اقدام نمایید.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[13px] font-[400] text-[#3E4C63] leading-7">
|
<p v-if="document" class="text-[13px] font-[400] text-[#3E4C63] leading-7">
|
||||||
این فاکتور جهت پردازش درخواست
|
این فاکتور جهت پردازش درخواست
|
||||||
<span class="font-[600] text-[#7A1419]">(({{ modalityTitle(document.modality) }}))</span>
|
<span class="font-[600] text-[#7A1419]">(({{ modalityTitle(document.modality) }}))</span>
|
||||||
به شناسه
|
به شناسه
|
||||||
@@ -48,9 +90,14 @@ const handlePay = () => {
|
|||||||
<br>
|
<br>
|
||||||
بدیهیست که در صورت عدم تایید کارشناسان مبلغ مربوطه به کیف پول شما بازگردانده خواهد شد.
|
بدیهیست که در صورت عدم تایید کارشناسان مبلغ مربوطه به کیف پول شما بازگردانده خواهد شد.
|
||||||
</p>
|
</p>
|
||||||
|
<p class="text-[12px] font-[400] text-[#576989]">
|
||||||
|
موجودی کیف پول شما: <span class="font-[700] text-[#3E4C63]">{{ formattedWalletBalance }} تومان</span>
|
||||||
|
<span v-if="!hasEnoughBalance" class="text-[#7A1419]"> (موجودی کیف پول کافی نیست)</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full pt-3 flex items-center justify-start border-t border-[#EBEBEB]">
|
<div class="w-full pt-3 flex items-center justify-start gap-2 border-t border-[#EBEBEB]">
|
||||||
<ui-button label="تایید و پرداخت" severity="success" size="small" @click="handlePay" />
|
<ui-button label="پرداخت با کیف پول" severity="primary" size="small" :loading="payingWithWallet" :disabled="payingWithWallet || paying || !hasEnoughBalance" @click="handleWalletPay" />
|
||||||
|
<ui-button label="پرداخت از طریق درگاه" severity="success" size="small" :loading="paying" :disabled="paying || payingWithWallet" @click="handlePay" />
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useForm, usePage } from '@inertiajs/inertia-vue3';
|
||||||
|
import { Inertia } from '@inertiajs/inertia';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import { toast } from 'vue3-toastify';
|
||||||
|
import UiCard from '../../ui/UiCard.vue';
|
||||||
|
import UiInput from '../../ui/UiInput.vue';
|
||||||
|
import UiButton from '../../ui/UiButton.vue';
|
||||||
|
import { convertEnglishToPersian, convertPersianToEnglish, formatPrice } from '../../../utils/numberUtils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
bankCards: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { props: pageProps } = usePage();
|
||||||
|
|
||||||
|
const walletBalance = computed(() => Number(pageProps.value.auth?.wallet_balance || 0));
|
||||||
|
|
||||||
|
const amount = ref('');
|
||||||
|
const selectedCardId = ref(props.bankCards[0]?.id ?? null);
|
||||||
|
|
||||||
|
const numericAmount = computed(() => Number(convertPersianToEnglish(amount.value).replace(/\D/g, '')) || 0);
|
||||||
|
|
||||||
|
const formatCardNumber = (cardNumber) => convertEnglishToPersian(cardNumber.replace(/(\d{4})(?=\d)/g, '$1 - '));
|
||||||
|
|
||||||
|
const selectFullBalance = () => {
|
||||||
|
amount.value = String(walletBalance.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const withdrawForm = useForm({
|
||||||
|
amount: null,
|
||||||
|
bank_card_id: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitWithdraw = () => {
|
||||||
|
if (numericAmount.value < 10000) {
|
||||||
|
toast.error('حداقل مبلغ برداشت ۱۰٬۰۰۰ تومان است.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedCardId.value) {
|
||||||
|
toast.error('لطفا کارت بانکی مقصد را انتخاب کنید.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
withdrawForm.amount = numericAmount.value;
|
||||||
|
withdrawForm.bank_card_id = selectedCardId.value;
|
||||||
|
|
||||||
|
withdrawForm.post('/dashboard/wallet/withdraw-requests', {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
amount.value = '';
|
||||||
|
toast.success('درخواست برداشت شما ثبت شد و در انتظار بررسی است.');
|
||||||
|
},
|
||||||
|
onError: (errors) => toast.error(Object.values(errors)[0] || 'خطا در ثبت درخواست برداشت.'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardModalVisible = ref(false);
|
||||||
|
|
||||||
|
const cardForm = useForm({
|
||||||
|
card_number: '',
|
||||||
|
holder_name: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitCard = () => {
|
||||||
|
cardForm.card_number = convertPersianToEnglish(cardForm.card_number).replace(/\D/g, '');
|
||||||
|
|
||||||
|
cardForm.post('/dashboard/wallet/bank-cards', {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
cardForm.reset();
|
||||||
|
cardModalVisible.value = false;
|
||||||
|
toast.success('کارت بانکی با موفقیت ثبت شد.');
|
||||||
|
},
|
||||||
|
onError: (errors) => toast.error(Object.values(errors)[0] || 'خطا در ثبت کارت بانکی.'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCard = (card) => {
|
||||||
|
Inertia.delete(`/dashboard/wallet/bank-cards/${card.id}`, {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
if (selectedCardId.value === card.id) {
|
||||||
|
selectedCardId.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiCard title="درخواست برداشت" variant="default">
|
||||||
|
<div class="grid grid-cols-2 max-lg:grid-cols-1 gap-6 items-start">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<p class="text-[11px] font-normal text-[#576989] leading-6">
|
||||||
|
مبلغ مورد نظر پس از بررسی و تایید توسط کارشناسان، از کیف پول شما کسر و به کارت انتخابی واریز میشود.
|
||||||
|
</p>
|
||||||
|
<div class="flex items-end gap-3 max-sm:flex-col max-sm:items-stretch">
|
||||||
|
<UiInput
|
||||||
|
v-model="amount"
|
||||||
|
label="مبلغ برداشت"
|
||||||
|
placeholder="مبلغ مورد نظر را به تومان وارد کنید"
|
||||||
|
prefixEnd="تومان"
|
||||||
|
prefixClasses="!border-dominant-50"
|
||||||
|
class="!w-full"
|
||||||
|
customClass="!border-dominant-50 !font-normal !text-[12px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-[11px] font-normal text-[#576989] bg-[#F5F7FA] border border-[#DEE3EB] rounded-lg px-3 py-1.5 hover:bg-[#EEF2F8]"
|
||||||
|
@click="selectFullBalance"
|
||||||
|
>
|
||||||
|
کل موجودی ({{ formatPrice(walletBalance) }} تومان)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<UiButton
|
||||||
|
label="ثبت درخواست برداشت"
|
||||||
|
severity="success"
|
||||||
|
size="small"
|
||||||
|
class="self-start"
|
||||||
|
:loading="withdrawForm.processing"
|
||||||
|
:disabled="withdrawForm.processing"
|
||||||
|
@click="submitWithdraw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-[11px] font-semibold text-[#3E4C63]">کارت بانکی مقصد</span>
|
||||||
|
<button type="button" class="text-[11px] font-semibold text-[#365891]" @click="cardModalVisible = true">
|
||||||
|
+ افزودن کارت
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p v-if="!bankCards.length" class="bg-[#F8FAFC] border border-dashed border-[#C1D0E8] rounded-xl p-4 text-[11px] font-normal text-[#576989] text-center leading-6">
|
||||||
|
هنوز کارتی ثبت نکردهاید. برای ثبت درخواست برداشت ابتدا یک کارت بانکی اضافه کنید.
|
||||||
|
</p>
|
||||||
|
<label
|
||||||
|
v-for="card in bankCards"
|
||||||
|
:key="card.id"
|
||||||
|
class="flex items-center gap-3 border rounded-xl px-4 py-3 cursor-pointer transition-colors"
|
||||||
|
:class="selectedCardId === card.id ? 'border-[#365891] bg-[#EEF2F8]' : 'border-[#DEE3EB] bg-white hover:bg-[#F8FAFC]'"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="selectedCardId"
|
||||||
|
type="radio"
|
||||||
|
name="bank_card"
|
||||||
|
:value="card.id"
|
||||||
|
class="accent-[#365891]"
|
||||||
|
/>
|
||||||
|
<span class="flex flex-col gap-1 flex-1">
|
||||||
|
<span class="text-[12px] font-semibold text-[#3E4C63]" dir="ltr">{{ formatCardNumber(card.card_number) }}</span>
|
||||||
|
<span class="text-[11px] font-normal text-[#576989]">{{ card.holder_name }}</span>
|
||||||
|
</span>
|
||||||
|
<button type="button" class="text-[11px] font-semibold text-[#7A1419]" @click.prevent="deleteCard(card)">حذف</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="cardModalVisible" :modal="true" :closable="true" :dismissableMask="false" :style="{ width: '420px' }" class="fast-request-dialog" :pt="{ header: '!bg-client-primaryBlue' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="w-full flex items-center justify-start">
|
||||||
|
<h2 class="text-[14px] font-[700] text-[#576989]">افزودن کارت بانکی</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="w-full flex flex-col gap-4 border-t border-[#EBEBEB] pt-4">
|
||||||
|
<UiInput
|
||||||
|
v-model="cardForm.card_number"
|
||||||
|
label="شماره کارت"
|
||||||
|
placeholder="شماره کارت ۱۶ رقمی"
|
||||||
|
maxlength="16"
|
||||||
|
customClass="!border-dominant-50 !font-normal !text-[12px]"
|
||||||
|
/>
|
||||||
|
<UiInput
|
||||||
|
v-model="cardForm.holder_name"
|
||||||
|
label="نام صاحب حساب"
|
||||||
|
placeholder="نام و نام خانوادگی صاحب کارت"
|
||||||
|
customClass="!border-dominant-50 !font-normal !text-[12px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full pt-4 flex items-center justify-start gap-2 border-t border-[#EBEBEB] mt-4">
|
||||||
|
<UiButton label="ثبت کارت" severity="success" size="small" :loading="cardForm.processing" :disabled="cardForm.processing" @click="submitCard" />
|
||||||
|
<UiButton label="انصراف" severity="secondary" size="small" :disabled="cardForm.processing" @click="cardModalVisible = false" />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</UiCard>
|
||||||
|
</template>
|
||||||
@@ -1,14 +1,47 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
import { Link, usePage } from '@inertiajs/inertia-vue3';
|
||||||
import { useGlobalStore } from '../../store/Global';
|
import { useGlobalStore } from '../../store/Global';
|
||||||
import UiInput from '../ui/UiInput.vue';
|
import UiInput from '../ui/UiInput.vue';
|
||||||
import ArrowDownSvg from '../svg/arrowDown.svg';
|
|
||||||
import GlobalSvg from '../svg/global.svg';
|
import GlobalSvg from '../svg/global.svg';
|
||||||
|
import EditProfileModal from '../dashboard/EditProfileModal.vue';
|
||||||
|
import { formatPrice } from '../../utils/numberUtils';
|
||||||
const globalStore = useGlobalStore();
|
const globalStore = useGlobalStore();
|
||||||
|
const { props } = usePage();
|
||||||
|
|
||||||
|
const walletBalance = computed(() => formatPrice(props.value.auth?.wallet_balance || 0));
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
globalStore.sidebarOpen = !globalStore.sidebarOpen;
|
globalStore.sidebarOpen = !globalStore.sidebarOpen;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const userMenuOpen = ref(false);
|
||||||
|
const userMenuRef = ref(null);
|
||||||
|
const editProfileVisible = ref(false);
|
||||||
|
|
||||||
|
const toggleUserMenu = () => {
|
||||||
|
userMenuOpen.value = !userMenuOpen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditProfile = () => {
|
||||||
|
userMenuOpen.value = false;
|
||||||
|
editProfileVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeOnClickOutside = (event) => {
|
||||||
|
if (userMenuRef.value && !userMenuRef.value.contains(event.target)) {
|
||||||
|
userMenuOpen.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('click', closeOnClickOutside));
|
||||||
|
onBeforeUnmount(() => document.removeEventListener('click', closeOnClickOutside));
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ label: 'کیف پول', icon: 'fi-rr-wallet', href: '/dashboard/wallet' },
|
||||||
|
{ label: 'فاکتورها', icon: 'fi-rr-receipt', href: '/dashboard/invoices' },
|
||||||
|
{ label: 'برنامه شیفتها', icon: 'fi-rr-calendar-clock', href: '/dashboard/shifts' },
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -39,17 +72,63 @@ const toggleSidebar = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4">
|
<div class="flex items-center gap-4">
|
||||||
|
<Link href="/dashboard/wallet" class="flex items-center gap-2.5 rounded-lg bg-dominant-light border border-dominant-50 ps-1.5 pe-3 py-1.5 transition-colors hover:border-dominant-100">
|
||||||
|
<span class="flex items-center justify-center w-8 h-8 rounded-lg bg-white border border-dominant-50 text-dominant-400">
|
||||||
|
<i class="fi fi-rr-wallet flex text-[15px]"></i>
|
||||||
|
</span>
|
||||||
|
<span class="flex flex-col max-sm:hidden">
|
||||||
|
<span class="text-[10px] font-normal text-dominant-200">موجودی کیف پول</span>
|
||||||
|
<span class="text-[12px] font-bold text-dominant-500">{{ walletBalance }} <span class="text-[10px] font-normal text-dominant-300">تومان</span></span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
<div class="flex items-center justify-center border-l-[1px] !border-dominant-50 pl-4">
|
<div class="flex items-center justify-center border-l-[1px] !border-dominant-50 pl-4">
|
||||||
<GlobalSvg />
|
<GlobalSvg />
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2a justify-start items-center gap-2">
|
<div ref="userMenuRef" class="relative">
|
||||||
<span class="text-dominant-300 text-[12px] font-normal">
|
<button type="button" class="flex items-center gap-2 cursor-pointer" @click="toggleUserMenu">
|
||||||
|
<span class="flex items-center justify-center w-9 h-9 rounded-full bg-dominant-light border border-dominant-50 overflow-hidden">
|
||||||
|
<img v-if="globalStore.current_user.profile_url" :src="globalStore.current_user.profile_url" alt="تصویر پروفایل" class="w-full h-full object-cover" />
|
||||||
|
<i v-else class="fi fi-rr-user flex text-sm text-dominant-300"></i>
|
||||||
|
</span>
|
||||||
|
<span class="text-dominant-300 text-[12px] font-normal max-sm:hidden">
|
||||||
{{ globalStore.current_user.fullname }}
|
{{ globalStore.current_user.fullname }}
|
||||||
</span>
|
</span>
|
||||||
<ArrowDownSvg class="!w-4 !h-4 flex -mt-0.5"/>
|
<i class="fi fi-rr-angle-small-down flex text-sm text-dominant-300 transition-transform duration-200" :class="{ 'rotate-180': userMenuOpen }"></i>
|
||||||
|
</button>
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition duration-150 ease-out"
|
||||||
|
enter-from-class="opacity-0 -translate-y-1"
|
||||||
|
enter-to-class="opacity-100 translate-y-0"
|
||||||
|
leave-active-class="transition duration-100 ease-in"
|
||||||
|
leave-from-class="opacity-100 translate-y-0"
|
||||||
|
leave-to-class="opacity-0 -translate-y-1"
|
||||||
|
>
|
||||||
|
<nav v-if="userMenuOpen" class="absolute left-0 top-full mt-2 w-52 rounded-xl bg-white border border-complement-100 shadow-all py-2 z-30">
|
||||||
|
<ul class="flex flex-col">
|
||||||
|
<li>
|
||||||
|
<button type="button" class="w-full flex items-center gap-2.5 px-4 py-2.5 text-[12px] font-normal text-complement-900 cursor-pointer transition-colors hover:bg-complement-50" @click="openEditProfile">
|
||||||
|
<i class="fi fi-rr-user-pen flex text-[14px] text-complement-700"></i>
|
||||||
|
ویرایش حساب کاربری
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li v-for="item in menuItems" :key="item.href">
|
||||||
|
<Link :href="item.href" class="flex items-center gap-2.5 px-4 py-2.5 text-[12px] font-normal text-complement-900 transition-colors hover:bg-complement-50" @click="userMenuOpen = false">
|
||||||
|
<i class="fi flex text-[14px] text-complement-700" :class="item.icon"></i>
|
||||||
|
{{ item.label }}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li class="border-t border-complement-100 mt-1 pt-1">
|
||||||
|
<Link href="/dashboard/logout" class="flex items-center gap-2.5 px-4 py-2.5 text-[12px] font-normal text-dominant-400 transition-colors hover:bg-dominant-light">
|
||||||
|
<i class="fi fi-rr-sign-out-alt flex text-[14px]"></i>
|
||||||
|
خروج از حساب
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<EditProfileModal v-model:visible="editProfileVisible" />
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user