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 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;
|
||||
|
||||
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\Models\Document;
|
||||
use App\Services\Document\CaseService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
@@ -28,4 +31,25 @@ class CaseController extends Controller
|
||||
'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', [
|
||||
'cases' => CaseResource::collection($cases),
|
||||
'stats' => $this->caseService->statsForUser(Auth::user()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,6 @@ class DocumentController extends Controller
|
||||
|
||||
public function show(Document $document)
|
||||
{
|
||||
return Inertia::client('Dashboard/Documents/show', [
|
||||
'document' => $document,
|
||||
]);
|
||||
return redirect()->route('dashboard.cases.show', $document);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,53 @@
|
||||
namespace App\Http\Controllers\Client\Doctor;
|
||||
|
||||
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;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
protected CaseService $caseService;
|
||||
|
||||
public function __construct(CaseService $caseService)
|
||||
{
|
||||
$this->caseService = $caseService;
|
||||
}
|
||||
|
||||
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([
|
||||
'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;
|
||||
|
||||
use App\Enums\DocumentModalityEnum;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
@@ -17,10 +16,7 @@ class FastRequestResource extends JsonResource
|
||||
'status' => $this->status,
|
||||
'priority' => $this->priority,
|
||||
'pet' => new PetArchiveResource($this->whenLoaded('pet')),
|
||||
'invoice' => [
|
||||
'number' => str_pad((string) $this->id, 5, '0', STR_PAD_LEFT),
|
||||
'amount' => DocumentModalityEnum::from($this->modality)->price(),
|
||||
],
|
||||
'invoice' => new InvoiceResource($this->whenLoaded('invoice')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
return $this->hasOne(Invoice::class);
|
||||
}
|
||||
|
||||
public function assignments()
|
||||
{
|
||||
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',
|
||||
'status',
|
||||
'body',
|
||||
'technique',
|
||||
'finding',
|
||||
'diagnose',
|
||||
'recommendation',
|
||||
'submitted_at',
|
||||
];
|
||||
|
||||
@@ -43,7 +47,7 @@ class Report extends Model
|
||||
'media_assignments',
|
||||
'mediable_id',
|
||||
'media_id'
|
||||
)->withPivot('collection')->withTimestamps();
|
||||
)->withPivot('collection');
|
||||
}
|
||||
|
||||
public function medium()
|
||||
|
||||
@@ -22,6 +22,7 @@ class User extends Authenticatable
|
||||
protected $fillable = [
|
||||
'username',
|
||||
'fullname',
|
||||
'birthdate',
|
||||
'national_code',
|
||||
'level',
|
||||
'email',
|
||||
@@ -56,6 +57,7 @@ class User extends Authenticatable
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'birthdate' => 'date:Y-m-d',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
@@ -76,4 +78,24 @@ class User extends Authenticatable
|
||||
{
|
||||
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 [
|
||||
'User' => $user,
|
||||
'permissions' => $permissions,
|
||||
'wallet_balance' => (int) ($user?->wallet?->balance ?? 0),
|
||||
];
|
||||
},
|
||||
]);
|
||||
@@ -52,6 +53,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
return [
|
||||
'success' => Session::get('success'),
|
||||
'error' => Session::get('error'),
|
||||
'payment_result' => Session::get('payment_result'),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,25 +3,167 @@
|
||||
namespace App\Services\Document;
|
||||
|
||||
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\Report;
|
||||
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\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
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
|
||||
{
|
||||
$statuses = $this->isDoctor($user)
|
||||
? [DocumentStatusEnum::InProgress->value]
|
||||
: [DocumentStatusEnum::Registered->value, DocumentStatusEnum::InProgress->value];
|
||||
|
||||
return Document::with(['pet', 'requester'])
|
||||
->whereIn('status', [
|
||||
DocumentStatusEnum::Registered->value,
|
||||
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);
|
||||
});
|
||||
})
|
||||
->whereIn('status', $statuses)
|
||||
->where(fn ($query) => $this->scopeVisibleTo($query, $user))
|
||||
->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\User;
|
||||
use App\Services\FileStorageService;
|
||||
use App\Services\Invoice\InvoiceService;
|
||||
use App\Services\Shift\ShiftScheduleService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -19,10 +20,13 @@ class FastRequestService
|
||||
|
||||
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->shiftScheduleService = $shiftScheduleService;
|
||||
$this->invoiceService = $invoiceService;
|
||||
}
|
||||
|
||||
public function create(User $requester, array $data, array $historyFiles = [], array $scanFiles = []): Document
|
||||
@@ -39,6 +43,8 @@ class FastRequestService
|
||||
'requester_id' => $requester->id,
|
||||
]);
|
||||
|
||||
$this->invoiceService->createForDocument($document);
|
||||
|
||||
$this->attachFiles($document, $historyFiles, MediaCollectionEnum::History);
|
||||
$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();
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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' => [
|
||||
'api_url' => env('IRANPAYAMAK_SMS_API_URL', 'https://api.iranpayamak.com/ws/v1/sms/pattern'),
|
||||
'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' => 'دسترسی به شیفت های خود',
|
||||
],
|
||||
],
|
||||
'withdrawals' => [
|
||||
'label' => 'دسترسی به درخواستهای برداشت',
|
||||
'children' => [
|
||||
'index' => 'دسترسی به لیست درخواستهای برداشت',
|
||||
],
|
||||
],
|
||||
];
|
||||
foreach ($permissions as $key => $data) {
|
||||
$parent = Permission::create([
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
"@fullcalendar/core": "^6.1.21",
|
||||
"@fullcalendar/interaction": "^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-vue3": "^0.6.0",
|
||||
"@inertiajs/progress": "^0.2.7",
|
||||
|
||||
@@ -1,3 +1,51 @@
|
||||
.p-formfield .p-select{
|
||||
@apply !py-0.5;
|
||||
.p-inputtext,
|
||||
.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 {
|
||||
@apply transition-all duration-200;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
/* 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 */
|
||||
.fi.fi-af:before { content: "🇦🇫"; }
|
||||
.fi.fi-al:before { content: "🇦🇱"; }
|
||||
|
||||
@@ -99,3 +99,8 @@
|
||||
.shift-calendar .shift-event--inactive {
|
||||
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;
|
||||
}
|
||||
|
||||
.vpd-input-group{
|
||||
@apply !h-9;
|
||||
}
|
||||
.ql-container {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
|
||||
@@ -118,17 +118,17 @@ const removeHistoryFile = (index) => {
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="submitted_at" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="processing_deadline" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="note" initialValue="" class="flex flex-col col-span-2 gap-1">
|
||||
<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>
|
||||
<div class="grid grid-cols-2 col-span-2 gap-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="grid grid-cols-1 justify-between items-center">
|
||||
<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 class="grid grid-cols-1 justify-start">
|
||||
<Button :loading="documentStore.loaderCreateForm" @click="documentStore.update_and_store" type="button" severity="secondary" :label="documentStore.buttonLabel" />
|
||||
@@ -166,29 +166,3 @@ const removeHistoryFile = (index) => {
|
||||
</section>
|
||||
</master>
|
||||
</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">
|
||||
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
|
||||
<label>نام سطح</label>
|
||||
<InputText type="text" :class="[
|
||||
'placeholder:!text-sm',
|
||||
permissionStore.errorsCreateForm?.name && !permissionStore.createForm.name ? 'p-invalid' : ''
|
||||
]" v-model="permissionStore.createForm.name" placeholder="نام سطح را وارد کنید" />
|
||||
<InputText type="text" :class="permissionStore.errorsCreateForm?.name && !permissionStore.createForm.name ? 'p-invalid' : ''" v-model="permissionStore.createForm.name" placeholder="نام سطح را وارد کنید" />
|
||||
<Message v-if="permissionStore.errorsCreateForm?.name"
|
||||
severity="error" size="small" variant="simple">{{ permissionStore.errorsCreateForm?.name[0] }}</Message>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="label" initialValue="" class="flex flex-col gap-1">
|
||||
<label>نام فارسی سطح</label>
|
||||
<InputText type="text" :class="[
|
||||
'placeholder:!text-sm',
|
||||
permissionStore.errorsCreateForm?.label && !permissionStore.createForm.label ? 'p-invalid' : ''
|
||||
]" v-model="permissionStore.createForm.label" placeholder="نام فارسی سطح را وارد کنید" />
|
||||
<InputText type="text" :class="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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="parent_id" initialValue="" class="flex flex-col gap-1">
|
||||
<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
|
||||
placeholder="انتخاب سطح" class="w-full">
|
||||
<template #empty>
|
||||
|
||||
@@ -54,41 +54,38 @@ onUnmounted(() => {
|
||||
<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">
|
||||
<label>نام کیس</label>
|
||||
<InputText type="text" :class="[
|
||||
'placeholder:!text-sm',
|
||||
petStore.errorsCreateForm?.name && !petStore.createForm.name ? 'p-invalid' : ''
|
||||
]" v-model="petStore.createForm.name" placeholder="نام کیس خود را وارد کنید" />
|
||||
<InputText type="text" :class="petStore.errorsCreateForm?.name && !petStore.createForm.name ? 'p-invalid' : ''" v-model="petStore.createForm.name" placeholder="نام کیس خود را وارد کنید" />
|
||||
<Message v-if="petStore.errorsCreateForm?.name"
|
||||
severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.name[0] }}</Message>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="email" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="breed" initialValue="" class="flex flex-col gap-1">
|
||||
<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 v-slot="$field" name="weight" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="sterilized" initialValue="" class="flex flex-col gap-1">
|
||||
<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"
|
||||
severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.sterilized[0] }}</Message>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="gender" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="birthday" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
</Form>
|
||||
@@ -105,7 +102,7 @@ onUnmounted(() => {
|
||||
<div class="p-4 grid grid-cols-1 gap-4 rounded-2xl">
|
||||
<div class="grid grid-cols-1 justify-between items-center">
|
||||
<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 class="grid grid-cols-1 justify-start">
|
||||
<Button :loading="petStore.loaderCreateForm" @click="petStore.update_and_store" type="button" severity="secondary" :label="petStore.buttonLabel" />
|
||||
@@ -116,29 +113,3 @@ onUnmounted(() => {
|
||||
</section>
|
||||
</master>
|
||||
</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">
|
||||
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
|
||||
<label>نام مقام</label>
|
||||
<InputText type="text" :class="[
|
||||
'placeholder:!text-sm',
|
||||
roleStore.errorsCreateForm?.name && !roleStore.createForm.name ? 'p-invalid' : ''
|
||||
]" v-model="roleStore.createForm.name" placeholder="نام مقام را وارد کنید" />
|
||||
<InputText type="text" :class="roleStore.errorsCreateForm?.name && !roleStore.createForm.name ? 'p-invalid' : ''" v-model="roleStore.createForm.name" placeholder="نام مقام را وارد کنید" />
|
||||
<Message v-if="roleStore.errorsCreateForm?.name"
|
||||
severity="error" size="small" variant="simple">{{ roleStore.errorsCreateForm?.name[0] }}</Message>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="label" initialValue="" class="flex flex-col gap-1">
|
||||
<label>نام فارسی مقام</label>
|
||||
<InputText type="text" :class="[
|
||||
'placeholder:!text-sm',
|
||||
roleStore.errorsCreateForm?.label && !roleStore.createForm.label ? 'p-invalid' : ''
|
||||
]" v-model="roleStore.createForm.label" placeholder="نام فارسی مقام را وارد کنید" />
|
||||
<InputText type="text" :class="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>
|
||||
</FormField>
|
||||
<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="grid grid-cols-1 justify-between items-center">
|
||||
<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 class="grid grid-cols-2 gap-x-3 mt-1 justify-between items-center">
|
||||
<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">
|
||||
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
|
||||
<label>نام کاربری</label>
|
||||
<InputText type="text" :class="[
|
||||
'placeholder:!text-sm !text-sm',
|
||||
errors?.name && !errors.name ? 'p-invalid' : ''
|
||||
]" v-model="user_form.name" placeholder="نام کاربری را وارد کنید" />
|
||||
<InputText type="text" :class="errors?.name && !errors.name ? 'p-invalid' : ''" v-model="user_form.name" placeholder="نام کاربری را وارد کنید" />
|
||||
<Message v-if="errors?.name"
|
||||
severity="error" size="small" variant="simple">{{ errors?.name[0] }}</Message>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="first_name" initialValue="" class="flex flex-col gap-1">
|
||||
<label>نام کوچک</label>
|
||||
<InputText type="text" :class="[
|
||||
'placeholder:!text-sm !text-sm',
|
||||
errors?.first_name && !errors.first_name ? 'p-invalid' : ''
|
||||
]" v-model="user_form.first_name" placeholder="نام کوچک خود را وارد کنید" />
|
||||
<InputText type="text" :class="errors?.first_name && !errors.first_name ? 'p-invalid' : ''" v-model="user_form.first_name" placeholder="نام کوچک خود را وارد کنید" />
|
||||
<Message v-if="errors?.first_name"
|
||||
severity="error" size="small" variant="simple">{{ errors?.first_name[0] }}</Message>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="last_name" initialValue="" class="flex flex-col gap-1">
|
||||
<label>نام خانوادگی</label>
|
||||
<InputText type="text" :class="[
|
||||
'placeholder:!text-sm !text-sm',
|
||||
errors?.last_name && !errors.last_name ? 'p-invalid' : ''
|
||||
]" v-model="user_form.last_name" placeholder="نام کوچک خود را وارد کنید" />
|
||||
<InputText type="text" :class="errors?.last_name && !errors.last_name ? 'p-invalid' : ''" v-model="user_form.last_name" placeholder="نام کوچک خود را وارد کنید" />
|
||||
<Message v-if="errors?.last_name"
|
||||
severity="error" size="small" variant="simple">{{ errors?.last_name[0] }}</Message>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" asChild name="email" :initialValue="globalStore.current_user.email">
|
||||
<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>
|
||||
</FormField>
|
||||
<Button type="submit" :loading="loading" severity="secondary" label="ذخیره اطلاعات" />
|
||||
|
||||
@@ -60,7 +60,7 @@ const ChangePasswordHandler = () => {
|
||||
<Password id="password" type="password" :feedback="false"
|
||||
appendTo="self" toggleMask
|
||||
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>
|
||||
</FloatLabel>
|
||||
<Message
|
||||
@@ -74,7 +74,7 @@ const ChangePasswordHandler = () => {
|
||||
<Password id="password" type="password" :feedback="false"
|
||||
appendTo="self" toggleMask
|
||||
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>
|
||||
</FloatLabel>
|
||||
<Message
|
||||
@@ -88,7 +88,7 @@ const ChangePasswordHandler = () => {
|
||||
<Password id="password" type="password" :feedback="false"
|
||||
appendTo="self" toggleMask
|
||||
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>
|
||||
</FloatLabel>
|
||||
<Message
|
||||
|
||||
@@ -4,6 +4,8 @@ import {onMounted , onUnmounted} from 'vue';
|
||||
import {useShiftStore} from "../../store/Shift.js";
|
||||
import {Link} from "@inertiajs/inertia-vue3";
|
||||
import Card from "../../components/ui/Card.vue";
|
||||
import ShiftCalendar from "../../components/shifts/ShiftCalendar.vue";
|
||||
import { weekdayName } from "../../../Shared/shiftCalendarEvents";
|
||||
|
||||
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(() => {
|
||||
shiftStore.fetchDoctorOptions();
|
||||
shiftStore.fetchCalendarData();
|
||||
if (props.is_edit) {
|
||||
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">
|
||||
<FormField v-slot="$field" name="user_id" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="schedule_type" initialValue="" class="flex flex-col gap-1">
|
||||
<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 v-if="shiftStore.createForm.schedule_type === 'weekly'" v-slot="$field" name="day_of_week" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
<FormField v-else v-slot="$field" name="date" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="start_time" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="end_time" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="is_active" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
</Form>
|
||||
@@ -97,5 +113,18 @@ onUnmounted(() => {
|
||||
</Card>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -58,41 +58,32 @@ onUnmounted(() => {
|
||||
<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">
|
||||
<label>نام کاربری</label>
|
||||
<InputText type="text" :class="[
|
||||
'placeholder:!text-sm',
|
||||
userStore.errorsCreateForm?.username && !userStore.createForm.username ? 'p-invalid' : ''
|
||||
]" v-model="userStore.createForm.username" placeholder="نام کاربری خود را وارد کنید" />
|
||||
<InputText type="text" :class="userStore.errorsCreateForm?.username && !userStore.createForm.username ? 'p-invalid' : ''" v-model="userStore.createForm.username" placeholder="نام کاربری خود را وارد کنید" />
|
||||
<Message v-if="userStore.errorsCreateForm?.username"
|
||||
severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.username[0] }}</Message>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="email" initialValue="" class="flex flex-col gap-1">
|
||||
<label>ایمیل کاربری</label>
|
||||
<InputText type="text" :class="[
|
||||
'placeholder:!text-sm',
|
||||
userStore.errorsCreateForm?.email && !userStore.createForm.email ? 'p-invalid' : ''
|
||||
]" v-model="userStore.createForm.email" placeholder="ایمیل خود را وارد کنید" />
|
||||
<InputText type="text" :class="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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="national_code" initialValue="" class="flex flex-col gap-1">
|
||||
<label>کد ملی</label>
|
||||
<InputText type="text" :class="[
|
||||
'placeholder:!text-sm',
|
||||
userStore.errorsCreateForm?.national_code && !userStore.createForm.national_code ? 'p-invalid' : ''
|
||||
]" v-model="userStore.createForm.national_code" placeholder="کد ملی خود را وارد کنید" />
|
||||
<InputText type="text" :class="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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="phone" initialValue="" class="flex flex-col gap-1">
|
||||
<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 v-slot="$field" name="fullname" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="password" initialValue="" class="flex flex-col gap-1">
|
||||
<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 />
|
||||
<Message v-if="userStore.errorsCreateForm?.password"
|
||||
severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.password[0] }}</Message>
|
||||
@@ -100,7 +91,7 @@ onUnmounted(() => {
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="password_confirmation" initialValue="" class="flex flex-col gap-1">
|
||||
<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>
|
||||
</FormField>
|
||||
</Form>
|
||||
@@ -117,7 +108,7 @@ onUnmounted(() => {
|
||||
<div class="mt-4 p-4 grid grid-cols-1 gap-4 rounded-2xl">
|
||||
<div class="grid grid-cols-1 justify-between items-center">
|
||||
<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 class="grid grid-cols-2a justify-between items-center">
|
||||
<span class="flex">سطح:</span>
|
||||
@@ -125,9 +116,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div class="grid grid-cols-2a justify-between items-center">
|
||||
<span class="flex">مقام:</span>
|
||||
<MultiSelect class="!text-xs"
|
||||
v-model="userStore.createForm.role"
|
||||
:pt="{ option : '!text-sm' }"
|
||||
<MultiSelect v-model="userStore.createForm.role"
|
||||
placeholder="انتخاب مقام"
|
||||
optionLabel="label"
|
||||
optionValue="id"
|
||||
@@ -142,29 +131,3 @@ onUnmounted(() => {
|
||||
</section>
|
||||
</master>
|
||||
</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",
|
||||
expanded: false,
|
||||
},
|
||||
{
|
||||
label: "درخواستهای برداشت",
|
||||
icon: "fi fi-rr-wallet",
|
||||
to: "/admin/withdraw-requests",
|
||||
key: 'withdrawals',
|
||||
permission: "withdrawals.index",
|
||||
expanded: false,
|
||||
},
|
||||
{
|
||||
label: "مدیریت شیفت ها",
|
||||
icon: "fi fi-rr-calendar",
|
||||
|
||||
@@ -251,7 +251,6 @@ const getColumnStyle = (column) => {
|
||||
<div class="grid grid-cols-1 items-center justify-start">
|
||||
<InputText
|
||||
size="small"
|
||||
class="placeholder:!font-light placeholder:!text-xs !h-10"
|
||||
placeholder="جستجو..."
|
||||
type="text"
|
||||
v-model="localSearchValue"
|
||||
@@ -274,8 +273,7 @@ const getColumnStyle = (column) => {
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
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] !text-[12px] !h-10"
|
||||
class="min-w-[130px] max-sm:!w-full max-w-[200px]"
|
||||
:loading="renderFilter(column).loading"
|
||||
:disabled="renderFilter(column).loading"
|
||||
:filter="renderFilter(column).search"
|
||||
@@ -297,8 +295,7 @@ const getColumnStyle = (column) => {
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
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] !text-[12px] !h-10"
|
||||
class="min-w-[150px] max-sm:!w-full max-w-[200px]"
|
||||
:loading="renderFilter(column).loading"
|
||||
:disabled="renderFilter(column).loading"
|
||||
:filter="renderFilter(column).search"
|
||||
|
||||
@@ -11,10 +11,14 @@ const props = defineProps({
|
||||
shifts: {
|
||||
type: Array,
|
||||
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'];
|
||||
|
||||
@@ -71,6 +75,11 @@ const calendarOptions = computed(() => ({
|
||||
eventClick: (info) => {
|
||||
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();
|
||||
|
||||
@@ -45,15 +45,11 @@ const props = defineProps({
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
default: '!text-xs !h-full'
|
||||
default: 'w-full'
|
||||
},
|
||||
pt: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
option: '!text-sm',
|
||||
placeholder: '!text-sm',
|
||||
label: '!pt-3'
|
||||
})
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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 UiCard from '../../components/ui/UiCard.vue';
|
||||
import RequestCard from '../../components/dashboard/RequestCard.vue';
|
||||
import { ref } from 'vue';
|
||||
import { Link } from '@inertiajs/inertia-vue3';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { Link, usePage } from '@inertiajs/inertia-vue3';
|
||||
import FastRequestModal from '../../components/dashboard/FastRequestModal.vue';
|
||||
import PaymentResultModal from '../../components/dashboard/PaymentResultModal.vue';
|
||||
import { useFastRequestStore } from '../../store/FastRequest';
|
||||
import { modalities } from '../../data/fastRequestOptions';
|
||||
import { caseColumns } from '../../data/casesTableColumns';
|
||||
@@ -22,6 +23,17 @@ const openFastRequestModal = (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({
|
||||
user: {
|
||||
type: Object,
|
||||
@@ -30,13 +42,17 @@ defineProps({
|
||||
cases: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
stats: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientLayout>
|
||||
<ActivitiesWidget />
|
||||
<ActivitiesWidget :stats="stats" />
|
||||
<UiCard title="ثبت آنی درخواست جدید" variant="default" class="mt-4">
|
||||
<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]">
|
||||
@@ -77,5 +93,6 @@ defineProps({
|
||||
<ActivitiesChart />
|
||||
</UiCard>
|
||||
<fast-request-modal v-model:visible="fastRequestModalVisible"></fast-request-modal>
|
||||
<payment-result-modal v-model:visible="paymentResultVisible" :result="paymentResult"></payment-result-modal>
|
||||
</ClientLayout>
|
||||
</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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<ClientLayout>
|
||||
<h1 class="text-2xl font-bold">
|
||||
Reports
|
||||
</h1>
|
||||
<UiCard title="گزارشات" variant="default">
|
||||
<TableGeneric
|
||||
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>
|
||||
</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>
|
||||
import { Link } from '@inertiajs/inertia-vue3'
|
||||
import ActivityCard from './ActivityCard.vue'
|
||||
import { formatPrice } from '../../utils/numberUtils'
|
||||
|
||||
const props = defineProps({
|
||||
stats: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="grid grid-cols-4 max-lg:grid-cols-2 max-sm:grid-cols-1 gap-4">
|
||||
<ActivityCard title="کیس در انتظار بررسی"
|
||||
:count="5" link="/dashboard/doctor/cases" label-link="مشاهده و بررسی" />
|
||||
:count="stats.pending_cases" link="/dashboard/cases" label-link="مشاهده و بررسی" />
|
||||
<ActivityCard title="کیس بررسی شده در ماه"
|
||||
:count="999" link="/dashboard/doctor/cases" label-link="مشاهده گزارشات کیسها" />
|
||||
<ActivityCard title="تومان درآمد"
|
||||
:count="formatPrice(50000000)" link="/dashboard/doctor/cases" label-link="مشاهده گزارشات مالی " />
|
||||
<ActivityCard title="ساعت تا شیت بعدی"
|
||||
:count="4" link="/dashboard/doctor/cases" label-link="مشاهده شیفتها" />
|
||||
:count="stats.monthly_reviewed" link="/dashboard/reports" label-link="مشاهده گزارشات کیسها" />
|
||||
<ActivityCard :title="stats.is_doctor ? 'تومان درآمد' : 'تومان هزینه در ماه'"
|
||||
:count="formatPrice(stats.monthly_amount)" :link="stats.is_doctor ? '/dashboard/wallet' : '/dashboard/invoices'" label-link="مشاهده گزارشات مالی" />
|
||||
<ActivityCard v-if="stats.next_shift_hours !== null" title="ساعت تا شیفت بعدی"
|
||||
:count="stats.next_shift_hours" link="/dashboard/shifts" label-link="مشاهده شیفتها" />
|
||||
</section>
|
||||
</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>
|
||||
import UiButton from '../../components/ui/UiButton.vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import ProgressBar from 'primevue/progressbar';
|
||||
import { toast } from 'vue3-toastify';
|
||||
@@ -9,11 +9,15 @@ import RequestCase from './fastRequest/RequestCase.vue';
|
||||
import RequestDetails from './fastRequest/RequestDetails.vue';
|
||||
import RequestDoctor from './fastRequest/RequestDoctor.vue';
|
||||
import RequestReview from './fastRequest/RequestReview.vue';
|
||||
import InvoiceModal from './fastRequest/InvoiceModal.vue';
|
||||
import { useFastRequestStore } from '../../store/FastRequest';
|
||||
|
||||
const visible = defineModel('visible');
|
||||
const fastRequestStore = useFastRequestStore();
|
||||
|
||||
const invoiceVisible = ref(false);
|
||||
const invoiceDocument = ref(null);
|
||||
|
||||
const steps = [
|
||||
{ label: 'نوع درخواست', component: RequestType },
|
||||
{ label: 'انتخاب کیس', component: RequestCase },
|
||||
@@ -49,8 +53,9 @@ const handleNext = async () => {
|
||||
|
||||
try {
|
||||
const document = await fastRequestStore.submit();
|
||||
toast.success(`درخواست شما با شماره پرونده ${document.code} با موفقیت ثبت شد.`);
|
||||
invoiceDocument.value = document;
|
||||
visible.value = false;
|
||||
invoiceVisible.value = true;
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.message || 'خطا در ثبت درخواست. لطفا دوباره تلاش کنید.');
|
||||
}
|
||||
@@ -93,4 +98,5 @@ const handleNext = async () => {
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
<invoice-modal v-model:visible="invoiceVisible" :document="invoiceDocument"></invoice-modal>
|
||||
</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>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import { usePage } from '@inertiajs/inertia-vue3';
|
||||
import { Inertia } from '@inertiajs/inertia';
|
||||
import UiButton from '../../ui/UiButton.vue';
|
||||
import { toast } from 'vue3-toastify';
|
||||
import { modalityTitle } from '../../../data/fastRequestOptions';
|
||||
import { formatPrice } from '../../../utils/numberUtils';
|
||||
|
||||
const visible = defineModel('visible');
|
||||
|
||||
@@ -10,31 +14,69 @@ const props = defineProps({
|
||||
document: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
invoice: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const formattedAmount = computed(() => {
|
||||
return Number(props.document?.invoice?.amount || 0).toLocaleString('fa-IR');
|
||||
});
|
||||
const { props: pageProps } = usePage();
|
||||
|
||||
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;
|
||||
Inertia.reload();
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.message || 'خطا در پرداخت از کیف پول. لطفا دوباره تلاش کنید.');
|
||||
} finally {
|
||||
payingWithWallet.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<div v-if="document" 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">
|
||||
<i class="fi-rr-info text-[20px] text-[#576989]"></i>
|
||||
<p class="text-[13px] font-[400] text-[#3E4C63]">
|
||||
<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-3">
|
||||
<i class="fi-rr-info text-[18px] text-[#576989]"></i>
|
||||
<p class="text-[12px] font-[400] text-[#3E4C63] leading-6">
|
||||
یک فاکتور به مبلغ « <span class="font-[700]">{{ formattedAmount }} تومان</span> » برای شما ایجاد شد. لطفا جهت ادامه مراحل درخواست نسبت به پرداخت اقدام نمایید.
|
||||
</p>
|
||||
</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>
|
||||
به شناسه
|
||||
@@ -48,9 +90,14 @@ const handlePay = () => {
|
||||
<br>
|
||||
بدیهیست که در صورت عدم تایید کارشناسان مبلغ مربوطه به کیف پول شما بازگردانده خواهد شد.
|
||||
</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 class="w-full pt-3 flex items-center justify-start border-t border-[#EBEBEB]">
|
||||
<ui-button label="تایید و پرداخت" severity="success" size="small" @click="handlePay" />
|
||||
<div class="w-full pt-3 flex items-center justify-start gap-2 border-t border-[#EBEBEB]">
|
||||
<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>
|
||||
</Dialog>
|
||||
</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>
|
||||
import { ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { Link, usePage } from '@inertiajs/inertia-vue3';
|
||||
import { useGlobalStore } from '../../store/Global';
|
||||
import UiInput from '../ui/UiInput.vue';
|
||||
import ArrowDownSvg from '../svg/arrowDown.svg';
|
||||
import GlobalSvg from '../svg/global.svg';
|
||||
import EditProfileModal from '../dashboard/EditProfileModal.vue';
|
||||
import { formatPrice } from '../../utils/numberUtils';
|
||||
const globalStore = useGlobalStore();
|
||||
const { props } = usePage();
|
||||
|
||||
const walletBalance = computed(() => formatPrice(props.value.auth?.wallet_balance || 0));
|
||||
|
||||
const toggleSidebar = () => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -39,17 +72,63 @@ const toggleSidebar = () => {
|
||||
/>
|
||||
</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">
|
||||
<GlobalSvg />
|
||||
</div>
|
||||
<div class="grid grid-cols-2a justify-start items-center gap-2">
|
||||
<span class="text-dominant-300 text-[12px] font-normal">
|
||||
<div ref="userMenuRef" class="relative">
|
||||
<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 }}
|
||||
</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>
|
||||
<EditProfileModal v-model:visible="editProfileVisible" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user