feat: Implement Fast Request feature with file upload and request management

This commit is contained in:
2026-08-26 17:40:45 +03:30
parent ba734679cf
commit 2d96978c38
33 changed files with 1294 additions and 852 deletions
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Enums;
enum DocumentModalityEnum: string
{
use EnumHasOptions;
case CtScan = 'ct-scan';
case Mri = 'mri';
case Sonography = 'sonography';
case Echo = 'echo';
case Radiology = 'radiology';
}
@@ -3,13 +3,29 @@
namespace App\Http\Controllers\Client\Doctor;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Http\Resources\Client\CaseResource;
use App\Services\Document\CaseService;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
class CaseController extends Controller
{
protected CaseService $caseService;
public function __construct(CaseService $caseService)
{
$this->caseService = $caseService;
}
public function index()
{
return Inertia::client('Dashboard/Cases/Index');
$cases = $this->caseService->pendingForUser(Auth::user())
->paginate(10)
->withQueryString()
->onEachSide(1);
return Inertia::client('Dashboard/Cases/Index', [
'cases' => CaseResource::collection($cases),
]);
}
}
@@ -3,13 +3,26 @@
namespace App\Http\Controllers\Client\Doctor;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Http\Resources\Client\CaseResource;
use App\Services\Document\CaseService;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
class DashboardController extends Controller
{
protected CaseService $caseService;
public function __construct(CaseService $caseService)
{
$this->caseService = $caseService;
}
public function index()
{
return Inertia::client('Dashboard/Dashboard');
$cases = $this->caseService->pendingForUser(Auth::user())->take(5)->get();
return Inertia::client('Dashboard/Dashboard', [
'cases' => CaseResource::collection($cases),
]);
}
}
@@ -3,15 +3,31 @@
namespace App\Http\Controllers\Client\Doctor;
use App\Http\Controllers\Controller;
use App\Http\Resources\Client\CaseResource;
use App\Models\Document;
use Illuminate\Http\Request;
use App\Services\Document\CaseService;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
class DocumentController extends Controller
{
protected CaseService $caseService;
public function __construct(CaseService $caseService)
{
$this->caseService = $caseService;
}
public function index()
{
return Inertia::client('Dashboard/Documents/Index');
$documents = $this->caseService->pendingForUser(Auth::user())
->paginate(10)
->withQueryString()
->onEachSide(1);
return Inertia::client('Dashboard/Documents/Index', [
'documents' => CaseResource::collection($documents),
]);
}
public function show(Document $document)
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Client;
use App\Enums\UserEnum;
use App\Http\Controllers\Controller;
use App\Http\Requests\Client\FastRequestRequest;
use App\Http\Resources\Client\DoctorResource;
use App\Http\Resources\Client\FastRequestResource;
use App\Http\Resources\Client\PetArchiveResource;
use App\Models\User;
use App\Services\Document\FastRequestService;
use Illuminate\Support\Facades\Auth;
class FastRequestController extends Controller
{
protected FastRequestService $fastRequestService;
public function __construct(FastRequestService $fastRequestService)
{
$this->fastRequestService = $fastRequestService;
}
public function create()
{
$pets = Auth::user()->pets()->latest()->get();
$doctors = User::where('level', UserEnum::Doctor->value)
->latest()
->get();
return response()->json([
'pets' => PetArchiveResource::collection($pets),
'doctors' => DoctorResource::collection($doctors),
]);
}
public function store(FastRequestRequest $request)
{
$document = $this->fastRequestService->create(
Auth::user(),
$request->validated(),
$request->file('history_files', []),
$request->file('scan_files', [])
);
return response()->json([
'document' => new FastRequestResource($document->load('pet')),
]);
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Requests\Client;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Enums\DocumentModalityEnum;
use App\Enums\DocumentPriorityEnum;
use App\Enums\PetGenderEnum;
use App\Enums\PetTypeEnum;
class FastRequestRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'modality' => ['required', 'string', Rule::in(DocumentModalityEnum::values())],
'priority' => ['required', 'string', Rule::in(DocumentPriorityEnum::keys())],
'note' => ['nullable', 'string', 'max:1000'],
'pet_id' => ['nullable', 'exists:pets,id'],
'pet' => ['required_without:pet_id', 'array'],
'pet.name' => ['required_without:pet_id', 'nullable', 'string', 'max:255'],
'pet.type' => ['required_without:pet_id', 'nullable', 'string', Rule::in(PetTypeEnum::keys())],
'pet.gender' => ['nullable', 'string', Rule::in(PetGenderEnum::keys())],
'pet.breed' => ['nullable', 'string', 'max:255'],
'pet.age' => ['nullable', 'numeric', 'min:0', 'max:100'],
'pet.weight' => ['nullable', 'numeric', 'min:0', 'max:1000'],
'pet.sterilized' => ['nullable', 'boolean'],
'doctor_id' => ['nullable', 'exists:users,id'],
'history_files' => ['nullable', 'array'],
'history_files.*' => ['file', 'image', 'max:10240'],
'scan_files' => ['nullable', 'array'],
'scan_files.*' => ['file', 'image', 'max:10240'],
];
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class CaseResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'code' => $this->code,
'status' => $this->status,
'modality' => $this->modality,
'requester' => optional($this->requester)->fullname,
'submitted_at' => $this->submitted_at ? verta($this->submitted_at)->format('%Y/%m/%d - H:i') : null,
'processing_deadline' => $this->deadlineLabel(),
];
}
protected function deadlineLabel(): ?string
{
if (!$this->processing_deadline) {
return null;
}
if (now()->greaterThanOrEqualTo($this->processing_deadline)) {
return 'اتمام فرصت رسیدگی';
}
$minutes = (int) now()->diffInMinutes($this->processing_deadline, true);
if ($minutes < 60) {
return $minutes . ' دقیقه';
}
return 'کمتر از ' . (int) ceil($minutes / 60) . ' ساعت';
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class DoctorResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'fullname' => $this->fullname,
'specialty' => $this->specialty,
'profile_url' => $this->profile_url,
];
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class FastRequestResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'code' => $this->code,
'modality' => $this->modality,
'status' => $this->status,
'priority' => $this->priority,
'pet' => new PetArchiveResource($this->whenLoaded('pet')),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PetArchiveResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'type' => $this->type,
'breed' => $this->breed,
];
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Services\Document;
use App\Enums\DocumentStatusEnum;
use App\Models\Document;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
class CaseService
{
public function pendingForUser(User $user): Builder
{
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);
});
})
->latest('submitted_at');
}
}
@@ -0,0 +1,79 @@
<?php
namespace App\Services\Document;
use App\Enums\DocumentPriorityEnum;
use App\Enums\MediaCollectionEnum;
use App\Enums\PetGenderEnum;
use App\Enums\PetTypeEnum;
use App\Models\Document;
use App\Models\Pet;
use App\Models\User;
use App\Services\FileStorageService;
use Illuminate\Support\Facades\DB;
class FastRequestService
{
protected FileStorageService $fileStorageService;
public function __construct(FileStorageService $fileStorageService)
{
$this->fileStorageService = $fileStorageService;
}
public function create(User $requester, array $data, array $historyFiles = [], array $scanFiles = []): Document
{
return DB::transaction(function () use ($requester, $data, $historyFiles, $scanFiles) {
$pet = $this->resolvePet($requester, $data);
$document = Document::create([
'modality' => $data['modality'],
'priority' => DocumentPriorityEnum::fromName($data['priority'])->value,
'note' => $data['note'] ?? null,
'submitted_at' => now(),
'pet_id' => $pet->id,
'requester_id' => $requester->id,
]);
$this->attachFiles($document, $historyFiles, MediaCollectionEnum::History);
$this->attachFiles($document, $scanFiles, MediaCollectionEnum::Primary);
if (!empty($data['doctor_id'])) {
$document->assignments()->create([
'user_id' => $data['doctor_id'],
'role' => 'رادیولوژیست',
]);
}
return $document;
});
}
protected function resolvePet(User $requester, array $data): Pet
{
if (!empty($data['pet_id'])) {
return Pet::findOrFail($data['pet_id']);
}
$pet = $data['pet'];
return Pet::create([
'name' => $pet['name'],
'type' => PetTypeEnum::fromName($pet['type'])->value,
'gender' => isset($pet['gender']) ? PetGenderEnum::fromName($pet['gender'])->value : PetGenderEnum::Unknown->value,
'breed' => $pet['breed'] ?? null,
'weight' => $pet['weight'] ?? null,
'sterilized' => $pet['sterilized'] ?? null,
'birthday' => isset($pet['age']) ? now()->subYears((int) $pet['age'])->startOfDay() : null,
'owner_id' => $requester->id,
]);
}
protected function attachFiles(Document $document, array $files, MediaCollectionEnum $collection): void
{
foreach ($files as $file) {
$medium = $this->fileStorageService->storeFile($file);
$document->media()->attach($medium->id, ['collection' => $collection->value]);
}
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
namespace Database\Seeders;
use App\Enums\DocumentModalityEnum;
use App\Enums\DocumentPriorityEnum;
use App\Enums\DocumentStatusEnum;
use App\Enums\UserEnum;
use App\Models\Document;
use App\Models\User;
use Illuminate\Database\Seeder;
class DocumentSeeder extends Seeder
{
protected int $documentsPerOwner = 30;
public function run(): void
{
$doctors = User::where('level', UserEnum::Doctor->value)->get();
$owners = User::whereIn('level', [UserEnum::Admin->value, UserEnum::Supervisor->value, UserEnum::Vet->value])->get();
if ($doctors->isEmpty() || $owners->isEmpty()) {
$this->call(FastRequestSeeder::class);
$doctors = User::where('level', UserEnum::Doctor->value)->get();
$owners = User::whereIn('level', [UserEnum::Admin->value, UserEnum::Supervisor->value, UserEnum::Vet->value])->get();
}
$notes = [
'لنگش اندام حرکتی جلو از دو روز پیش، درخواست گرافی مفصل شانه و آرنج.',
'استفراغ مکرر و بی‌اشتهایی، احتمال جسم خارجی در دستگاه گوارش. سونوگرافی شکم انجام شود.',
'سرفه مزمن و تنگی نفس، درخواست گرافی قفسه سینه در دو نما.',
'تشنج ناگهانی بدون سابقه قبلی، بررسی جمجمه از نظر ضایعه فضاگیر.',
'سوفل قلبی درجه سه در سمع، درخواست اکوکاردیوگرافی کامل.',
'ضعف پیش‌رونده اندام حرکتی عقب، بررسی ستون فقرات کمری از نظر دیسک.',
'تروما به دنبال تصادف با خودرو، بررسی لگن و اندام حرکتی از نظر شکستگی.',
'اتساع شکم و درد در لمس، درخواست سونوگرافی اورژانسی جهت رد پیچ‌خوردگی معده.',
'توده قابل لمس در ناحیه گردن، بررسی ابعاد و منشا توده با سونوگرافی.',
'کج شدن سر و نیستاگموس، بررسی گوش میانی و داخلی با سی‌تی‌اسکن.',
'پرنوشی و پرادراری از یک هفته پیش، بررسی کلیه‌ها و مثانه.',
'خونریزی از بینی و عطسه مکرر، بررسی حفره بینی از نظر جسم خارجی.',
'عدم تحمل وزن روی اندام عقب چپ، احتمال پارگی رباط صلیبی. گرافی زانو انجام شود.',
'کاهش وزن مزمن با وجود اشتهای طبیعی، سونوگرافی کامل محوطه بطنی.',
'دیسترس تنفسی حاد، بررسی پرده جنب از نظر تجمع مایع.',
];
$rejectionReasons = [
'کیفیت تصاویر ارسالی برای تفسیر کافی نیست، لطفا مجددا تصویربرداری شود.',
'اطلاعات بالینی ناقص است، شرح حال کامل بیمار ثبت شود.',
'نمای درخواستی با شکایت بالینی همخوانی ندارد.',
];
$statuses = [
DocumentStatusEnum::Registered,
DocumentStatusEnum::InProgress,
DocumentStatusEnum::Registered,
DocumentStatusEnum::InProgress,
DocumentStatusEnum::Registered,
DocumentStatusEnum::Completed,
DocumentStatusEnum::Registered,
DocumentStatusEnum::InProgress,
DocumentStatusEnum::Rejected,
DocumentStatusEnum::Registered,
];
$deadlineMinutes = [-180, 25, 53, 240, 420, null, 700, 1100, 1400, -30, 1900, 2600];
$modalities = DocumentModalityEnum::cases();
foreach ($owners as $owner) {
$pets = $owner->pets;
if ($pets->isEmpty()) {
continue;
}
$missing = $this->documentsPerOwner - Document::where('requester_id', $owner->id)->count();
for ($i = 0; $i < $missing; $i++) {
$status = $statuses[$i % count($statuses)];
$deadline = $deadlineMinutes[$i % count($deadlineMinutes)];
$submittedAt = now()->subDays(rand(0, 20))->subMinutes(rand(0, 1440));
$document = Document::create([
'modality' => $modalities[$i % count($modalities)]->value,
'priority' => ($i % 4 === 0 ? DocumentPriorityEnum::High : DocumentPriorityEnum::Medium)->value,
'status' => $status->value,
'note' => $notes[$i % count($notes)],
'submitted_at' => $submittedAt,
'scan_date' => $status === DocumentStatusEnum::Registered ? null : $submittedAt->copy()->addHours(rand(1, 12)),
'processing_deadline' => $deadline !== null ? now()->addMinutes($deadline) : null,
'finalized_at' => $status === DocumentStatusEnum::Completed ? $submittedAt->copy()->addDays(rand(1, 3)) : null,
'finalized_by' => $status === DocumentStatusEnum::Completed ? $doctors->random()->id : null,
'rejection_reason' => $status === DocumentStatusEnum::Rejected ? $rejectionReasons[$i % count($rejectionReasons)] : null,
'pet_id' => $pets->random()->id,
'requester_id' => $owner->id,
]);
$document->assignments()->create([
'user_id' => $doctors->random()->id,
'role' => 'رادیولوژیست',
]);
}
}
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
namespace Database\Seeders;
use App\Enums\DocumentModalityEnum;
use App\Enums\DocumentPriorityEnum;
use App\Enums\DocumentStatusEnum;
use App\Enums\PetGenderEnum;
use App\Enums\PetTypeEnum;
use App\Enums\UserEnum;
use App\Models\Document;
use App\Models\Pet;
use App\Models\User;
use Illuminate\Database\Seeder;
class FastRequestSeeder extends Seeder
{
public function run(): void
{
$doctors = $this->seedDoctors();
$this->seedPets();
$this->seedDocuments($doctors);
}
protected function seedDoctors()
{
$doctors = [
['email' => 'dr.hosseini@radical.dev', 'fullname' => 'دکتر حسین حسینی', 'specialty' => 'متخصص جراحی پوست و مو'],
['email' => 'dr.amin@radical.dev', 'fullname' => 'دکتر امین حسینی', 'specialty' => 'متخصص و رادیولوژیست'],
['email' => 'dr.mahdipour@radical.dev', 'fullname' => 'دکتر مریم مهدی‌پور', 'specialty' => 'متخصص تصویربرداری'],
['email' => 'dr.golian@radical.dev', 'fullname' => 'دکتر رضا گلیان', 'specialty' => 'متخصص داخلی دام کوچک'],
['email' => 'dr.karimi@radical.dev', 'fullname' => 'دکتر سارا کریمی', 'specialty' => 'متخصص سونوگرافی و اکو'],
['email' => 'dr.nazari@radical.dev', 'fullname' => 'دکتر بهرام نظری', 'specialty' => 'متخصص ارتوپدی دام کوچک'],
];
return collect($doctors)->map(function ($doctor) {
return User::firstOrCreate(
['username' => explode('@', $doctor['email'])[0]],
User::factory()->raw([
'email' => $doctor['email'],
'level' => UserEnum::Doctor->value,
'fullname' => $doctor['fullname'],
'specialty' => $doctor['specialty'],
])
);
});
}
protected function seedPets(): void
{
$pets = [
['name' => 'مایلو', 'type' => PetTypeEnum::Dog, 'gender' => PetGenderEnum::Male, 'breed' => 'ژرمن شپرد', 'weight' => 28, 'sterilized' => true],
['name' => 'تدی', 'type' => PetTypeEnum::Cat, 'gender' => PetGenderEnum::Male, 'breed' => 'پرشین', 'weight' => 4, 'sterilized' => false],
['name' => 'پونی', 'type' => PetTypeEnum::Rabbit, 'gender' => PetGenderEnum::Female, 'breed' => 'لوپ هلندی', 'weight' => 2, 'sterilized' => false],
['name' => 'روبیک', 'type' => PetTypeEnum::Hamster, 'gender' => PetGenderEnum::Male, 'breed' => 'سوری', 'weight' => 1, 'sterilized' => false],
['name' => 'کولا', 'type' => PetTypeEnum::GuineaPig, 'gender' => PetGenderEnum::Female, 'breed' => 'آمریکایی', 'weight' => 1, 'sterilized' => true],
['name' => 'هاپو', 'type' => PetTypeEnum::Dog, 'gender' => PetGenderEnum::Male, 'breed' => 'هاسکی سیبری', 'weight' => 24, 'sterilized' => false],
['name' => 'نازی', 'type' => PetTypeEnum::Cat, 'gender' => PetGenderEnum::Female, 'breed' => 'اسکاتیش فولد', 'weight' => 3, 'sterilized' => true],
['name' => 'قناری', 'type' => PetTypeEnum::Bird, 'gender' => PetGenderEnum::Unknown, 'breed' => 'قناری رسمی', 'weight' => 1, 'sterilized' => false],
['name' => 'لاکی', 'type' => PetTypeEnum::Turtle, 'gender' => PetGenderEnum::Female, 'breed' => 'برکه‌ای', 'weight' => 1, 'sterilized' => false],
['name' => 'اسنوپی', 'type' => PetTypeEnum::Dog, 'gender' => PetGenderEnum::Female, 'breed' => 'شیتزو', 'weight' => 6, 'sterilized' => true],
];
$owners = User::whereIn('level', [UserEnum::Admin->value, UserEnum::Supervisor->value, UserEnum::Vet->value])->get();
foreach ($owners as $owner) {
foreach ($pets as $pet) {
Pet::firstOrCreate(
['name' => $pet['name'], 'owner_id' => $owner->id],
[
'type' => $pet['type']->value,
'gender' => $pet['gender']->value,
'breed' => $pet['breed'],
'weight' => $pet['weight'],
'sterilized' => $pet['sterilized'],
'birthday' => now()->subYears(rand(1, 8))->startOfDay(),
]
);
}
}
}
protected function seedDocuments($doctors): void
{
$requests = [
['pet' => 'مایلو', 'modality' => DocumentModalityEnum::Radiology, 'priority' => DocumentPriorityEnum::High, 'status' => DocumentStatusEnum::Registered, 'deadline_minutes' => -120, 'note' => 'لنگش اندام حرکتی عقب پس از پرش، احتمال آسیب مفصل ران. لطفا گرافی لگن بررسی شود.'],
['pet' => 'تدی', 'modality' => DocumentModalityEnum::Sonography, 'priority' => DocumentPriorityEnum::Medium, 'status' => DocumentStatusEnum::Registered, 'deadline_minutes' => 53, 'note' => 'بی‌اشتهایی و استفراغ متناوب از سه روز پیش، درخواست سونوگرافی شکمی.'],
['pet' => 'هاپو', 'modality' => DocumentModalityEnum::CtScan, 'priority' => DocumentPriorityEnum::High, 'status' => DocumentStatusEnum::InProgress, 'deadline_minutes' => 290, 'note' => 'تشنج ناگهانی، درخواست سی‌تی‌اسکن جمجمه جهت رد ضایعه فضاگیر.'],
['pet' => 'نازی', 'modality' => DocumentModalityEnum::Echo, 'priority' => DocumentPriorityEnum::Medium, 'status' => DocumentStatusEnum::Registered, 'deadline_minutes' => 1400, 'note' => 'سوفل قلبی در معاینه شنیده شد، درخواست اکوکاردیوگرافی.'],
['pet' => 'اسنوپی', 'modality' => DocumentModalityEnum::Mri, 'priority' => DocumentPriorityEnum::Medium, 'status' => DocumentStatusEnum::InProgress, 'deadline_minutes' => 1900, 'note' => 'ضعف پیش‌رونده اندام حرکتی، بررسی ستون فقرات از نظر دیسک بین‌مهره‌ای.'],
['pet' => 'پونی', 'modality' => DocumentModalityEnum::Radiology, 'priority' => DocumentPriorityEnum::Medium, 'status' => DocumentStatusEnum::Registered, 'deadline_minutes' => null, 'note' => 'کاهش وزن و دندان قروچه، درخواست گرافی دندان و فک.'],
['pet' => 'روبیک', 'modality' => DocumentModalityEnum::Sonography, 'priority' => DocumentPriorityEnum::High, 'status' => DocumentStatusEnum::InProgress, 'deadline_minutes' => 45, 'note' => 'تورم ناحیه شکم و بی‌حالی شدید، درخواست سونوگرافی اورژانسی.'],
['pet' => 'کولا', 'modality' => DocumentModalityEnum::CtScan, 'priority' => DocumentPriorityEnum::Medium, 'status' => DocumentStatusEnum::Registered, 'deadline_minutes' => 600, 'note' => 'کج شدن سر و عدم تعادل، بررسی گوش میانی و داخلی.'],
];
$owners = User::whereIn('level', [UserEnum::Admin->value, UserEnum::Supervisor->value, UserEnum::Vet->value])->get();
foreach ($owners as $owner) {
if (Document::where('requester_id', $owner->id)->exists()) {
continue;
}
foreach ($requests as $index => $request) {
$pet = Pet::where('name', $request['pet'])->where('owner_id', $owner->id)->first();
if (!$pet) {
continue;
}
$document = Document::create([
'modality' => $request['modality']->value,
'priority' => $request['priority']->value,
'status' => $request['status']->value,
'note' => $request['note'],
'submitted_at' => now()->subDays(count($requests) - $index),
'processing_deadline' => $request['deadline_minutes'] !== null ? now()->addMinutes($request['deadline_minutes']) : null,
'pet_id' => $pet->id,
'requester_id' => $owner->id,
]);
$document->assignments()->create([
'user_id' => $doctors[$index % $doctors->count()]->id,
'role' => 'رادیولوژیست',
]);
}
}
}
}
+112
View File
@@ -0,0 +1,112 @@
.fast-request-dialog .p-select {
border: 1px solid #DEE3EB !important;
border-radius: 8px !important;
height: 37px !important;
display: flex !important;
align-items: center !important;
}
.fast-request-dialog .p-select-label {
padding: 0 12px;
line-height: 1;
display: flex;
align-items: center;
height: 100%;
}
.fast-request-dialog .p-select-label.p-placeholder {
color: #6E91C9 !important;
font-size: 11px !important;
font-weight: 400 !important;
}
.fast-request-dialog .p-inputtext {
font-size: 11px !important;
font-weight: 400 !important;
border: 1px solid #DEE3EB !important;
border-radius: 8px !important;
}
.fast-request-dialog .p-inputtext,
.fast-request-dialog .p-select,
.fast-request-dialog .p-textarea {
box-shadow: none !important;
}
.fast-request-dialog .ui-button .p-button {
font-size: 11px !important;
font-weight: 600 !important;
white-space: nowrap !important;
border-radius: 6px !important;
padding: 8px 16px !important;
}
.fast-request-dialog .ui-button .p-button.p-button-success {
background: #576989 !important;
border-color: #576989 !important;
color: #FFFFFF !important;
}
.fast-request-dialog .p-inputtext::placeholder {
color: #6E91C9;
}
.fast-request-dialog .ui-input-wrapper {
@apply !mb-0;
}
.fast-request-dialog .ui-input-wrapper-border {
border: 1px solid #DEE3EB;
border-radius: 8px;
}
.fast-request-dialog .ui-input-wrapper-border .p-inputtext {
border: 0 !important;
}
.fast-request-dialog .p-textarea {
border: 1px solid #DEE3EB !important;
border-radius: 8px !important;
height: 100% !important;
font-size: 11px !important;
font-weight: 400 !important;
}
.fast-request-dialog .p-textarea::placeholder {
color: #6E91C9 !important;
font-size: 11px !important;
font-weight: 400 !important;
}
.fast-request-dialog .p-progressbar {
min-width: 60px !important;
height: 4px;
}
.fast-request-dialog .p-progressbar-value {
background: #576989 !important;
}
.fast-request-dialog .p-fileupload-choose-button,
.fast-request-dialog .p-fileupload-header {
display: none !important;
}
.fast-request-dialog .p-fileupload {
width: 100% !important;
height: 100% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.fast-request-dialog .p-fileupload-content {
pointer-events: auto !important;
width: 100%;
}
.fast-request-dialog .p-fileupload-advanced {
border: 2px solid #C1D0E8 !important;
border-style: dashed !important;
border-radius: 6px !important;
}
+1
View File
@@ -7,6 +7,7 @@
@import "./Client/ui/button.css";
@import "./Client/ui/card.css";
@import "./Client/ui/select.css";
@import "./Client/ui/fast-request.css";
@import "./Client/ui/table.css";
@import "tailwindcss";
@import "./Client/sidebar/sidebar.css";
@@ -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 { caseColumns } from '../../../data/casesTableColumns';
defineProps({
cases: {
type: Object,
required: true
}
})
</script>
<template>
<ClientLayout>
<h1 class="text-2xl font-bold">
Cases
</h1>
<UiCard title="لیست کیس‌ها" variant="default">
<TableGeneric
class="bg-white !rounded-2xl"
:data="cases.data"
customStyle="!text-[11px] !font-normal"
:searchable="false"
:columns="caseColumns"
:selectable="false"
:pagination="cases.meta"
:model-value="[]"
>
<template #column-actions="{ data }">
<div class="flex items-center justify-end">
<Link :href="`/dashboard/cases/${data.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>
@@ -11,6 +11,8 @@ import { ref } from 'vue';
import { Link } from '@inertiajs/inertia-vue3';
import FastRequestModal from '../../components/dashboard/FastRequestModal.vue';
import { useFastRequestStore } from '../../store/FastRequest';
import { modalities } from '../../data/fastRequestOptions';
import { caseColumns } from '../../data/casesTableColumns';
const fastRequestStore = useFastRequestStore();
const fastRequestModalVisible = ref(false);
@@ -24,90 +26,12 @@ defineProps({
user: {
type: Object,
required: true
},
cases: {
type: Object,
required: true
}
})
const data = ref([
{
id: 1,
file_number: '1234567890',
status: 'مشاهده نشد',
request_type: 'CT',
requester: 'محراب اسماعیل‌نیا',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'اتمام فرصت رسیدگی'
},
{
id: 2,
file_number: '1234567891',
status: 'مشاهده نشد',
request_type: 'CT',
requester: 'رضا گلیان',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: '۵۳ دقیقه'
},
{
id: 3,
file_number: '1234567892',
status: 'مشاهده نشد',
request_type: 'MRI',
requester: 'دکتر مریم مهدی‌پور',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۳۲ ساعت'
},
{
id: 4,
file_number: '1234567893',
status: 'در انتظار ثبت گزارش',
request_type: 'MRI',
requester: 'دکتر مریم مهدی‌پور',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۲۴ ساعت'
},
{
id: 5,
file_number: '1234567894',
status: 'در انتظار ثبت گزارش',
request_type: 'MRI',
requester: 'نام شخص و یا پزشک',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۵ ساعت'
}
])
const columns = ref([
{
title: 'شماره پرونده',
key: 'file_number'
},
{
title: 'وضعیت',
key: 'status'
},
{
title: 'نوع درخواست',
key: 'request_type'
},
{
title: 'درخواست‌کننده',
key: 'requester'
},
{
title: 'تاریخ ثبت',
key: 'registration_date'
},
{
title: 'فرصت رسیدگی',
key: 'processing_deadline'
},
{
title: '',
key: 'actions',
width: 80,
style: 'grid grid-cols-1',
slot: true,
}
])
</script>
<template>
@@ -116,27 +40,31 @@ const columns = ref([
<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]">
<RequestCard title="CT Scan" icon="ct-scan" severity="primary" @click="openFastRequestModal('ct-scan')"/>
<RequestCard title="MRI" icon="mri" severity="primary" @click="openFastRequestModal('mri')"/>
<RequestCard title="سونوگرافی" icon="sonography" severity="primary" @click="openFastRequestModal('sonography')"/>
<RequestCard title="اکوکاردیوگرافی" icon="echo" severity="primary" @click="openFastRequestModal('echo')"/>
<RequestCard title="رادیولوژی" icon="radiology" severity="primary" @click="openFastRequestModal('radiology')"/>
<RequestCard
v-for="modality in modalities"
:key="modality.code"
:value="modality.code"
:title="modality.title"
:icon="modality.icon"
severity="primary"
@click="openFastRequestModal(modality.code)"
/>
</div>
</UiCard>
<UiCard title="آخرین کیس‌های در انتظار بررسی" variant="default" class="mt-4">
<TableGeneric
class="bg-white !rounded-2xl"
:data="data"
:data="cases.data"
customStyle="!text-[11px] !font-normal"
:searchable="false"
:columns="columns"
:columns="caseColumns"
:selectable="false"
:pagination="null"
:model-value="[]"
>
<template #column-actions="{ data }" class="">
<div class="flex items-center justify-end">
<Link to="/dashboard/doctor/cases/1"
<Link :href="`/dashboard/cases/${data.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" />
@@ -4,157 +4,17 @@ import UiCard from '../../../components/ui/UiCard.vue'
import TableGeneric from '../../../components/ui/TableGeneric.vue'
import EyeSvg from '../../../components/svg/eye.svg'
import { Link } from '@inertiajs/inertia-vue3'
import { ref } from 'vue'
import { useDocumentStore } from '../../../store/Document'
import { caseColumns } from '../../../data/casesTableColumns'
const documentStore = useDocumentStore();
const selectedItems = ref([])
const data = ref([
{
id: 1,
file_number: '1234567890',
status: 'مشاهده نشد',
request_type: 'CT',
requester: 'محراب اسماعیل‌نیا',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'اتمام فرصت رسیدگی'
},
{
id: 2,
file_number: '1234567891',
status: 'مشاهده نشد',
request_type: 'CT',
requester: 'رضا گلیان',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: '۵۳ دقیقه'
},
{
id: 3,
file_number: '1234567892',
status: 'مشاهده نشد',
request_type: 'MRI',
requester: 'دکتر مریم مهدی‌پور',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۳۲ ساعت'
},
{
id: 4,
file_number: '1234567893',
status: 'در انتظار ثبت گزارش',
request_type: 'MRI',
requester: 'دکتر مریم مهدی‌پور',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۲۴ ساعت'
},
{
id: 5,
file_number: '1234567894',
status: 'در انتظار ثبت گزارش',
request_type: 'MRI',
requester: 'نام شخص و یا پزشک',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۵ ساعت'
},
{
id: 6,
file_number: '1234567893',
status: 'در انتظار ثبت گزارش',
request_type: 'MRI',
requester: 'دکتر مریم مهدی‌پور',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۲۴ ساعت'
},
{
id: 7,
file_number: '1234567894',
status: 'در انتظار ثبت گزارش',
request_type: 'MRI',
requester: 'نام شخص و یا پزشک',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۵ ساعت'
},
{
id: 8,
file_number: '1234567893',
status: 'در انتظار ثبت گزارش',
request_type: 'MRI',
requester: 'دکتر مریم مهدی‌پور',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۲۴ ساعت'
},
{
id: 9,
file_number: '1234567894',
status: 'در انتظار ثبت گزارش',
request_type: 'MRI',
requester: 'نام شخص و یا پزشک',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۵ ساعت'
},
{
id: 7,
file_number: '1234567894',
status: 'در انتظار ثبت گزارش',
request_type: 'MRI',
requester: 'نام شخص و یا پزشک',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۵ ساعت'
},
{
id: 8,
file_number: '1234567893',
status: 'در انتظار ثبت گزارش',
request_type: 'MRI',
requester: 'دکتر مریم مهدی‌پور',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۲۴ ساعت'
},
{
id: 9,
file_number: '1234567894',
status: 'در انتظار ثبت گزارش',
request_type: 'MRI',
requester: 'نام شخص و یا پزشک',
registration_date: '۱۴۰۴/۰۳/۰۲ ۱۴:۱۸',
processing_deadline: 'کمتر از ۵ ساعت'
defineProps({
documents: {
type: Object,
required: true
}
])
const columns = ref([
{
title: 'شماره پرونده',
key: 'file_number'
},
{
title: 'وضعیت',
key: 'status'
},
{
title: 'نوع درخواست',
key: 'request_type'
},
{
title: 'درخواست‌کننده',
key: 'requester'
},
{
title: 'تاریخ ثبت',
key: 'registration_date'
},
{
title: 'فرصت رسیدگی',
key: 'processing_deadline'
},
{
title: '',
key: 'actions',
width: 80,
style: 'grid grid-cols-1',
slot: true,
},
])
})
</script>
<template>
@@ -162,13 +22,13 @@ const columns = ref([
<UiCard title="کیس‌های در انتظار بررسی" variant="default" class="">
<TableGeneric
class="bg-white !rounded-2xl"
:data="data"
:data="documents.data"
customStyle="!text-[11px] !font-normal"
:searchable="false"
:columns="columns"
:columns="caseColumns"
:selectable="false"
:pagination="null"
:model-value="selectedItems"
:pagination="documents.meta"
:model-value="[]"
>
<template #column-actions="{ data }" class="">
<div class="flex items-center justify-end">
@@ -1,108 +1,96 @@
<script setup>
import UiButton from '../../components/ui/UiButton.vue';
import { ref, computed } from 'vue';
import Dialog from 'primevue/dialog';
import ProgressBar from 'primevue/progressbar';
import RequestType from './fastRequest/RequestType.vue';
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 UiButton from '../../components/ui/UiButton.vue';
import { computed, watch } from 'vue';
import Dialog from 'primevue/dialog';
import ProgressBar from 'primevue/progressbar';
import { toast } from 'vue3-toastify';
import RequestType from './fastRequest/RequestType.vue';
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 { useFastRequestStore } from '../../store/FastRequest';
const props = defineProps({
title: {
type: String,
required: true
},
icon: {
type: String,
required: true
}
});
const visible = defineModel('visible');
const fastRequestStore = useFastRequestStore();
const step = ref(1);
const stepRef = ref(null);
const steps = [
{ label: 'نوع درخواست', component: RequestType },
{ label: 'انتخاب کیس', component: RequestCase },
{ label: 'جزئیات درخواست', component: RequestDetails },
{ label: 'انتخاب پزشک', component: RequestDoctor },
{ label: 'مرور نهایی', component: RequestReview },
];
const steps = ref({
1: RequestType,
2: RequestCase,
3: RequestDetails,
4: RequestDoctor,
5: RequestReview,
});
const currentStepComponent = computed(() => {
return steps[fastRequestStore.step - 1]?.component || RequestType;
});
const currentStepComponent = computed(() => {
return steps.value[step.value] || RequestType
});
const handleNext = () => {
step.value++;
const isLastStep = computed(() => fastRequestStore.step === steps.length);
watch(visible, (isVisible) => {
if (isVisible) {
fastRequestStore.fetchCreateData();
} else {
fastRequestStore.reset();
}
});
const handleNext = async () => {
if (!fastRequestStore.canProceed) {
toast.error('لطفا اطلاعات این مرحله را کامل کنید.');
return;
}
if (!isLastStep.value) {
fastRequestStore.nextStep();
return;
}
try {
const document = await fastRequestStore.submit();
toast.success(`درخواست شما با شماره پرونده ${document.code} با موفقیت ثبت شد.`);
visible.value = false;
} catch (error) {
toast.error(error.response?.data?.message || 'خطا در ثبت درخواست. لطفا دوباره تلاش کنید.');
}
};
</script>
<template>
<Dialog :modal="true" :closable="true" :dismissableMask="true" :maximizable="false" :style="{ width: '688px' , minHeight: '376px' }" :pt="{header : '!bg-client-primaryBlue'}">
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="true" :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>
<div class="w-full h-[56px] flex items-center justify-between border-b border-[#EBEBEB] pb-3">
<div class="w-auto h-[40px] grid grid-cols-1 items-center justify-center gap-1">
<div class="w-full h-[20px] flex items-center justify-center ">
<span class="w-[20px] h-[20px] bg-[#576989] rounded-full text-white flex items-center justify-center text-[10px] font-[600]">1</span>
<template v-for="(item, index) in steps" :key="item.label">
<ProgressBar v-if="index > 0" :value="fastRequestStore.step > index ? 100 : 0" :showValue="false" mode="determinate"></ProgressBar>
<div class="w-auto h-[40px] grid grid-cols-1 items-center justify-center gap-1">
<div class="w-full h-[20px] flex items-center justify-center">
<span
class="w-[20px] h-[20px] rounded-full text-white flex items-center justify-center text-[10px] font-[600]"
:class="fastRequestStore.step >= index + 1 ? 'bg-[#576989]' : 'bg-[#C1D0E8]'"
>{{ index + 1 }}</span>
</div>
<span class="text-[10px] font-[600]" :class="fastRequestStore.step >= index + 1 ? 'text-[#576989]' : 'text-[#7188AD]'">{{ item.label }}</span>
</div>
<span class="text-[10px] font-[600] text-[#7188AD]">نوع درخواست</span>
</div>
<ProgressBar :value="value1" :showValue="false" mode="determinate"></ProgressBar>
<div class="w-auto h-[40px] grid grid-cols-1 items-center justify-center gap-1">
<div class="w-full h-[20px] flex items-center justify-center ">
<span class="w-[20px] h-[20px] bg-[#576989] rounded-full text-white flex items-center justify-center text-[10px] font-[600]">2</span>
</div>
<span class="text-[10px] font-[600] text-[#7188AD]">انتخاب کیس</span>
</div>
<ProgressBar :value="value2" :showValue="false" mode="determinate"></ProgressBar>
<div class="w-auto h-[40px] grid grid-cols-1 items-center justify-center gap-1">
<div class="w-full h-[20px] flex items-center justify-center ">
<span class="w-[20px] h-[20px] bg-[#576989] rounded-full text-white flex items-center justify-center text-[10px] font-[600]">3</span>
</div>
<span class="text-[10px] font-[600] text-[#7188AD]">جزئیات درخواست</span>
</div>
<ProgressBar :value="value3" :showValue="false" mode="determinate"></ProgressBar>
<div class="w-auto h-[40px] grid grid-cols-1 items-center justify-center gap-1">
<div class="w-full h-[20px] flex items-center justify-center ">
<span class="w-[20px] h-[20px] bg-[#576989] rounded-full text-white flex items-center justify-center text-[10px] font-[600]">4</span>
</div>
<span class="text-[10px] font-[600] text-[#7188AD]">انتخاب پزشک</span>
</div>
<ProgressBar :value="value4" :showValue="false" mode="determinate"></ProgressBar>
<div class="w-auto h-[40px] grid grid-cols-1 items-center justify-center gap-1">
<div class="w-full h-[20px] flex items-center justify-center ">
<span class="w-[20px] h-[20px] bg-[#576989] rounded-full text-white flex items-center justify-center text-[10px] font-[600]">5</span>
</div>
<span class="text-[10px] font-[600] text-[#7188AD]">مرور نهایی</span>
</div>
</template>
</div>
<component :is="currentStepComponent" ref="stepRef" @validated="onValidated"/>
<component :is="currentStepComponent" />
<div class="w-full pt-3 flex items-center justify-end border-t border-[#EBEBEB]">
<ui-button label="تایید و مرحله بعد" severity="success" size="small" @click="handleNext" />
<div class="w-full pt-3 flex items-center justify-between border-t border-[#EBEBEB]">
<ui-button v-if="fastRequestStore.step > 1" label="مرحله قبل" severity="secondary" size="small" @click="fastRequestStore.prevStep" />
<span v-else></span>
<ui-button
:label="isLastStep ? 'ثبت نهایی درخواست' : 'تایید و مرحله بعد'"
severity="success"
size="small"
:loading="fastRequestStore.submitting"
:disabled="fastRequestStore.submitting"
@click="handleNext"
/>
</div>
</Dialog>
</template>
<style scoped>
:deep(.p-progressbar) {
min-width: 60px !important;
height: 4px;
}
:deep(.p-progressbar-value) {
background: #576989 !important;
}
</style>
@@ -2,46 +2,36 @@
import Dialog from 'primevue/dialog';
import UiButton from '../../ui/UiButton.vue';
import TableGeneric from '../../ui/TableGeneric.vue';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import CaseNumber from './CaseNumber.vue';
import { useFastRequestStore } from '../../../store/FastRequest';
const visible = defineModel('visible');
const fastRequestStore = useFastRequestStore();
const isChooseCaseNumber = ref(false);
const selectedPetId = ref(null);
const showChooseCaseNumber = () => {
isChooseCaseNumber.value = true;
}
const data = ref([
{
id: 1,
case_number: '1234567890',
case_name: 'مایلو',
case_type: 'سگ',
},
{
id: 2,
case_number: '1234567891',
case_name: 'تدی',
case_type: 'گربه',
},
{
id: 3,
case_number: '1234567892',
case_name: 'پونی',
case_type: 'خرگوش',
},
{
id: 4,
case_number: '1234567893',
case_name: 'روبیک',
case_type: 'همستر',
},
{
id: 5,
case_number: '1234567894',
case_name: 'کولا',
case_type: 'خوکچه هندی',
}
]);
const data = computed(() => fastRequestStore.pets.map((pet) => ({
id: pet.id,
case_number: pet.id,
case_name: pet.name,
case_type: pet.breed ? `${pet.type} (${pet.breed})` : pet.type,
})));
const selectPet = (petId) => {
selectedPetId.value = petId;
}
const confirmSelection = () => {
if (!selectedPetId.value) return;
fastRequestStore.selectArchivePet(selectedPetId.value);
visible.value = false;
}
const columns = ref([
{
@@ -65,9 +55,9 @@ const columns = ref([
}
]);
</script>
<template>
<Dialog :modal="true" :closable="true" :dismissableMask="true" :maximizable="false" :style="{ width: '554px' , minHeight: '326px' }">
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="true" :maximizable="false" :style="{ width: '554px' , minHeight: '326px' }" class="fast-request-dialog">
<template #header>
<h3>انتخاب کیس از آرشیو</h3>
</template>
@@ -76,7 +66,9 @@ const columns = ref([
<p>در صورتیکه شماره مرجع یک کیس را در اختیار داشته باشید می‌توانید درخواست را برای کیس مربوطه نیز ثبت کنید.</p>
</div>
<div class="h-[180px] overflow-y-scroll">
<p v-if="!data.length" class="w-full py-8 text-center text-[11px] font-[400] text-[#7188AD]">کیسی در آرشیو شما ثبت نشده است.</p>
<TableGeneric
v-else
class="bg-white !rounded-2xl"
:data="data"
customStyle="!text-[11px] !font-normal"
@@ -88,25 +80,20 @@ const columns = ref([
>
<template #column-actions="{ data }">
<div class="flex items-center justify-end">
<UiButton severity="secondary" size="small">
<template #icon>
</template>
</UiButton>
<UiButton
:severity="selectedPetId === data.id ? 'success' : 'secondary'"
size="small"
:label="selectedPetId === data.id ? 'انتخاب شد' : 'انتخاب'"
@click="selectPet(data.id)"
/>
</div>
</template>
</TableGeneric>
</div>
<div class="w-full pt-3 flex items-center justify-end gap-2 border-t border-[#EBEBEB]">
<ui-button label="افزودن از طریق شناسه کیس" severity="secondary" @click="showChooseCaseNumber"/>
<ui-button label="تایید و انتخاب" severity="success" size="small" @click="handleNext" />
<ui-button label="تایید و انتخاب" severity="success" size="small" :disabled="!selectedPetId" @click="confirmSelection" />
</div>
<case-number v-model:visible="isChooseCaseNumber"></case-number>
<case-number v-model:visible="isChooseCaseNumber" @selected="visible = false"></case-number>
</Dialog>
</template>
<style scoped>
</style>
@@ -1,45 +1,43 @@
<script setup>
import Dialog from 'primevue/dialog';
import InputText from 'primevue/inputtext';
import UiButton from '../../ui/UiButton.vue';
import { ref } from 'vue';
import { useFastRequestStore } from '../../../store/FastRequest';
const visible = defineModel('visible');
const emit = defineEmits(['selected']);
const fastRequestStore = useFastRequestStore();
const caseNumber = ref(null);
const confirmCaseNumber = () => {
if (!caseNumber.value) return;
fastRequestStore.selectArchivePet(Number(caseNumber.value));
caseNumber.value = null;
visible.value = false;
emit('selected');
}
</script>
<template>
<Dialog :modal="true" :closable="true" :dismissableMask="true" :maximizable="false" :style="{ width: '408px' , minHeight: '210px' }">
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="true" :maximizable="false" :style="{ width: '408px' , minHeight: '210px' }" class="fast-request-dialog">
<template #header>
<h3>افزودن کیس از طریق شناسه مرجع</h3>
</template>
<div class="w-full py-2 flex flex-col items-start justify-center border-t border-[#EBEBEB] text-[11px] font-[600]">
<p>لطفا شناسه کیس که از پزشک و یا سرپرست کیس دریافت کرده‌اید را وارد نمایید.</p>
</div>
<Form ref="formRef" v-slot="$form" class="p-4 grid grid-cols-1 !w-full">
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col">
<InputText
type="text"
placeholder="شناسه کیس"
size="large"/>
</FormField>
</Form>
<form class="p-4 grid grid-cols-1 w-full" @submit.prevent="confirmCaseNumber">
<InputText
v-model="caseNumber"
type="number"
placeholder="شناسه کیس"
size="large"
/>
</form>
<div class="w-full pt-3 flex items-center justify-end gap-2 border-t border-[#EBEBEB]">
<ui-button label="تایید و افزودن" severity="success" size="small"/>
<ui-button label="تایید و افزودن" severity="success" size="small" :disabled="!caseNumber" @click="confirmCaseNumber"/>
</div>
</Dialog>
</template>
<style scoped>
:deep(.p-inputtext::placeholder) {
color: #6E91C9;
}
:deep(.p-inputtext) {
border: 1px solid #DEE3EB !important;
border-radius: 8px !important;
font-size: 13px !important;
font-weight: 400 !important;
}
</style>
@@ -0,0 +1,68 @@
<script setup>
import { ref } from 'vue';
import FileUpload from 'primevue/fileupload';
import Badge from 'primevue/badge';
import { formatFileSize } from '../../../utils/fileUtils';
const files = defineModel({ type: Array, default: () => [] });
defineProps({
title: {
type: String,
required: true
}
});
const fileUpload = ref(null);
const openFileDialog = () => {
fileUpload.value.choose();
}
const onSelectedFiles = (event) => {
files.value = event.files;
}
const onRemoveFile = (removeFileCallback, index) => {
removeFileCallback(index);
files.value = fileUpload.value.files;
}
</script>
<template>
<FileUpload
ref="fileUpload"
class="w-full !border-dashed !border-[#C1D0E8] !min-h-[100px] flex items-center justify-center"
name="files[]"
:customUpload="true"
:showUploadButton="false"
:showCancelButton="false"
:showChooseButton="false"
:multiple="true"
accept="image/*"
:maxFileSize="10485760"
@select="onSelectedFiles"
>
<template #content="{ files: selectedFiles, removeFileCallback }">
<div v-if="selectedFiles.length > 0" class="flex flex-wrap gap-4 pt-4">
<div v-for="(file, index) of selectedFiles" :key="file.name + file.type + file.size" class="relative w-[5rem] h-[5rem] rounded-lg border border-gray-200 flex flex-col items-center overflow-hidden">
<img role="presentation" :alt="file.name" :src="file.objectURL" class="absolute top-0 left-0 w-full h-full object-cover"/>
<div class="absolute bottom-0 left-0 w-full flex flex-col items-center gap-1 bg-white/80 p-1">
<span class="w-full text-[10px] font-[400] text-center text-ellipsis whitespace-nowrap overflow-hidden">{{ file.name }}</span>
<span class="text-[9px]">{{ formatFileSize(file.size) }}</span>
</div>
<button type="button" class="absolute top-1 left-1 w-4 h-4 flex items-center justify-center rounded-full bg-white text-[#7A1419] text-[10px] cursor-pointer" @click.stop="onRemoveFile(removeFileCallback, index)">✕</button>
</div>
<button type="button" class="w-[5rem] h-[5rem] rounded-lg border border-dashed border-[#C1D0E8] text-[11px] text-[#365891] cursor-pointer" @click="openFileDialog">افزودن</button>
</div>
</template>
<template #empty>
<div class="w-full flex items-center justify-center flex-col cursor-pointer" @click="openFileDialog">
<p class="text-[11px] font-[600] text-[#365891]">{{ title }}</p>
<p class="mt-1 mb-0 text-[11px] font-[400] text-[#365891]">
جهت آپلود تصویر کلیک کنید و یا فایل خود را کشیده و در این قسمت رها (Drag and Drop) کنید.
</p>
</div>
</template>
</FileUpload>
</template>
@@ -1,172 +1,86 @@
<script setup>
import Select from 'primevue/select';
import InputText from 'primevue/inputtext';
import { ref } from 'vue';
import ArchiveCase from './ArchiveCase.vue';
import UiInput from '../../ui/UiInput.vue';
import { useFastRequestStore } from '../../../store/FastRequest';
import { petTypes, petGenders, neuteredOptions } from '../../../data/fastRequestOptions';
const fastRequestStore = useFastRequestStore();
const isChooseArchive = ref(false);
const showChooseArchive = () => {
isChooseArchive.value = true;
}
const genders = ref([
{ name: 'نر', code: 'male' },
{ name: 'ماده', code: 'female' }
]);
const types = ref([
{ name: 'سگ', code: 'dog' },
{ name: 'گربه', code: 'cat' },
{ name: 'پرنده', code: 'bird' },
{ name: 'خرگوش', code: 'rabbit' },
{ name: 'همستر', code: 'hamster' },
{ name: 'خوکچه هندی', code: 'guinea_pig' },
{ name: 'لاک پشت', code: 'turtle' },
{ name: 'مار', code: 'snake' },
{ name: 'مارمولک', code: 'lizard' },
]);
const neuteredStatus = ref([
{ name: 'عقیم سازی شده', code: 'neutered' },
{ name: 'عقیم سازی نشده', code: 'not_neutered' }
]);
</script>
<template>
<div class="w-full space-y-2 py-6">
<div class="w-full h-[12px] flex items-center justify-between">
<p class="flex h-full items-center justify-start text-[11px] font-[600] text-[#3E4C63]">مشخصات کیس(حیوان) مورد نظر را وارد نمایید.</p>
<button @click="showChooseArchive" class="flex h-full items-center justify-end text-[11px] font-[400] text-[#7A1419] cursor-pointer">انتخاب از آرشیو</button>
<button type="button" @click="showChooseArchive" class="flex h-full items-center justify-end text-[11px] font-[400] text-[#7A1419] cursor-pointer">انتخاب از آرشیو</button>
</div>
<p class="flex items-center justify-start text-[11px] font-[600] text-[#576989]">
در صورتی که قبلا این کیس در پلتفرم رادیکال ثبت شده، برای بررسی سوابق قبلی از دکمه «انتخاب از آرشیو» استفاده نمایید.
</p>
<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">
<!-- <InputText
<div v-if="fastRequestStore.requestCase.petId" class="w-full flex items-center justify-between border border-[#DEE3EB] rounded-[8px] p-3">
<p class="text-[11px] font-[600] text-[#3E4C63]">
کیس انتخاب‌شده از آرشیو:
<span v-if="fastRequestStore.selectedPet" class="text-[#7A1419]">{{ fastRequestStore.selectedPet.name }} ({{ fastRequestStore.selectedPet.type }})</span>
<span v-else class="text-[#7A1419]">شناسه {{ fastRequestStore.requestCase.petId }}</span>
</p>
<button type="button" @click="fastRequestStore.clearArchivePet" class="text-[11px] font-[400] text-[#7A1419] cursor-pointer">حذف و ثبت کیس جدید</button>
</div>
<form v-else class="p-4 grid grid-cols-2 gap-4 w-full">
<InputText
v-model="fastRequestStore.requestCase.name"
type="text"
placeholder="نام"
size="large"/> -->
<UiInput
type="text"
size="large"
prefixStart="نام"
direction="ltr"
/>
</FormField>
<FormField v-slot="$field" name="type" initialValue="" class="flex flex-col">
<Select
:options="types"
size="large"
/>
<Select
v-model="fastRequestStore.requestCase.type"
:options="petTypes"
optionLabel="name"
optionValue="code"
placeholder="نوع"
/>
</FormField>
<FormField v-slot="$field" name="breed" initialValue="" class="flex flex-col">
<InputText
type="text"
placeholder="نژاد"
size="large"/>
</FormField>
<FormField v-slot="$field" name="gender" initialValue="" class="flex flex-col">
<Select :options="genders" optionLabel="name" optionValue="code" placeholder="جنسیت" size="small"/>
</FormField>
<FormField v-slot="$field" name="age" initialValue="" class="flex flex-col">
<InputText
type="text"
placeholder="سن"
size="large"
/>
</FormField>
<FormField v-slot="$field" name="birthday" initialValue="" class="flex flex-col">
<InputText
type="text"
placeholder="وزن"
size="large"
/>
</FormField>
<FormField v-slot="$field" name="sterilized" initialValue="" class="flex flex-col">
<Select
:options="neuteredStatus"
/>
<InputText
v-model="fastRequestStore.requestCase.breed"
type="text"
placeholder="نژاد"
size="large"
/>
<Select
v-model="fastRequestStore.requestCase.gender"
:options="petGenders"
optionLabel="name"
optionValue="code"
placeholder="جنسیت"
size="small"
/>
<InputText
v-model="fastRequestStore.requestCase.age"
type="number"
placeholder="سن (سال)"
size="large"
/>
<InputText
v-model="fastRequestStore.requestCase.weight"
type="number"
placeholder="وزن (کیلوگرم)"
size="large"
/>
<Select
v-model="fastRequestStore.requestCase.sterilized"
:options="neuteredOptions"
optionLabel="name"
optionValue="code"
placeholder="وضعیت عقیم سازی"
size="small"
/>
</FormField>
</Form>
/>
</form>
<archive-case v-model:visible="isChooseArchive"></archive-case>
</div>
</template>
<style scoped>
:deep(.p-select) {
border: 1px solid #DEE3EB !important;
border-radius: 8px !important;
height: 37px !important;
display: flex !important;
align-items: center !important;
}
:deep(.p-select-label) {
padding: 0 12px;
line-height: 1;
display: flex;
align-items: center;
height: 100%;
}
:deep(.p-select-label.p-placeholder) {
color: #6E91C9 !important;
font-size: 11px !important;
font-weight: 400 !important;
}
:deep(.p-inputtext) {
font-size: 11px !important;
font-weight: 400 !important;
border: 1px solid #DEE3EB !important;
border-radius: 8px !important;
}
:deep(.p-inputtext::placeholder) {
color: #6E91C9;
}
:deep(.vpd-main) {
border: 1px solid #DEE3EB !important;
border-radius: 8px !important;
overflow: hidden !important;
height: 45px !important;
}
:deep(.vpd-input-group) {
border: 0 !important;
font-size: 16px !important;
border-radius: 8px !important;
border: 0 !important;
font-weight: 400 !important;
height: 45px !important;
}
:deep(.vpd-input-group .vpd-placeholder) {
font-size: 13px !important;
font-weight: 400 !important;
color: #3E4C63 !important;
}
:deep(.vpd-input-group input) {
border: 0 !important;
}
:deep(.vpd-icon-btn) {
background: #576989 !important;
}
:deep(.vpd-content) {
border-radius: 12px !important;
overflow: hidden !important;
border: 0 !important;
}
:deep(.vpd-wrapper) {
background-color: rgba(0,0,0,.1) !important;
}
</style>
@@ -1,256 +1,37 @@
<script setup>
import { ref } from 'vue';
import { usePrimeVue } from 'primevue/config';
import { useToast } from "primevue/usetoast";
const $primevue = usePrimeVue();
const toast = useToast();
const totalSize = ref(0);
const totalSizePercent = ref(0);
const files = ref([]);
const fileUpload = ref(null)
const openFileDialog = () => {
fileUpload.value.choose()
}
const onRemoveTemplatingFile = (file, removeFileCallback, index) => {
removeFileCallback(index);
totalSize.value -= parseInt(formatSize(file.size));
totalSizePercent.value = totalSize.value / 10;
};
const onClearTemplatingUpload = (clear) => {
clear();
totalSize.value = 0;
totalSizePercent.value = 0;
};
const onSelectedFiles = (event) => {
files.value = event.files;
files.value.forEach((file) => {
totalSize.value += parseInt(formatSize(file.size));
});
};
const uploadEvent = (callback) => {
totalSizePercent.value = totalSize.value / 10;
callback();
};
const onTemplatedUpload = () => {
toast.add({ severity: "info", summary: "Success", detail: "File Uploaded", life: 3000 });
};
const formatSize = (bytes) => {
const k = 1024;
const dm = 3;
const sizes = $primevue.config.locale.fileSizeTypes;
if (bytes === 0) {
return `0 ${sizes[0]}`;
}
const i = Math.floor(Math.log(bytes) / Math.log(k));
const formattedSize = parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
return `${formattedSize} ${sizes[i]}`;
};
import Select from 'primevue/select';
import Textarea from 'primevue/textarea';
import FileUploadBox from './FileUploadBox.vue';
import { useFastRequestStore } from '../../../store/FastRequest';
import { priorities, modalityTitle } from '../../../data/fastRequestOptions';
const fastRequestStore = useFastRequestStore();
</script>
<template>
<div class="w-full">
<p class="text-[11px] font-[400] text-[#3E4C63] py-3">جزئیات درخواست و اطلاعات مورد نیاز را با دقت و حوصله وار نمایید.</p>
<p class="text-[11px] font-[400] text-[#3E4C63] py-3">جزئیات درخواست و اطلاعات مورد نیاز را با دقت و حوصله وارد نمایید.</p>
<div class="w-full grid grid-cols-2 gap-2 items-center justify-between">
<Select
:options="types"
optionLabel="name"
optionValue="code"
placeholder="نوع درخواست"
/>
<span class="text-[11px] font-[400] text-[#7188AD]">زمان تخمینی ثبت گزارش: ۷۲ ساعت</span>
<Select
v-model="fastRequestStore.requestDetails.priority"
:options="priorities"
optionLabel="name"
optionValue="code"
placeholder="اولویت درخواست"
/>
<span class="text-[11px] font-[400] text-[#7188AD]">زمان تخمینی ثبت گزارش: ۷۲ ساعت</span>
</div>
<div class="w-full h-[118px] mt-3">
<Textarea
v-model="value1"
size="small"
placeholder="شرح درخواست و توضیحات"
rows="3"
class="w-full h-full" />
v-model="fastRequestStore.requestDetails.note"
size="small"
placeholder="شرح درخواست و توضیحات"
rows="3"
class="w-full h-full" />
</div>
<div class="w-full grid grid-cols-2 gap-2 items-center justify-between my-4">
<FileUpload class="w-full !border-dashed !border-[#C1D0E8] !min-h-[100px] flex items-center justify-center" ref="fileUpload" name="files[]" :customUpload="true" :showUploadButton="false" :showCancelButton="false" :showChooseButton="false" url="/api/upload" @upload="onTemplatedUpload($event)" :multiple="true" accept="image/*" :maxFileSize="1000000" @select="onSelectedFiles">
<template #content="{ files, uploadedFiles, removeUploadedFileCallback, removeFileCallback }">
<div class="flex flex-col gap-8 pt-4">
<div v-if="files.length > 0">
<div class="flex flex-wrap gap-4">
<div v-for="(file, index) of files" :key="file.name + file.type + file.size" class="relative w-[5rem] h-[5rem] rounded-lg border border-gray-200 bg-amber-100 flex flex-col items-center gap-4 overflow-hidden">
<div class="w-full h-full absolute top-0 left-0">
<img role="presentation" :alt="file.name" :src="file.objectURL" class="w-full h-full object-cover"/>
</div>
<div class="w-full h-full flex flex-col items-center justify-center gap-2 absolute bottom-0 left-0">
<span class="w-full px-2 text-xs font-[400] text-ellipsis whitespace-nowrap overflow-hidden">{{ file.name }}</span>
<div>{{ formatSize(file.size) }}</div>
<Badge value="Pending" severity="warn" />
</div>
</div>
</div>
</div>
<div v-if="uploadedFiles.length > 0">
<h5>Completed</h5>
<div class="flex flex-wrap gap-4">
<div v-for="(file, index) of uploadedFiles" :key="file.name + file.type + file.size" class="p-8 rounded-border flex flex-col border border-surface items-center gap-4">
<div>
<img role="presentation" :alt="file.name" :src="file.objectURL" width="100" height="50" />
</div>
<span class="font-semibold text-ellipsis max-w-60 whitespace-nowrap overflow-hidden">{{ file.name }}</span>
<div>{{ formatSize(file.size) }}</div>
<Badge value="Completed" class="mt-4" severity="success" />
<Button icon="pi pi-times" @click="removeUploadedFileCallback(index)" variant="outlined" rounded severity="danger" />
</div>
</div>
</div>
</div>
</template>
<template #empty>
<div class="w-full flex items-center justify-center flex-col cursor-pointer" @click="openFileDialog">
<Button
icon="pi pi-images"
rounded
variant="outlined"
severity="secondary"
class="!hidden"
@click.stop="openFileDialog"/>
<p class="text-[11px] font-[600] text-[#365891]">سوابق پزشکی قبلی (اختیاری)</p>
<p class="mt-1 mb-0 text-[11px] font-[400] text-[#365891]">
جهت آپلود تصویر کلیک کنید و یا فایل خود کشیده و در این قسمت رها (Drag and Drup) کنید.
</p>
</div>
</template>
</FileUpload>
<FileUpload class="w-full !border-dashed !border-[#C1D0E8] !min-h-[100px] flex items-center justify-center" ref="fileUpload" name="files[]" :customUpload="true" :showUploadButton="false" :showCancelButton="false" :showChooseButton="false" url="/api/upload" @upload="onTemplatedUpload($event)" :multiple="true" accept="image/*" :maxFileSize="1000000" @select="onSelectedFiles">
<template #content="{ files, uploadedFiles, removeUploadedFileCallback, removeFileCallback }">
<div class="flex flex-col gap-8 pt-4">
<div v-if="files.length > 0">
<div class="flex flex-wrap gap-4">
<div v-for="(file, index) of files" :key="file.name + file.type + file.size" class="relative w-[5rem] h-[5rem] rounded-lg border border-gray-200 bg-amber-100 flex flex-col items-center gap-4 overflow-hidden">
<div class="w-full h-full absolute top-0 left-0">
<img role="presentation" :alt="file.name" :src="file.objectURL" class="w-full h-full object-cover"/>
</div>
<div class="w-full h-full flex flex-col items-center justify-center gap-2 absolute bottom-0 left-0">
<span class="w-full px-2 text-xs font-[400] text-ellipsis whitespace-nowrap overflow-hidden">{{ file.name }}</span>
<div>{{ formatSize(file.size) }}</div>
<Badge value="Pending" severity="warn" />
</div>
</div>
</div>
</div>
<div v-if="uploadedFiles.length > 0">
<h5>Completed</h5>
<div class="flex flex-wrap gap-4">
<div v-for="(file, index) of uploadedFiles" :key="file.name + file.type + file.size" class="p-8 rounded-border flex flex-col border border-surface items-center gap-4">
<div>
<img role="presentation" :alt="file.name" :src="file.objectURL" width="100" height="50" />
</div>
<span class="font-semibold text-ellipsis max-w-60 whitespace-nowrap overflow-hidden">{{ file.name }}</span>
<div>{{ formatSize(file.size) }}</div>
<Badge value="Completed" class="mt-4" severity="success" />
<Button icon="pi pi-times" @click="removeUploadedFileCallback(index)" variant="outlined" rounded severity="danger" />
</div>
</div>
</div>
</div>
</template>
<template #empty>
<div class="w-full flex items-center justify-center flex-col cursor-pointer" @click="openFileDialog">
<Button
icon="pi pi-images"
rounded
variant="outlined"
severity="secondary"
class="!hidden"
@click.stop="openFileDialog"/>
<p class="text-[11px] font-[600] text-[#365891]">فایل «CT Scan»</p>
<p class="mt-1 mb-0 text-[11px] font-[400] text-[#365891]">
جهت آپلود تصویر کلیک کنید و یا فایل خود کشیده و در این قسمت رها (Drag and Drup) کنید.
</p>
</div>
</template>
</FileUpload>
<div class="w-full grid grid-cols-2 gap-2 items-start justify-between my-4">
<FileUploadBox v-model="fastRequestStore.historyFiles" title="سوابق پزشکی قبلی (اختیاری)" />
<FileUploadBox v-model="fastRequestStore.scanFiles" :title="`فایل «${modalityTitle(fastRequestStore.requestType)}»`" />
</div>
</div>
</template>
<style scoped>
:deep(.p-select) {
border: 1px solid #DEE3EB !important;
border-radius: 8px !important;
height: 37px !important;
display: flex !important;
align-items: center !important;
}
:deep(.p-select-label) {
padding: 0 12px;
line-height: 1;
display: flex;
align-items: center;
height: 100%;
}
:deep(.p-select-label.p-placeholder) {
color: #6E91C9 !important;
font-size: 11px !important;
font-weight: 400 !important;
}
:deep(.p-textarea) {
border: 1px solid #DEE3EB !important;
border-radius: 8px !important;
height: 100% !important;
display: flex !important;
align-items: center !important;
}
:deep(.p-textarea::placeholder) {
color: #6E91C9 !important;
font-size: 13px !important;
font-weight: 400 !important;
}
.p-fileupload-content {
pointer-events: auto !important;
}
:deep(.p-fileupload-choose-button) {
display: none !important;
}
:deep(.p-fileupload-header) {
display: none !important;
}
:deep(.p-fileupload) {
width: 100% !important;
height: 100% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
:deep(.p-fileupload-advanced) {
border: none !important;
border: 2px solid #C1D0E8 !important;
border-style: dashed !important;
border-radius: 6px !important;
}
</style>
@@ -1,29 +1,35 @@
<script setup>
import { useFastRequestStore } from '../../../store/FastRequest';
const fastRequestStore = useFastRequestStore();
const selectDoctor = (doctorId) => {
fastRequestStore.doctorId = doctorId;
}
</script>
<template>
<div class="w-full h-full grid grid-cols-3 gap-2 items-center justify-between py-4">
<div class="w-full bg-white h-[134px] flex flex-col items-center justify-center border-[2px] border-[#576989] rounded-[12px]">
<button
type="button"
@click="selectDoctor(null)"
class="w-full bg-white h-[134px] flex flex-col items-center justify-center rounded-[12px] cursor-pointer"
:class="fastRequestStore.doctorId === null ? 'border-[2px] border-[#576989]' : 'border-[1px] border-[#DEE3EB]'"
>
<h3 class="text-[13px] font-[600] text-[#576989]">انتخاب خودکار</h3>
<p class="w-[190px] text-center text-[11px] font-[600] text-[#3E4C63]">انتخاب در دسترس‌ ترین پزشک جهت بررسی سریع درخواست شما</p>
</div>
<div class="w-full bg-white h-[134px] flex flex-col items-center justify-center border-[1px] border-[#DEE3EB] rounded-[12px]">
<img src="/public/images/profile_default.png" alt="doctor" class="w-[65px] h-[65px] rounded-[50px] object-contain ">
<h3 class="text-[13px] font-[600] text-[#576989]">دکتر حسین حسینی</h3>
<p class="text-center text-[11px] font-[600] text-[#7188AD]">متخصص جراحی پوست و مو</p>
</div>
<div class="w-full bg-white h-[134px] flex flex-col items-center justify-center border-[1px] border-[#DEE3EB] rounded-[12px]">
<img src="/public/images/profile_default.png" alt="doctor" class="w-[65px] h-[65px] rounded-[50px] object-contain ">
<h3 class="text-[13px] font-[600] text-[#576989]">دکتر امین حسینی</h3>
<p class="text-center text-[11px] font-[600] text-[#7188AD]">متخصص و رادیولوژیست</p>
</div>
</button>
<button
v-for="doctor in fastRequestStore.doctors"
:key="doctor.id"
type="button"
@click="selectDoctor(doctor.id)"
class="w-full bg-white h-[134px] flex flex-col items-center justify-center rounded-[12px] cursor-pointer"
:class="fastRequestStore.doctorId === doctor.id ? 'border-[2px] border-[#576989]' : 'border-[1px] border-[#DEE3EB]'"
>
<img :src="doctor.profile_url || '/images/profile_default.png'" :alt="doctor.fullname" class="w-[65px] h-[65px] rounded-full object-cover">
<h3 class="text-[13px] font-[600] text-[#576989]">{{ doctor.fullname }}</h3>
<p class="text-center text-[11px] font-[600] text-[#7188AD]">{{ doctor.specialty }}</p>
</button>
</div>
</template>
<style scoped>
</style>
@@ -1,15 +1,22 @@
<script setup>
import { computed } from 'vue';
import { useFastRequestStore } from '../../../store/FastRequest';
import { modalityTitle, priorityTitle } from '../../../data/fastRequestOptions';
const fastRequestStore = useFastRequestStore();
const caseName = computed(() => fastRequestStore.selectedPet?.name || fastRequestStore.requestCase.name || 'کیس جدید');
const caseNumber = computed(() => fastRequestStore.requestCase.petId || 'جدید');
const doctorName = computed(() => fastRequestStore.selectedDoctor?.fullname || 'اولین پزشک در دسترس');
</script>
<template>
<div class="w-full h-full flex flex-col items-start justify-between gap-8 py-8">
<p class="text-[13px] font-[400] text-[#3E4C63]">
شما در حال تکمیل فرایند ثبت درخواست
<span class="font-[600] text-[#7A1419]">((CT Scan))</span>
شما در حال تکمیل فرایند ثبت درخواست
<span class="font-[600] text-[#7A1419]">(({{ modalityTitle(fastRequestStore.requestType) }}))</span>
با اولویت
<span class="font-[600] text-[#7A1419]">((اورژانسی))</span> برای کیس <span class="font-[600] text-[#7A1419]">((مایلو))</span> با شماره کیس <span class="font-[600] text-[#7A1419]">((۱۲۳۴۵۶))</span> می‌باشید. درخواست شما در ابتدا توسط کارشناسان ما بررسی و در صورت تایید به <span class="font-[600] text-[#7A1419]">((اولین پزشک در دسترس))</span> جهت بررسی و گزارش نویسی ارجاع خواهد شد.
<span class="font-[600] text-[#7A1419]">(({{ priorityTitle(fastRequestStore.requestDetails.priority) }}))</span> برای کیس <span class="font-[600] text-[#7A1419]">(({{ caseName }}))</span> با شماره کیس <span class="font-[600] text-[#7A1419]">(({{ caseNumber }}))</span> می‌باشید. درخواست شما در ابتدا توسط کارشناسان ما بررسی و در صورت تایید به <span class="font-[600] text-[#7A1419]">(({{ doctorName }}))</span> جهت بررسی و گزارش نویسی ارجاع خواهد شد.
</p>
<p class="text-[13px] font-[400] text-[#3E4C63]">
وضعیت و مراحل پرونده از طریق <span class="font-[600] text-[#7A1419]">((پیامک))</span> به شما اطلاع‌رسانی خواهد شد.
@@ -17,9 +24,3 @@
</div>
</template>
<style scoped>
</style>
@@ -1,23 +1,21 @@
<script setup>
import RequestCard from '../../../components/dashboard/RequestCard.vue';
import RequestCard from '../../../components/dashboard/RequestCard.vue';
import { modalities } from '../../../data/fastRequestOptions';
</script>
<template>
<div class="w-full space-y-3 py-6">
<p class="h-[17px] flex items-center justify-start text-[11px] font-[600] text-[#3E4C63]">نوع درخواست خود را مشخص کنید</p>
<div class="w-full h-[133px] grid grid-cols-5 gap-3 justify-start">
<RequestCard value="ct-scan" title="CT Scan" icon="ct-scan" severity="primary" :selectable="true"/>
<RequestCard value="mri" title="MRI" icon="mri" severity="primary" :selectable="true"/>
<RequestCard value="sonography" title="سونوگرافی" icon="sonography" severity="primary" :selectable="true"/>
<RequestCard value="echo" title="اکوکاردیوگرافی" icon="echo" severity="primary" :selectable="true"/>
<RequestCard value="radiology" title="رادیولوژی" icon="radiology" severity="primary" :selectable="true"/>
<RequestCard
v-for="modality in modalities"
:key="modality.code"
:value="modality.code"
:title="modality.title"
:icon="modality.icon"
severity="primary"
:selectable="true"
/>
</div>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,39 @@
import { modalityTitle } from './fastRequestOptions'
import { convertEnglishToPersian } from '../utils/numberUtils'
export const caseColumns = [
{
title: 'شماره پرونده',
key: 'code'
},
{
title: 'وضعیت',
key: 'status'
},
{
title: 'نوع درخواست',
key: 'modality',
render: (row) => modalityTitle(row.modality)
},
{
title: 'درخواست‌کننده',
key: 'requester'
},
{
title: 'تاریخ ثبت',
key: 'submitted_at',
render: (row) => row.submitted_at ? convertEnglishToPersian(row.submitted_at) : '-'
},
{
title: 'فرصت رسیدگی',
key: 'processing_deadline',
render: (row) => row.processing_deadline ? convertEnglishToPersian(row.processing_deadline) : '-'
},
{
title: '',
key: 'actions',
width: 80,
style: 'grid grid-cols-1',
slot: true,
}
]
@@ -0,0 +1,44 @@
export const modalities = [
{ code: 'ct-scan', title: 'CT Scan', icon: 'ct-scan' },
{ code: 'mri', title: 'MRI', icon: 'mri' },
{ code: 'sonography', title: 'سونوگرافی', icon: 'sonography' },
{ code: 'echo', title: 'اکوکاردیوگرافی', icon: 'echo' },
{ code: 'radiology', title: 'رادیولوژی', icon: 'radiology' },
]
export const petTypes = [
{ name: 'سگ', code: 'Dog' },
{ name: 'گربه', code: 'Cat' },
{ name: 'پرنده', code: 'Bird' },
{ name: 'ماهی', code: 'Fish' },
{ name: 'خرگوش', code: 'Rabbit' },
{ name: 'همستر', code: 'Hamster' },
{ name: 'خوکچه هندی', code: 'GuineaPig' },
{ name: 'لاک پشت', code: 'Turtle' },
{ name: 'مار', code: 'Snake' },
{ name: 'مارمولک', code: 'Lizard' },
{ name: 'سایر', code: 'Other' },
]
export const petGenders = [
{ name: 'نر', code: 'Male' },
{ name: 'ماده', code: 'Female' },
]
export const neuteredOptions = [
{ name: 'عقیم سازی شده', code: true },
{ name: 'عقیم سازی نشده', code: false },
]
export const priorities = [
{ name: 'عادی', code: 'Medium' },
{ name: 'اورژانسی', code: 'High' },
]
export const modalityTitle = (code) => {
return modalities.find((modality) => modality.code === code)?.title || code
}
export const priorityTitle = (code) => {
return priorities.find((priority) => priority.code === code)?.name || code
}
+94 -12
View File
@@ -1,22 +1,104 @@
import { step } from '@primeuix/themes/aura/stepper'
import { defineStore } from 'pinia'
const emptyCase = () => ({
petId: null,
name: null,
type: null,
gender: null,
breed: null,
age: null,
weight: null,
sterilized: null,
})
export const useFastRequestStore = defineStore('FastRequestStore', {
state: () => ({
endpoints: {
create: '/dashboard/fast-requests/create',
store: '/dashboard/fast-requests',
},
step: 1,
requestType: null,
requestCase: {
name: null,
species: null,
gender: null,
age: null,
weight: null,
neutered: null,
requestCase: emptyCase(),
requestDetails: {
priority: null,
note: null,
},
requestDetails: null,
requestDoctor: null,
requestReview: null,
historyFiles: [],
scanFiles: [],
doctorId: null,
pets: [],
doctors: [],
submitting: false,
submittedDocument: null,
}),
getters: {
selectedPet: (state) => state.pets.find((pet) => pet.id === state.requestCase.petId) || null,
selectedDoctor: (state) => state.doctors.find((doctor) => doctor.id === state.doctorId) || null,
canProceed: (state) => {
switch (state.step) {
case 1:
return !!state.requestType
case 2:
return !!state.requestCase.petId || (!!state.requestCase.name && !!state.requestCase.type)
case 3:
return !!state.requestDetails.priority
default:
return true
}
},
},
actions: {
async fetchCreateData() {
const { data } = await window.axios.get(this.endpoints.create)
this.pets = data.pets
this.doctors = data.doctors
},
selectArchivePet(petId) {
this.requestCase = emptyCase()
this.requestCase.petId = petId
},
clearArchivePet() {
this.requestCase.petId = null
},
nextStep() {
if (this.step < 5) this.step++
},
prevStep() {
if (this.step > 1) this.step--
},
async submit() {
this.submitting = true
try {
const formData = new FormData()
formData.append('modality', this.requestType)
formData.append('priority', this.requestDetails.priority)
if (this.requestDetails.note) formData.append('note', this.requestDetails.note)
}
if (this.requestCase.petId) {
formData.append('pet_id', this.requestCase.petId)
} else {
Object.entries(this.requestCase).forEach(([key, value]) => {
if (key !== 'petId' && value !== null && value !== '') {
formData.append(`pet[${key}]`, key === 'sterilized' ? (value ? 1 : 0) : value)
}
})
}
if (this.doctorId) formData.append('doctor_id', this.doctorId)
this.historyFiles.forEach((file) => formData.append('history_files[]', file))
this.scanFiles.forEach((file) => formData.append('scan_files[]', file))
const { data } = await window.axios.post(this.endpoints.store, formData)
this.submittedDocument = data.document
return data.document
} finally {
this.submitting = false
}
},
reset() {
this.$reset()
},
},
})
+13
View File
@@ -0,0 +1,13 @@
const sizes = ['B', 'KB', 'MB', 'GB']
export const formatFileSize = (bytes) => {
if (!bytes) {
return `0 ${sizes[0]}`
}
const k = 1024
const i = Math.floor(Math.log(bytes) / Math.log(k))
const formattedSize = parseFloat((bytes / Math.pow(k, i)).toFixed(1))
return `${formattedSize} ${sizes[i]}`
}
+4
View File
@@ -3,6 +3,7 @@
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Controllers\Client\AuthController;
use App\Http\Controllers\Client\FastRequestController;
use App\Http\Controllers\Client\Doctor\DashboardController;
use App\Http\Controllers\Client\Doctor\CaseController;
use App\Http\Controllers\Client\Doctor\DocumentController;
@@ -38,6 +39,9 @@ Route::group(['prefix' => 'auth','middleware' => 'guest'] , function (){
Route::group(['prefix' => 'dashboard' , 'middleware' => 'auth' , 'as' => 'dashboard.'] , function (){
Route::get('/', [DashboardController::class, 'index'])->name('index');
Route::get('fast-requests/create', [FastRequestController::class, 'create'])->name('fastRequests.create');
Route::post('fast-requests', [FastRequestController::class, 'store'])->name('fastRequests.store');
Route::resource('/documents', DocumentController::class);
Route::resource('cases', CaseController::class);