DONE: membership application to generate letter along with member_number and attach to email, change to dropdown for company or job selection, redesign profile ui, change to incremental ids for jobs; WIP: member digital card
This commit is contained in:
@@ -16,5 +16,16 @@
|
|||||||
[ ] jana surat lepas lulus anggota
|
[ ] jana surat lepas lulus anggota
|
||||||
|
|
||||||
## Present to Boss (2/7/2026)
|
## Present to Boss (2/7/2026)
|
||||||
[ ] discuss logo baru MyKOPKB
|
[x] discuss logo baru MyKOPKB
|
||||||
[ ] layout footer untuk surat rasmi
|
[x] layout footer untuk surat rasmi
|
||||||
|
[ ] maklumat pekerjaan kena buat dropdown untuk nama majikan
|
||||||
|
[ ] tambah minima untuk borang permohonan (tukar jadi caruman saham potongan bulanan)
|
||||||
|
[x] tukar waris ke penama
|
||||||
|
[x] ic dan surat pengesahan majikkan perlu mandatori
|
||||||
|
[ ] semakan lembaga tukar jadi maklumat keputusan
|
||||||
|
[x] boleh attach document untuk peringkat pentadbir
|
||||||
|
[x] surat attach dekat email selepas bos
|
||||||
|
[ ] pembayaran pertama tukar jadi one-off
|
||||||
|
[ ] syer maksima silap tukar jadi rm50.00
|
||||||
|
[ ] letak sign digital
|
||||||
|
[x] running no anggota dalam surat dan masa.
|
||||||
+26
-9
@@ -2,19 +2,28 @@
|
|||||||
|
|
||||||
namespace Modules\MembershipApplication\Emails;
|
namespace Modules\MembershipApplication\Emails;
|
||||||
|
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Notifications\Concerns\BuildsMailMessage;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Notifications\Messages\MailMessage;
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||||
|
|
||||||
class MembershipApplicationFailedNotification extends Notification
|
class MembershipApplicationFailedNotification extends Notification implements ShouldQueue
|
||||||
{
|
{
|
||||||
|
use BuildsMailMessage;
|
||||||
use Queueable;
|
use Queueable;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected MembershipApplication $application
|
public MembershipApplication $application,
|
||||||
|
public Document $resultLetter,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
public function via(object $notifiable): array
|
public function via(object $notifiable): array
|
||||||
{
|
{
|
||||||
return ['mail'];
|
return ['mail'];
|
||||||
@@ -23,13 +32,21 @@ class MembershipApplicationFailedNotification extends Notification
|
|||||||
public function toMail(object $notifiable): MailMessage
|
public function toMail(object $notifiable): MailMessage
|
||||||
{
|
{
|
||||||
$this->application->loadMissing('applicant');
|
$this->application->loadMissing('applicant');
|
||||||
|
$applicant = $this->application->applicant;
|
||||||
|
|
||||||
return (new MailMessage)
|
return $this->mailMessage(
|
||||||
->subject('Keputusan Permohonan Keahlian')
|
subject: 'Keputusan Permohonan Keahlian KOPKB - TIDAK LULUS',
|
||||||
->markdown('membershipapplication::emails.failed', [
|
view: 'membershipapplication::emails.failed',
|
||||||
'name' => $this->application->applicant->name,
|
data: [
|
||||||
'applicationNumber' => $this->application->application_number,
|
'application' => $this->application,
|
||||||
'logoPath' => public_path('images/logo-kopkb.svg'),
|
'applicant' => $applicant,
|
||||||
]);
|
],
|
||||||
|
)->attach(
|
||||||
|
Storage::disk(Document::STORAGE_DISK)->path($this->resultLetter->path),
|
||||||
|
[
|
||||||
|
'as' => $this->resultLetter->name,
|
||||||
|
'mime' => $this->resultLetter->mime_type,
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-9
@@ -2,19 +2,28 @@
|
|||||||
|
|
||||||
namespace Modules\MembershipApplication\Emails;
|
namespace Modules\MembershipApplication\Emails;
|
||||||
|
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Notifications\Concerns\BuildsMailMessage;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Notifications\Messages\MailMessage;
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||||
|
|
||||||
class MembershipApplicationPassedNotification extends Notification
|
class MembershipApplicationPassedNotification extends Notification implements ShouldQueue
|
||||||
{
|
{
|
||||||
|
use BuildsMailMessage;
|
||||||
use Queueable;
|
use Queueable;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected MembershipApplication $application
|
public MembershipApplication $application,
|
||||||
|
public Document $resultLetter,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
public function via(object $notifiable): array
|
public function via(object $notifiable): array
|
||||||
{
|
{
|
||||||
return ['mail'];
|
return ['mail'];
|
||||||
@@ -23,13 +32,21 @@ class MembershipApplicationPassedNotification extends Notification
|
|||||||
public function toMail(object $notifiable): MailMessage
|
public function toMail(object $notifiable): MailMessage
|
||||||
{
|
{
|
||||||
$this->application->loadMissing('applicant');
|
$this->application->loadMissing('applicant');
|
||||||
|
$applicant = $this->application->applicant;
|
||||||
|
|
||||||
return (new MailMessage)
|
return $this->mailMessage(
|
||||||
->subject('Permohonan Keahlian Diluluskan')
|
subject: 'Keputusan Permohonan Keahlian KOPKB - LULUS',
|
||||||
->markdown('membershipapplication::emails.passed', [
|
view: 'membershipapplication::emails.passed',
|
||||||
'name' => $this->application->applicant->name,
|
data: [
|
||||||
'applicationNumber' => $this->application->application_number,
|
'application' => $this->application,
|
||||||
'logoPath' => public_path('images/logo-kopkb.svg'),
|
'applicant' => $applicant,
|
||||||
]);
|
],
|
||||||
|
)->attach(
|
||||||
|
Storage::disk(Document::STORAGE_DISK)->path($this->resultLetter->path),
|
||||||
|
[
|
||||||
|
'as' => $this->resultLetter->name,
|
||||||
|
'mime' => $this->resultLetter->mime_type,
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ class MembershipApplication extends Model
|
|||||||
|
|
||||||
public const RESULT_LETTER_DOCUMENT_TYPE = 'result_letter';
|
public const RESULT_LETTER_DOCUMENT_TYPE = 'result_letter';
|
||||||
|
|
||||||
|
public const ADMIN_ATTACHMENT_DOCUMENT_TYPE = 'admin_attachment';
|
||||||
|
|
||||||
public const MINIMUM_SHARE_CAPITAL = 500;
|
public const MINIMUM_SHARE_CAPITAL = 500;
|
||||||
|
|
||||||
public const SHARE_INSTALLMENT_MONTHS = 6;
|
public const SHARE_INSTALLMENT_MONTHS = 6;
|
||||||
|
|||||||
+24
-5
@@ -17,6 +17,7 @@ use Modules\MembershipApplication\Enums\ManagementDecision;
|
|||||||
use Modules\MembershipApplication\Http\Requests\AssignReferencesRequest;
|
use Modules\MembershipApplication\Http\Requests\AssignReferencesRequest;
|
||||||
use Modules\MembershipApplication\Http\Requests\BatchCompleteRequest;
|
use Modules\MembershipApplication\Http\Requests\BatchCompleteRequest;
|
||||||
use Modules\MembershipApplication\Http\Requests\BoardReviewRequest;
|
use Modules\MembershipApplication\Http\Requests\BoardReviewRequest;
|
||||||
|
use Modules\MembershipApplication\Http\Requests\CompleteMembershipApplicationRequest;
|
||||||
use Modules\MembershipApplication\Http\Requests\GenerateResultLetterRequest;
|
use Modules\MembershipApplication\Http\Requests\GenerateResultLetterRequest;
|
||||||
use Modules\MembershipApplication\Http\Requests\ManagementReviewRequest;
|
use Modules\MembershipApplication\Http\Requests\ManagementReviewRequest;
|
||||||
use Modules\MembershipApplication\Http\Requests\UpdateMembershipApplicationRequest;
|
use Modules\MembershipApplication\Http\Requests\UpdateMembershipApplicationRequest;
|
||||||
@@ -180,18 +181,19 @@ class MembershipApplicationController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function complete(Request $request, MembershipApplication $membershipApplication): JsonResponse
|
public function complete(CompleteMembershipApplicationRequest $request, MembershipApplication $membershipApplication): JsonResponse
|
||||||
{
|
{
|
||||||
$this->authorize('complete', $membershipApplication);
|
$this->authorize('complete', $membershipApplication);
|
||||||
|
|
||||||
$application = $this->membershipApplicationService->complete(
|
$application = $this->membershipApplicationService->complete(
|
||||||
$membershipApplication,
|
$membershipApplication,
|
||||||
$request->user()
|
$request->user(),
|
||||||
|
$request->validated('board_meeting_reference'),
|
||||||
);
|
);
|
||||||
|
|
||||||
$message = $application->board_result === BoardResult::Pass->value
|
$message = $application->board_result === BoardResult::Pass->value
|
||||||
? 'Permohonan selesai. Akaun ahli telah dicipta dan e-mel dihantar.'
|
? 'Permohonan selesai. Surat keputusan dijana, akaun ahli dicipta dan e-mel dihantar.'
|
||||||
: 'Permohonan selesai. E-mel keputusan dihantar.';
|
: 'Permohonan selesai. Surat keputusan dijana dan e-mel dihantar.';
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@@ -206,7 +208,8 @@ class MembershipApplicationController extends Controller
|
|||||||
|
|
||||||
$result = $this->membershipApplicationService->completeBatch(
|
$result = $this->membershipApplicationService->completeBatch(
|
||||||
$request->validated('application_ids'),
|
$request->validated('application_ids'),
|
||||||
$request->user()
|
$request->user(),
|
||||||
|
$request->validated('board_meeting_reference'),
|
||||||
);
|
);
|
||||||
|
|
||||||
$succeededCount = count($result['succeeded']);
|
$succeededCount = count($result['succeeded']);
|
||||||
@@ -245,6 +248,22 @@ class MembershipApplicationController extends Controller
|
|||||||
return $this->documentService->downloadDocument($document->id);
|
return $this->documentService->downloadDocument($document->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteDocument(MembershipApplication $membershipApplication, string $documentId): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', $membershipApplication);
|
||||||
|
|
||||||
|
$application = $this->membershipApplicationService->deleteDocument(
|
||||||
|
$membershipApplication,
|
||||||
|
$documentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Lampiran pentadbir berjaya dipadam.',
|
||||||
|
'data' => new MembershipApplicationResource($application),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function generateResultLetter(GenerateResultLetterRequest $request, MembershipApplication $membershipApplication,): JsonResponse {
|
public function generateResultLetter(GenerateResultLetterRequest $request, MembershipApplication $membershipApplication,): JsonResponse {
|
||||||
$this->authorize('generateResultLetter', $membershipApplication);
|
$this->authorize('generateResultLetter', $membershipApplication);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class BatchCompleteRequest extends FormRequest
|
|||||||
return [
|
return [
|
||||||
'application_ids' => 'required|array|min:1|max:100',
|
'application_ids' => 'required|array|min:1|max:100',
|
||||||
'application_ids.*' => 'required|uuid|distinct|exists:membership_applications,id',
|
'application_ids.*' => 'required|uuid|distinct|exists:membership_applications,id',
|
||||||
|
'board_meeting_reference' => 'required|string|max:255',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ class BatchCompleteRequest extends FormRequest
|
|||||||
'application_ids.max' => 'Maksimum 100 permohonan setiap batch.',
|
'application_ids.max' => 'Maksimum 100 permohonan setiap batch.',
|
||||||
'application_ids.*.exists' => 'Salah satu permohonan tidak dijumpai.',
|
'application_ids.*.exists' => 'Salah satu permohonan tidak dijumpai.',
|
||||||
'application_ids.*.distinct' => 'Permohonan duplikat tidak dibenarkan.',
|
'application_ids.*.distinct' => 'Permohonan duplikat tidak dibenarkan.',
|
||||||
|
'board_meeting_reference.required' => 'Rujukan mesyuarat lembaga diperlukan.',
|
||||||
|
'board_meeting_reference.max' => 'Rujukan mesyuarat lembaga tidak boleh melebihi 255 aksara.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\MembershipApplication\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class CompleteMembershipApplicationRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'board_meeting_reference' => 'required|string|max:255',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'board_meeting_reference.required' => 'Rujukan mesyuarat lembaga diperlukan.',
|
||||||
|
'board_meeting_reference.max' => 'Rujukan mesyuarat lembaga tidak boleh melebihi 255 aksara.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -48,7 +48,7 @@ class StoreMembershipApplicationRequest extends FormRequest
|
|||||||
'documents.ic_copy' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240',
|
'documents.ic_copy' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240',
|
||||||
'documents.photo' => 'nullable|file|mimes:jpg,jpeg,png|max:10240',
|
'documents.photo' => 'nullable|file|mimes:jpg,jpeg,png|max:10240',
|
||||||
'documents.salary_slip' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:10240',
|
'documents.salary_slip' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:10240',
|
||||||
'documents.employer_letter' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:10240',
|
'documents.employer_letter' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +89,8 @@ class StoreMembershipApplicationRequest extends FormRequest
|
|||||||
|
|
||||||
'documents.ic_copy.required' => 'Salinan kad pengenalan diperlukan.',
|
'documents.ic_copy.required' => 'Salinan kad pengenalan diperlukan.',
|
||||||
'documents.ic_copy.mimes' => 'Salinan kad pengenalan mestilah PDF, JPG, JPEG atau PNG.',
|
'documents.ic_copy.mimes' => 'Salinan kad pengenalan mestilah PDF, JPG, JPEG atau PNG.',
|
||||||
|
'documents.employer_letter.required' => 'Surat pengesahan majikan diperlukan.',
|
||||||
|
'documents.employer_letter.mimes' => 'Surat pengesahan majikan mestilah PDF, JPG, JPEG atau PNG.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -4,6 +4,7 @@ namespace Modules\MembershipApplication\Http\Requests;
|
|||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||||
|
|
||||||
class UploadMembershipApplicationDocumentRequest extends FormRequest
|
class UploadMembershipApplicationDocumentRequest extends FormRequest
|
||||||
{
|
{
|
||||||
@@ -18,7 +19,13 @@ class UploadMembershipApplicationDocumentRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'type' => ['required', Rule::in(['ic_copy', 'photo', 'salary_slip', 'employer_letter'])],
|
'type' => ['required', Rule::in([
|
||||||
|
'ic_copy',
|
||||||
|
'photo',
|
||||||
|
'salary_slip',
|
||||||
|
'employer_letter',
|
||||||
|
MembershipApplication::ADMIN_ATTACHMENT_DOCUMENT_TYPE,
|
||||||
|
])],
|
||||||
'file' => [
|
'file' => [
|
||||||
'required',
|
'required',
|
||||||
'file',
|
'file',
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class MembershipApplicationRepository implements MembershipApplicationRepository
|
|||||||
return MembershipApplication::with([
|
return MembershipApplication::with([
|
||||||
'applicant',
|
'applicant',
|
||||||
'heirs',
|
'heirs',
|
||||||
|
'user:id,member_number',
|
||||||
'references.member:id,name,ic_number',
|
'references.member:id,name,ic_number',
|
||||||
'documents',
|
'documents',
|
||||||
'reviews.reviewer:id,name',
|
'reviews.reviewer:id,name',
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
@component('mail::message')
|
@component('mail::message')
|
||||||
@if (! empty($logoPath) && file_exists($logoPath))
|
@include('emails.partials.header')
|
||||||
<div style="text-align: center; margin-bottom: 24px;">
|
|
||||||
<img src="{{ $message->embed($logoPath) }}" alt="{{ config('app.name') }}" style="max-height: 80px; width: auto;">
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
# Keputusan Permohonan Keahlian
|
# Keputusan Permohonan Keahlian
|
||||||
|
|
||||||
Assalamualaikum **{{ $name }}**,
|
Assalamualaikum **{{ $applicant->name }}**,
|
||||||
|
|
||||||
Permohonan keahlian anda (**{{ $applicationNumber }}**) **tidak diluluskan** pada peringkat lembaga.
|
Permohonan keahlian anda (**{{ $application->application_number }}**) **tidak diluluskan** pada peringkat lembaga.
|
||||||
|
|
||||||
|
Surat keputusan rasmi dilampirkan dalam e-mel ini.
|
||||||
|
|
||||||
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
|
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
|
||||||
|
|
||||||
Terima kasih,<br>
|
@include('emails.partials.footer')
|
||||||
{{ config('app.name') }}
|
|
||||||
@endcomponent
|
@endcomponent
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
@component('mail::message')
|
@component('mail::message')
|
||||||
@if (! empty($logoPath) && file_exists($logoPath))
|
@include('emails.partials.header')
|
||||||
<div style="text-align: center; margin-bottom: 24px;">
|
|
||||||
<img src="{{ $message->embed($logoPath) }}" alt="{{ config('app.name') }}" style="max-height: 80px; width: auto;">
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
# Keputusan Permohonan Keahlian
|
# Keputusan Permohonan Keahlian
|
||||||
|
|
||||||
Assalamualaikum **{{ $name }}**,
|
Assalamualaikum **{{ $applicant->name }}**,
|
||||||
|
|
||||||
Permohonan keahlian anda (**{{ $applicationNumber }}**) **diluluskan** pada peringkat lembaga.
|
Permohonan keahlian anda (**{{ $application->application_number }}**) **diluluskan** pada peringkat lembaga.
|
||||||
|
|
||||||
|
Surat keputusan rasmi dilampirkan dalam e-mel ini.
|
||||||
|
|
||||||
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
|
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
|
||||||
|
|
||||||
Terima kasih,<br>
|
@include('emails.partials.footer')
|
||||||
{{ config('app.name') }}
|
|
||||||
@endcomponent
|
@endcomponent
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p style="line-height: 1;">
|
<p style="line-height: 1;">
|
||||||
2. {{ $openingTone }} dimaklumkan bahawa, permohonan tuan untuk menjadi anggota Koperasi
|
2. {{ $openingTone }} dimaklumkan bahawa, permohonan tuan untuk menjadi anggota Koperasi
|
||||||
Permodalan Kelantan Berhad (KoPKB) telah <strong>{{ $boardResultLabel }}</strong> dalam {{ $boardMeetingReference }}.
|
Permodalan Kelantan Berhad (KoPKB) telah <strong>{{ $boardResultLabel }}</strong> dalam {{ $boardMeetingReference }}@if ($isPassed && $memberNumber). Nombor keanggotaan tuan adalah <strong>{{ $memberNumber }}</strong>@endif.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if ($isPassed)
|
@if ($isPassed)
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<div class="signature-header">Dengan hormatnya,</div>
|
<div class="signature-header">Dengan hormatnya,</div>
|
||||||
<div class="signature-company">KOPERASI PERMODALAN KELANTAN BERHAD</div>
|
<div class="signature-company">KOPERASI PERMODALAN KELANTAN BERHAD</div>
|
||||||
<div class="signature-space"></div>
|
<div class="signature-space"></div>
|
||||||
<div class="signatory-name">MUHAMMAD NAFIS BIN ZAINUDDIN</div>
|
<div class="signatory-name">MUHAMMAD NAFIS BIN ZAINUDIN</div>
|
||||||
<div class="signatory-title">Pengurus Besar</div>
|
<div class="signatory-title">Pengurus Besar</div>
|
||||||
<div class="signatory-title">b/p Setiausaha</div>
|
<div class="signatory-title">b/p Setiausaha</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(funct
|
|||||||
|
|
||||||
// Membership application upload document routes
|
// Membership application upload document routes
|
||||||
Route::post('membership-applications/{membershipApplication}/documents', [MembershipApplicationController::class, 'uploadDocument'])->name('membership-application.upload-document');
|
Route::post('membership-applications/{membershipApplication}/documents', [MembershipApplicationController::class, 'uploadDocument'])->name('membership-application.upload-document');
|
||||||
|
Route::delete('membership-applications/{membershipApplication}/documents/{documentId}', [MembershipApplicationController::class, 'deleteDocument'])->name('membership-application.delete-document');
|
||||||
// download document routes
|
// download document routes
|
||||||
Route::get('membership-applications/{membershipApplication}/documents/{documentId}/download', [MembershipApplicationController::class, 'downloadDocument'])->name('membership-application.download-document');
|
Route::get('membership-applications/{membershipApplication}/documents/{documentId}/download', [MembershipApplicationController::class, 'downloadDocument'])->name('membership-application.download-document');
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\MembershipApplication\Services;
|
namespace Modules\MembershipApplication\Services;
|
||||||
|
|
||||||
|
use App\Models\Document;
|
||||||
use App\Services\DocumentService;
|
use App\Services\DocumentService;
|
||||||
use App\Services\LetterService;
|
use App\Services\LetterService;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
@@ -137,15 +138,38 @@ class MembershipApplicationService
|
|||||||
|
|
||||||
$this->documentService->validateFile($file);
|
$this->documentService->validateFile($file);
|
||||||
|
|
||||||
$application->documentsOfType($type)->get()->each(
|
if ($type !== MembershipApplication::ADMIN_ATTACHMENT_DOCUMENT_TYPE) {
|
||||||
fn ($document) => $this->documentService->deleteDocument($document->id)
|
$application->documentsOfType($type)->get()->each(
|
||||||
);
|
fn ($document) => $this->documentService->deleteDocument($document->id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$this->documentService->uploadDocument($application, $file, $type);
|
$this->documentService->uploadDocument($application, $file, $type);
|
||||||
|
|
||||||
return $this->repository->findByIdWithRelations($application->id);
|
return $this->repository->findByIdWithRelations($application->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteDocument(MembershipApplication $application, string $documentId): MembershipApplication
|
||||||
|
{
|
||||||
|
if ($application->status === ApplicationStatus::Completed) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'status' => ['Permohonan yang telah selesai tidak boleh dikemaskini.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = $application->documents()->findOrFail($documentId);
|
||||||
|
|
||||||
|
if ($document->type !== MembershipApplication::ADMIN_ATTACHMENT_DOCUMENT_TYPE) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'document' => ['Hanya lampiran pentadbir boleh dipadam.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->documentService->deleteDocument($document->id);
|
||||||
|
|
||||||
|
return $this->repository->findByIdWithRelations($application->id);
|
||||||
|
}
|
||||||
|
|
||||||
public function managementReview(MembershipApplication $application, ManagementDecision $decision, ?string $remarks, User $reviewer): MembershipApplication {
|
public function managementReview(MembershipApplication $application, ManagementDecision $decision, ?string $remarks, User $reviewer): MembershipApplication {
|
||||||
if ($application->status !== ApplicationStatus::Submitted) {
|
if ($application->status !== ApplicationStatus::Submitted) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
@@ -230,8 +254,11 @@ class MembershipApplicationService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function complete(MembershipApplication $application, User $processedBy): MembershipApplication
|
public function complete(
|
||||||
{
|
MembershipApplication $application,
|
||||||
|
User $processedBy,
|
||||||
|
string $boardMeetingReference,
|
||||||
|
): MembershipApplication {
|
||||||
if ($application->status !== ApplicationStatus::PendingNotification) {
|
if ($application->status !== ApplicationStatus::PendingNotification) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'status' => ['Permohonan ini tidak boleh diselesaikan pada masa ini.'],
|
'status' => ['Permohonan ini tidak boleh diselesaikan pada masa ini.'],
|
||||||
@@ -244,13 +271,16 @@ class MembershipApplicationService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DB::transaction(function () use ($application) {
|
return DB::transaction(function () use ($application, $boardMeetingReference) {
|
||||||
if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) {
|
if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) {
|
||||||
$user = $this->createMemberFromApplication($application);
|
$user = $this->createMemberFromApplication($application);
|
||||||
$application->update(['user_id' => $user->id]);
|
$application->update(['user_id' => $user->id]);
|
||||||
|
$application->refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->sendResultNotification($application);
|
$resultLetter = $this->storeResultLetter($application, $boardMeetingReference);
|
||||||
|
|
||||||
|
$this->sendResultNotification($application, $resultLetter);
|
||||||
|
|
||||||
$application->update([
|
$application->update([
|
||||||
'status' => ApplicationStatus::Completed,
|
'status' => ApplicationStatus::Completed,
|
||||||
@@ -268,7 +298,7 @@ class MembershipApplicationService
|
|||||||
* failed: list<array{id: string, application_number: string|null, message: string}>
|
* failed: list<array{id: string, application_number: string|null, message: string}>
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public function completeBatch(array $applicationIds, User $processedBy): array
|
public function completeBatch(array $applicationIds, User $processedBy, string $boardMeetingReference): array
|
||||||
{
|
{
|
||||||
$succeeded = [];
|
$succeeded = [];
|
||||||
$failed = [];
|
$failed = [];
|
||||||
@@ -287,7 +317,7 @@ class MembershipApplicationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$succeeded[] = $this->complete($application, $processedBy);
|
$succeeded[] = $this->complete($application, $processedBy, $boardMeetingReference);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
$failed[] = [
|
$failed[] = [
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
@@ -317,40 +347,47 @@ class MembershipApplicationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
return DB::transaction(function () use ($application, $boardMeetingReference) {
|
return DB::transaction(function () use ($application, $boardMeetingReference) {
|
||||||
$lockedApplication = MembershipApplication::query()
|
$this->storeResultLetter($application, $boardMeetingReference);
|
||||||
->whereKey($application->id)
|
|
||||||
->lockForUpdate()
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
if ($lockedApplication->hasDocumentsOfType(MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE)) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'result_letter' => ['Surat keputusan telah dijana dan tidak boleh dijana semula.'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$application = $this->repository->findByIdWithRelations($lockedApplication->id);
|
|
||||||
|
|
||||||
if (! $application || ! $application->applicant) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'application' => ['Maklumat pemohon tidak lengkap.'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$viewData = $this->buildResultLetterViewData($application, $boardMeetingReference);
|
|
||||||
$pdf = $this->letterService->renderPdf('membershipapplication::pdf.approved-letter', $viewData);
|
|
||||||
$fileName = 'surat-keputusan-'.$application->application_number.'.pdf';
|
|
||||||
|
|
||||||
$this->documentService->storeGeneratedDocument(
|
|
||||||
$application,
|
|
||||||
$pdf,
|
|
||||||
$fileName,
|
|
||||||
MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE,
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->repository->findByIdWithRelations($application->id);
|
return $this->repository->findByIdWithRelations($application->id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function storeResultLetter(
|
||||||
|
MembershipApplication $application,
|
||||||
|
string $boardMeetingReference,
|
||||||
|
): Document {
|
||||||
|
$lockedApplication = MembershipApplication::query()
|
||||||
|
->whereKey($application->id)
|
||||||
|
->lockForUpdate()
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
if ($lockedApplication->hasDocumentsOfType(MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'result_letter' => ['Surat keputusan telah dijana dan tidak boleh dijana semula.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$application = $this->repository->findByIdWithRelations($lockedApplication->id);
|
||||||
|
|
||||||
|
if (! $application || ! $application->applicant) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'application' => ['Maklumat pemohon tidak lengkap.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$viewData = $this->buildResultLetterViewData($application, $boardMeetingReference);
|
||||||
|
$pdf = $this->letterService->renderPdf('membershipapplication::pdf.approved-letter', $viewData);
|
||||||
|
$fileName = 'surat-keputusan-'.$application->application_number.'.pdf';
|
||||||
|
|
||||||
|
return $this->documentService->storeGeneratedDocument(
|
||||||
|
$application,
|
||||||
|
$pdf,
|
||||||
|
$fileName,
|
||||||
|
MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
@@ -378,12 +415,13 @@ class MembershipApplicationService
|
|||||||
): array {
|
): array {
|
||||||
$applicant = $application->applicant;
|
$applicant = $application->applicant;
|
||||||
$isPassed = $application->board_result === BoardResult::Pass->value;
|
$isPassed = $application->board_result === BoardResult::Pass->value;
|
||||||
$monthlyShareInstallment = number_format(
|
// $monthlyShareInstallment = number_format(
|
||||||
MembershipApplication::MINIMUM_SHARE_CAPITAL / MembershipApplication::SHARE_INSTALLMENT_MONTHS,
|
// MembershipApplication::MINIMUM_SHARE_CAPITAL / MembershipApplication::SHARE_INSTALLMENT_MONTHS,
|
||||||
2,
|
// 2,
|
||||||
'.',
|
// '.',
|
||||||
'',
|
// '',
|
||||||
);
|
// );
|
||||||
|
$monthlyShareInstallment = 84;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'application' => $application,
|
'application' => $application,
|
||||||
@@ -400,6 +438,7 @@ class MembershipApplicationService
|
|||||||
'feeMonthlyContribution' => number_format((float) $applicant->fee_monthly_contribution, 2, '.', ''),
|
'feeMonthlyContribution' => number_format((float) $applicant->fee_monthly_contribution, 2, '.', ''),
|
||||||
'shareInstallmentMonths' => MembershipApplication::SHARE_INSTALLMENT_MONTHS,
|
'shareInstallmentMonths' => MembershipApplication::SHARE_INSTALLMENT_MONTHS,
|
||||||
'minimumShareCapital' => number_format(MembershipApplication::MINIMUM_SHARE_CAPITAL, 2, '.', ''),
|
'minimumShareCapital' => number_format(MembershipApplication::MINIMUM_SHARE_CAPITAL, 2, '.', ''),
|
||||||
|
'memberNumber' => $isPassed ? $application->user?->member_number : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,20 +527,30 @@ class MembershipApplicationService
|
|||||||
->notify(new MembershipApplicationSubmittedNotification($application));
|
->notify(new MembershipApplicationSubmittedNotification($application));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function sendResultNotification(MembershipApplication $application): void
|
protected function sendResultNotification(MembershipApplication $application, Document $resultLetter): void
|
||||||
{
|
{
|
||||||
$application->loadMissing('applicant');
|
$application->loadMissing('applicant');
|
||||||
|
|
||||||
$notification = $application->board_result === BoardResult::Pass->value
|
$notification = $application->board_result === BoardResult::Pass->value
|
||||||
? new MembershipApplicationPassedNotification($application)
|
? new MembershipApplicationPassedNotification($application, $resultLetter)
|
||||||
: new MembershipApplicationFailedNotification($application);
|
: new MembershipApplicationFailedNotification($application, $resultLetter);
|
||||||
|
|
||||||
Notification::route('mail', $application->applicant->email)->notify($notification);
|
Notification::route('mail', $application->applicant->email)->notify($notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function generateMemberNumber(): int
|
protected function generateMemberNumber(): int
|
||||||
{
|
{
|
||||||
return ((int) User::max('member_number')) + 1;
|
$latestUser = User::query()
|
||||||
|
->whereNotNull('member_number')
|
||||||
|
->orderByDesc('member_number')
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($latestUser === null && DB::getDriverName() === 'pgsql') {
|
||||||
|
DB::selectOne('SELECT pg_advisory_xact_lock(?) AS locked', [742891035]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((int) ($latestUser?->member_number ?? 0)) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ return new class extends Migration
|
|||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('jobs', function (Blueprint $table) {
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
$table->uuid('id')->primary();
|
$table->id();
|
||||||
$table->string('queue')->index();
|
$table->string('queue')->index();
|
||||||
$table->longText('payload');
|
$table->longText('payload');
|
||||||
$table->unsignedTinyInteger('attempts');
|
$table->unsignedTinyInteger('attempts');
|
||||||
@@ -22,7 +22,7 @@ return new class extends Migration
|
|||||||
});
|
});
|
||||||
|
|
||||||
Schema::create('job_batches', function (Blueprint $table) {
|
Schema::create('job_batches', function (Blueprint $table) {
|
||||||
$table->uuid('id')->primary();
|
$table->string('id')->primary();
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
$table->integer('total_jobs');
|
$table->integer('total_jobs');
|
||||||
$table->integer('pending_jobs');
|
$table->integer('pending_jobs');
|
||||||
@@ -35,7 +35,7 @@ return new class extends Migration
|
|||||||
});
|
});
|
||||||
|
|
||||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||||
$table->uuid('id')->primary();
|
$table->id();
|
||||||
$table->string('uuid')->unique();
|
$table->string('uuid')->unique();
|
||||||
$table->text('connection');
|
$table->text('connection');
|
||||||
$table->text('queue');
|
$table->text('queue');
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 275 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 275 KiB |
@@ -1,129 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import {
|
|
||||||
AccordionRoot,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
AccordionContent,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import {
|
|
||||||
Preview,
|
|
||||||
SectionTitle,
|
|
||||||
SectionContent,
|
|
||||||
InstallPackage,
|
|
||||||
PreviewCode,
|
|
||||||
} from "@/components/docs";
|
|
||||||
import { Box } from "@/components/ui/box";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="preview" class="-mt-20">
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Box raised="single" class="w-full">
|
|
||||||
<AccordionRoot class="w-full" :default-value="['product-information']">
|
|
||||||
<AccordionItem value="product-information">
|
|
||||||
<AccordionTrigger>Product Information</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<p class="mb-2">
|
|
||||||
Our flagship product combines cutting-edge technology with
|
|
||||||
sleek design. Built with premium materials, it offers
|
|
||||||
unparalleled performance and reliability.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Key features include advanced processing capabilities, and an
|
|
||||||
intuitive user interface designed for both beginners and
|
|
||||||
experts.
|
|
||||||
</p>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem value="shipping-details">
|
|
||||||
<AccordionTrigger>Shipping Details</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<p class="mb-2">
|
|
||||||
We offer worldwide shipping through trusted courier partners.
|
|
||||||
Standard delivery takes 3-5 business days, while express
|
|
||||||
shipping ensures delivery within 1-2 business days.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
All orders are carefully packaged and fully insured. Track
|
|
||||||
your shipment in real-time through our dedicated tracking
|
|
||||||
portal.
|
|
||||||
</p>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem value="return-policy">
|
|
||||||
<AccordionTrigger>Return Policy</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<p class="mb-2">
|
|
||||||
We stand behind our products with a comprehensive 30-day
|
|
||||||
return policy. If you're not completely satisfied, simply
|
|
||||||
return the item in its original condition.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Our hassle-free return process includes free return shipping
|
|
||||||
and full refunds processed within 48 hours of receiving the
|
|
||||||
returned item.
|
|
||||||
</p>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</AccordionRoot>
|
|
||||||
</Box>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Box raised="single" class="w-full">
|
|
||||||
<AccordionRoot class="w-full" :default-value="['product-information']">
|
|
||||||
<AccordionItem value="product-information">
|
|
||||||
<AccordionTrigger>Product Information</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<p class="mb-2">
|
|
||||||
Our flagship product combines cutting-edge technology with
|
|
||||||
sleek design. Built with premium materials, it offers
|
|
||||||
unparalleled performance and reliability.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Key features include advanced processing capabilities, and an
|
|
||||||
intuitive user interface designed for both beginners and
|
|
||||||
experts.
|
|
||||||
</p>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem value="shipping-details">
|
|
||||||
<AccordionTrigger>Shipping Details</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<p class="mb-2">
|
|
||||||
We offer worldwide shipping through trusted courier partners.
|
|
||||||
Standard delivery takes 3-5 business days, while express
|
|
||||||
shipping ensures delivery within 1-2 business days.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
All orders are carefully packaged and fully insured. Track
|
|
||||||
your shipment in real-time through our dedicated tracking
|
|
||||||
portal.
|
|
||||||
</p>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem value="return-policy">
|
|
||||||
<AccordionTrigger>Return Policy</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<p class="mb-2">
|
|
||||||
We stand behind our products with a comprehensive 30-day
|
|
||||||
return policy. If you're not completely satisfied, simply
|
|
||||||
return the item in its original condition.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Our hassle-free return process includes free return shipping
|
|
||||||
and full refunds processed within 48 hours of receiving the
|
|
||||||
returned item.
|
|
||||||
</p>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</AccordionRoot>
|
|
||||||
</Box>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import {
|
|
||||||
AvatarRoot,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
} from "@/components/ui/avatar";
|
|
||||||
import {
|
|
||||||
Preview,
|
|
||||||
SectionTitle,
|
|
||||||
SectionContent,
|
|
||||||
InstallPackage,
|
|
||||||
PreviewCode,
|
|
||||||
} from "@/components/docs";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="preview" class="-mt-20">
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<AvatarRoot>
|
|
||||||
<AvatarFallback>PA</AvatarFallback>
|
|
||||||
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
|
|
||||||
</AvatarRoot>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<AvatarRoot>
|
|
||||||
<AvatarFallback>PA</AvatarFallback>
|
|
||||||
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
|
|
||||||
</AvatarRoot>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
<div id="installation">
|
|
||||||
<SectionTitle>Installation</SectionTitle>
|
|
||||||
<SectionContent>Install the following dependencies:</SectionContent>
|
|
||||||
<InstallPackage>add @zag-js/vue @zag-js/avatar</InstallPackage>
|
|
||||||
<SectionContent>
|
|
||||||
Copy and paste the following code into your project.
|
|
||||||
</SectionContent>
|
|
||||||
<PreviewCode title="components/ui/avatar/AvatarFallback.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { Api } from "@zag-js/avatar";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { avatarFallback } from "@mykopkb/core/styles/avatar.styles";
|
|
||||||
import { inject } from "vue";
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
} = defineProps<{
|
|
||||||
asChild?: boolean;
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = inject<Api>("avatarApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot :class="cn(avatarFallback, className)" v-bind="{ ...props, ...$attrs, ...api?.getFallbackProps() }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<span v-else>
|
|
||||||
<slot />
|
|
||||||
</span>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/avatar/AvatarImage.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { avatarImage } from "@mykopkb/core/styles/avatar.styles";
|
|
||||||
import { inject } from "vue";
|
|
||||||
import type { Api } from "@zag-js/avatar";
|
|
||||||
|
|
||||||
const { class: className, ...props } = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = inject<Api>("avatarApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<img :class="cn(avatarImage, className)" v-bind="{ ...props, ...$attrs, ...api?.getImageProps() }" />
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/avatar/AvatarRoot.vue">
|
|
||||||
{{`
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import * as avatar from "@zag-js/avatar";
|
|
||||||
import { provide, computed } from "vue";
|
|
||||||
import { useMachine, normalizeProps } from "@zag-js/vue";
|
|
||||||
import type { Props } from "@zag-js/avatar";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import {
|
|
||||||
avatarRootVariants,
|
|
||||||
type AvatarRootVariants,
|
|
||||||
} from "@mykopkb/core/styles/avatar.styles";
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
bordered,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
} = defineProps<
|
|
||||||
AvatarRootVariants &
|
|
||||||
Partial<Props> & {
|
|
||||||
class?: string;
|
|
||||||
asChild?: boolean;
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
|
|
||||||
const service = useMachine(avatar.machine, {
|
|
||||||
...props,
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const api = computed(() => avatar.connect(service, normalizeProps));
|
|
||||||
|
|
||||||
provide("avatarApi", api);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot :class="
|
|
||||||
cn(
|
|
||||||
avatarRootVariants({
|
|
||||||
bordered,
|
|
||||||
className,
|
|
||||||
}),
|
|
||||||
className
|
|
||||||
)
|
|
||||||
" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<div v-else>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/avatar/index.ts">
|
|
||||||
{{ `
|
|
||||||
export { default as AvatarRoot } from "./AvatarRoot.vue";
|
|
||||||
export { default as AvatarFallback } from "./AvatarFallback.vue";
|
|
||||||
export { default as AvatarImage } from "./AvatarImage.vue";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<SectionContent>
|
|
||||||
Update the import paths to match your project setup.
|
|
||||||
</SectionContent>
|
|
||||||
</div>
|
|
||||||
<div id="usage">
|
|
||||||
<SectionTitle>Usage</SectionTitle>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
import {
|
|
||||||
AvatarRoot,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
} from "@/components/ui/avatar";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<AvatarRoot>
|
|
||||||
<AvatarFallback>PA</AvatarFallback>
|
|
||||||
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
|
|
||||||
</AvatarRoot>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</div>
|
|
||||||
<div id="variants">
|
|
||||||
<SectionTitle>Variants</SectionTitle>
|
|
||||||
<SectionContent>A collection of components you can use.</SectionContent>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<AvatarRoot :bordered="false">
|
|
||||||
<AvatarFallback>PA</AvatarFallback>
|
|
||||||
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
|
|
||||||
</AvatarRoot>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<AvatarRoot :bordered="false">
|
|
||||||
<AvatarFallback>PA</AvatarFallback>
|
|
||||||
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
|
|
||||||
</AvatarRoot>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<AvatarRoot class="rounded-full">
|
|
||||||
<AvatarFallback>PA</AvatarFallback>
|
|
||||||
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
|
|
||||||
</AvatarRoot>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<AvatarRoot class="rounded-full">
|
|
||||||
<AvatarFallback>PA</AvatarFallback>
|
|
||||||
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
|
|
||||||
</AvatarRoot>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<AvatarRoot class="rounded-full" :bordered="false">
|
|
||||||
<AvatarFallback>PA</AvatarFallback>
|
|
||||||
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
|
|
||||||
</AvatarRoot>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<AvatarRoot class="rounded-full" :bordered="false">
|
|
||||||
<AvatarFallback>PA</AvatarFallback>
|
|
||||||
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
|
|
||||||
</AvatarRoot>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { ChevronDown, CheckSquare } from "@lucide/vue";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Preview,
|
|
||||||
SectionTitle,
|
|
||||||
SectionContent,
|
|
||||||
PreviewCode,
|
|
||||||
} from "@/components/docs";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="preview" class="-mt-20">
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Badge variant="primary">12%</Badge>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Badge variant="primary">12%</Badge>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
<div id="installation">
|
|
||||||
<SectionTitle>Installation</SectionTitle>
|
|
||||||
<SectionContent>
|
|
||||||
Copy and paste the following code into your project.
|
|
||||||
</SectionContent>
|
|
||||||
<PreviewCode title="components/ui/badge/Badge.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import {
|
|
||||||
TooltipRoot,
|
|
||||||
TooltipTrigger,
|
|
||||||
TooltipPositioner,
|
|
||||||
TooltipContent,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import {
|
|
||||||
badgeVariants,
|
|
||||||
type BadgeVariants,
|
|
||||||
} from "@mykopkb/core/styles/badge.styles";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
look,
|
|
||||||
variant,
|
|
||||||
content,
|
|
||||||
...props
|
|
||||||
} = defineProps<
|
|
||||||
BadgeVariants & {
|
|
||||||
class?: string;
|
|
||||||
content?: string;
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<TooltipRoot :disabled="!content">
|
|
||||||
<TooltipTrigger as-child>
|
|
||||||
<span :class="cn(badgeVariants({ look, variant, className }))" v-bind="{ ...props, ...$attrs }">
|
|
||||||
<slot />
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPositioner>
|
|
||||||
<TooltipContent>\{\{ content \}\}</TooltipContent>
|
|
||||||
</TooltipPositioner>
|
|
||||||
</TooltipRoot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/badge/index.ts">
|
|
||||||
{{ `
|
|
||||||
export { default as Badge } from "./Badge.vue";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<SectionContent>
|
|
||||||
Update the import paths to match your project setup.
|
|
||||||
</SectionContent>
|
|
||||||
</div>
|
|
||||||
<div id="usage">
|
|
||||||
<SectionTitle>Usage</SectionTitle>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Badge>12%</Badge>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</div>
|
|
||||||
<div id="variants">
|
|
||||||
<SectionTitle>Variants</SectionTitle>
|
|
||||||
<SectionContent>A collection of components you can use.</SectionContent>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Badge variant="primary">12%</Badge>
|
|
||||||
<Badge variant="secondary">12%</Badge>
|
|
||||||
<Badge variant="success">12%</Badge>
|
|
||||||
<Badge variant="danger">12%</Badge>
|
|
||||||
<Badge variant="pending">12%</Badge>
|
|
||||||
<Badge variant="warning">12%</Badge>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Badge variant="primary">12%</Badge>
|
|
||||||
<Badge variant="secondary">12%</Badge>
|
|
||||||
<Badge variant="success">12%</Badge>
|
|
||||||
<Badge variant="danger">12%</Badge>
|
|
||||||
<Badge variant="pending">12%</Badge>
|
|
||||||
<Badge variant="warning">12%</Badge>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Badge variant="primary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="secondary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="success"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="danger"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="pending"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="warning"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Badge variant="primary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="secondary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="success"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="danger"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="pending"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="warning"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Badge look="outline" variant="primary"> 12% </Badge>
|
|
||||||
<Badge look="outline" variant="secondary"> 12% </Badge>
|
|
||||||
<Badge look="outline" variant="success"> 12% </Badge>
|
|
||||||
<Badge look="outline" variant="danger"> 12% </Badge>
|
|
||||||
<Badge look="outline" variant="pending"> 12% </Badge>
|
|
||||||
<Badge look="outline" variant="warning"> 12% </Badge>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Badge look="outline" variant="primary"> 12% </Badge>
|
|
||||||
<Badge look="outline" variant="secondary"> 12% </Badge>
|
|
||||||
<Badge look="outline" variant="success"> 12% </Badge>
|
|
||||||
<Badge look="outline" variant="danger"> 12% </Badge>
|
|
||||||
<Badge look="outline" variant="pending"> 12% </Badge>
|
|
||||||
<Badge look="outline" variant="warning"> 12% </Badge>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Badge look="outline" variant="primary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="secondary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="success"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="danger"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="pending"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="warning"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Badge look="outline" variant="primary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="secondary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="success"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="danger"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="pending"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="warning"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Badge look="filled" variant="primary"> 12% </Badge>
|
|
||||||
<Badge look="filled" variant="secondary"> 12% </Badge>
|
|
||||||
<Badge look="filled" variant="success"> 12% </Badge>
|
|
||||||
<Badge look="filled" variant="danger"> 12% </Badge>
|
|
||||||
<Badge look="filled" variant="pending"> 12% </Badge>
|
|
||||||
<Badge look="filled" variant="warning"> 12% </Badge>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Badge look="filled" variant="primary"> 12% </Badge>
|
|
||||||
<Badge look="filled" variant="secondary"> 12% </Badge>
|
|
||||||
<Badge look="filled" variant="success"> 12% </Badge>
|
|
||||||
<Badge look="filled" variant="danger"> 12% </Badge>
|
|
||||||
<Badge look="filled" variant="pending"> 12% </Badge>
|
|
||||||
<Badge look="filled" variant="warning"> 12% </Badge>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Badge look="filled" variant="primary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="filled" variant="secondary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="filled" variant="success"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="filled" variant="danger"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="filled" variant="pending"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="filled" variant="warning"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Badge look="filled" variant="primary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="filled" variant="secondary"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="filled" variant="success"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="filled" variant="danger"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="filled" variant="pending"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
<Badge look="filled" variant="warning"> 12%
|
|
||||||
<ChevronDown />
|
|
||||||
</Badge>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Badge look="outline" variant="primary" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="secondary" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="success" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="danger" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="pending" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="warning" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Badge look="outline" variant="primary" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="secondary" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="success" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="danger" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="pending" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
<Badge look="outline" variant="warning" content="12% Higher than last month">
|
|
||||||
<CheckSquare /> 12%
|
|
||||||
</Badge>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,445 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import {
|
|
||||||
CarouselRoot,
|
|
||||||
CarouselControl,
|
|
||||||
CarouselPrevTrigger,
|
|
||||||
CarouselNextTrigger,
|
|
||||||
CarouselIndicatorGroup,
|
|
||||||
CarouselIndicator,
|
|
||||||
CarouselItemGroup,
|
|
||||||
CarouselItem,
|
|
||||||
} from "@/components/ui/carousel";
|
|
||||||
import {
|
|
||||||
Preview,
|
|
||||||
SectionTitle,
|
|
||||||
SectionContent,
|
|
||||||
InstallPackage,
|
|
||||||
PreviewCode,
|
|
||||||
} from "@/components/docs";
|
|
||||||
const images = Array.from(
|
|
||||||
{ length: 5 },
|
|
||||||
(_, i) => `https://picsum.photos/seed/${i + 1}/500/300`
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="preview" class="-mt-20">
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<CarouselRoot :default-page="0" :slide-count="images.length" class="size-72">
|
|
||||||
<CarouselControl>
|
|
||||||
<CarouselPrevTrigger />
|
|
||||||
<CarouselNextTrigger />
|
|
||||||
</CarouselControl>
|
|
||||||
<CarouselIndicatorGroup>
|
|
||||||
<CarouselIndicator v-for="(_, index) in images" :key="index" :index="index" />
|
|
||||||
</CarouselIndicatorGroup>
|
|
||||||
<CarouselItemGroup>
|
|
||||||
<CarouselItem v-for="(image, index) in images" :key="index" :index="index"
|
|
||||||
class="text-5xl bold flex items-center justify-center">
|
|
||||||
{{ index + 1 }}
|
|
||||||
</CarouselItem>
|
|
||||||
</CarouselItemGroup>
|
|
||||||
</CarouselRoot>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{`
|
|
||||||
const images = Array.from(
|
|
||||||
{ length: 5 },
|
|
||||||
(_, i) => \`https://picsum.photos/seed/\${i + 1}/500/300\`
|
|
||||||
);
|
|
||||||
|
|
||||||
<CarouselRoot :default-page="0" :slide-count="images.length" class="size-72">
|
|
||||||
<CarouselControl>
|
|
||||||
<CarouselPrevTrigger />
|
|
||||||
<CarouselNextTrigger />
|
|
||||||
</CarouselControl>
|
|
||||||
<CarouselIndicatorGroup>
|
|
||||||
<CarouselIndicator v-for="(_, index) in images" :key="index" :index="index" />
|
|
||||||
</CarouselIndicatorGroup>
|
|
||||||
<CarouselItemGroup>
|
|
||||||
<CarouselItem v-for="(image, index) in images" :key="index" :index="index"
|
|
||||||
class="text-5xl bold flex items-center justify-center">
|
|
||||||
\{\{ index + 1 \}\}
|
|
||||||
</CarouselItem>
|
|
||||||
</CarouselItemGroup>
|
|
||||||
</CarouselRoot>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
<div id="installation">
|
|
||||||
<SectionTitle>Installation</SectionTitle>
|
|
||||||
<SectionContent>Install the following dependencies:</SectionContent>
|
|
||||||
<InstallPackage>add @zag-js/vue @zag-js/carousel</InstallPackage>
|
|
||||||
<SectionContent>
|
|
||||||
Copy and paste the following code into your project.
|
|
||||||
</SectionContent>
|
|
||||||
<PreviewCode title="components/ui/carousel/CarouselRoot.vue">
|
|
||||||
{{`
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import * as carousel from "@zag-js/carousel";
|
|
||||||
import { useMachine, normalizeProps } from "@zag-js/vue";
|
|
||||||
import type { Props } from "@zag-js/carousel";
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { carouselRoot } from "@mykopkb/core/styles/carousel.styles";
|
|
||||||
import { computed, provide } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
defaultPage,
|
|
||||||
slideCount,
|
|
||||||
spacing = "2rem",
|
|
||||||
allowMouseDrag = true,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
} = defineProps<Partial<Props> & { asChild?: boolean; class?: string }>();
|
|
||||||
|
|
||||||
const service = useMachine(carousel.machine, {
|
|
||||||
defaultPage,
|
|
||||||
slideCount,
|
|
||||||
spacing,
|
|
||||||
allowMouseDrag,
|
|
||||||
...props,
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const api = computed(() => carousel.connect(service, normalizeProps));
|
|
||||||
|
|
||||||
provide("carouselApi", api);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot :class="cn(carouselRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<div v-else>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/carousel/CarouselControl.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { carouselControl } from "@mykopkb/core/styles/carousel.styles";
|
|
||||||
import type { Api } from "@zag-js/carousel";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
} = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
asChild?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = inject<Api>("carouselApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot :class="cn(carouselControl, className)" v-bind="{ ...props, ...$attrs, ...api?.getControlProps() }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<div v-else>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/carousel/CarouselPrevTrigger.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ArrowLeft } from "@lucide/vue";
|
|
||||||
import { carouselPrevTrigger } from "@mykopkb/core/styles/carousel.styles";
|
|
||||||
import type { Api } from "@zag-js/carousel";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
} = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
asChild?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = inject<Api>("carouselApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot v-bind="{ ...props, ...$attrs, ...api?.getPrevTriggerProps() }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<Button variant="ghost" v-else :class="cn(carouselPrevTrigger, className)">
|
|
||||||
<slot v-if="$slots.default" />
|
|
||||||
<ArrowLeft v-else />
|
|
||||||
</Button>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/carousel/CarouselNextTrigger.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ArrowRight } from "@lucide/vue";
|
|
||||||
import { carouselNextTrigger } from "@mykopkb/core/styles/carousel.styles";
|
|
||||||
import type { Api } from "@zag-js/carousel";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
} = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
asChild?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = inject<Api>("carouselApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot v-bind="{ ...props, ...$attrs, ...api?.getNextTriggerProps() }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<Button variant="ghost" v-else :class="cn(carouselNextTrigger, className)">
|
|
||||||
<slot v-if="$slots.default" />
|
|
||||||
<ArrowRight v-else />
|
|
||||||
</Button>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/carousel/CarouselIndicatorGroup.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { carouselIndicatorGroup } from "@mykopkb/core/styles/carousel.styles";
|
|
||||||
import type { Api } from "@zag-js/carousel";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
} = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
asChild?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = inject<Api>("carouselApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot :class="cn(carouselIndicatorGroup, className)"
|
|
||||||
v-bind="{ ...props, ...$attrs, ...api?.getIndicatorGroupProps() }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<div v-else>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/carousel/CarouselIndicator.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { carouselIndicator } from "@mykopkb/core/styles/carousel.styles";
|
|
||||||
import type { Api, IndicatorProps } from "@zag-js/carousel";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
asChild = false,
|
|
||||||
index,
|
|
||||||
...props
|
|
||||||
} = defineProps<
|
|
||||||
{
|
|
||||||
class?: string;
|
|
||||||
asChild?: boolean;
|
|
||||||
} & IndicatorProps
|
|
||||||
>();
|
|
||||||
|
|
||||||
const api = inject<Api>("carouselApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<button :class="cn(carouselIndicator, className)"
|
|
||||||
v-bind="{ ...props, ...$attrs, ...api?.getIndicatorProps({ index }) }" />
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/carousel/CarouselItemGroup.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { carouselItemGroup } from "@mykopkb/core/styles/carousel.styles";
|
|
||||||
import type { Api } from "@zag-js/carousel";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
} = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
asChild?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = inject<Api>("carouselApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot :class="cn(carouselItemGroup, className)" v-bind="{ ...props, ...$attrs, ...api?.getItemGroupProps() }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<div v-else>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/carousel/CarouselItem.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { Box } from "@/components/ui/box";
|
|
||||||
import { carouselItem } from "@mykopkb/core/styles/carousel.styles";
|
|
||||||
import type { Api, ItemProps } from "@zag-js/carousel";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
asChild = false,
|
|
||||||
index,
|
|
||||||
...props
|
|
||||||
} = defineProps<
|
|
||||||
{
|
|
||||||
class?: string;
|
|
||||||
asChild?: boolean;
|
|
||||||
} & ItemProps
|
|
||||||
>();
|
|
||||||
|
|
||||||
const api = inject<Api>("carouselApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot v-bind="{ ...props, ...$attrs, ...api?.getItemProps({ index }) }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<Box v-else :class="cn(carouselItem, className)">
|
|
||||||
<slot />
|
|
||||||
</Box>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/carousel/index.ts">
|
|
||||||
{{ `
|
|
||||||
export { default as CarouselRoot } from "./CarouselRoot.vue";
|
|
||||||
export { default as CarouselControl } from "./CarouselControl.vue";
|
|
||||||
export { default as CarouselPrevTrigger } from "./CarouselPrevTrigger.vue";
|
|
||||||
export { default as CarouselNextTrigger } from "./CarouselNextTrigger.vue";
|
|
||||||
export { default as CarouselIndicatorGroup } from "./CarouselIndicatorGroup.vue";
|
|
||||||
export { default as CarouselIndicator } from "./CarouselIndicator.vue";
|
|
||||||
export { default as CarouselItemGroup } from "./CarouselItemGroup.vue";
|
|
||||||
export { default as CarouselItem } from "./CarouselItem.vue";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<SectionContent>
|
|
||||||
Update the import paths to match your project setup.
|
|
||||||
</SectionContent>
|
|
||||||
</div>
|
|
||||||
<div id="usage">
|
|
||||||
<SectionTitle>Usage</SectionTitle>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
import {
|
|
||||||
CarouselRoot,
|
|
||||||
CarouselControl,
|
|
||||||
CarouselPrevTrigger,
|
|
||||||
CarouselNextTrigger,
|
|
||||||
CarouselIndicatorGroup,
|
|
||||||
CarouselIndicator,
|
|
||||||
CarouselItemGroup,
|
|
||||||
CarouselItem,
|
|
||||||
} from "@/components/ui/carousel";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<CarouselRoot :default-page="0" :slide-count="images.length" class="size-72">
|
|
||||||
<CarouselControl>
|
|
||||||
<CarouselPrevTrigger />
|
|
||||||
<CarouselNextTrigger />
|
|
||||||
</CarouselControl>
|
|
||||||
<CarouselIndicatorGroup>
|
|
||||||
<CarouselIndicator v-for="(_, index) in images" :key="index" :index="index" />
|
|
||||||
</CarouselIndicatorGroup>
|
|
||||||
<CarouselItemGroup>
|
|
||||||
<CarouselItem v-for="(image, index) in images" :key="index" :index="index"
|
|
||||||
class="text-5xl bold flex items-center justify-center">
|
|
||||||
\{\{ index + 1 \}\}
|
|
||||||
</CarouselItem>
|
|
||||||
</CarouselItemGroup>
|
|
||||||
</CarouselRoot>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</div>
|
|
||||||
<div id="variants">
|
|
||||||
<SectionTitle>Variants</SectionTitle>
|
|
||||||
<SectionContent>A collection of components you can use.</SectionContent>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<CarouselRoot :default-page="0" :slide-count="images.length" class="size-72">
|
|
||||||
<CarouselControl>
|
|
||||||
<CarouselPrevTrigger />
|
|
||||||
<CarouselNextTrigger />
|
|
||||||
</CarouselControl>
|
|
||||||
<CarouselIndicatorGroup>
|
|
||||||
<CarouselIndicator v-for="(_, index) in images" :key="index" :index="index" />
|
|
||||||
</CarouselIndicatorGroup>
|
|
||||||
<CarouselItemGroup>
|
|
||||||
<CarouselItem v-for="(image, index) in images" :key="index" :index="index">
|
|
||||||
<img :src="image" :alt="`Slide $\{index\}`" />
|
|
||||||
</CarouselItem>
|
|
||||||
</CarouselItemGroup>
|
|
||||||
</CarouselRoot>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<CarouselRoot :default-page="0" :slide-count="images.length" class="size-72">
|
|
||||||
<CarouselControl>
|
|
||||||
<CarouselPrevTrigger />
|
|
||||||
<CarouselNextTrigger />
|
|
||||||
</CarouselControl>
|
|
||||||
<CarouselIndicatorGroup>
|
|
||||||
<CarouselIndicator v-for="(_, index) in images" :key="index" :index="index" />
|
|
||||||
</CarouselIndicatorGroup>
|
|
||||||
<CarouselItemGroup>
|
|
||||||
<CarouselItem v-for="(image, index) in images" :key="index" :index="index">
|
|
||||||
<img :src="image" :alt="\`Slide $\{index\}\`" />
|
|
||||||
</CarouselItem>
|
|
||||||
</CarouselItemGroup>
|
|
||||||
</CarouselRoot>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import {
|
|
||||||
CheckboxRoot,
|
|
||||||
CheckboxLabel,
|
|
||||||
CheckboxControl,
|
|
||||||
} from "@/components/ui/checkbox";
|
|
||||||
import {
|
|
||||||
Preview,
|
|
||||||
SectionTitle,
|
|
||||||
SectionContent,
|
|
||||||
InstallPackage,
|
|
||||||
PreviewCode,
|
|
||||||
} from "@/components/docs";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="preview" class="-mt-20">
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<CheckboxRoot>
|
|
||||||
<CheckboxControl />
|
|
||||||
<CheckboxLabel>Accept terms and conditions</CheckboxLabel>
|
|
||||||
</CheckboxRoot>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<CheckboxRoot>
|
|
||||||
<CheckboxControl />
|
|
||||||
<CheckboxLabel>Accept terms and conditions</CheckboxLabel>
|
|
||||||
</CheckboxRoot>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
<div id="installation">
|
|
||||||
<SectionTitle>Installation</SectionTitle>
|
|
||||||
<SectionContent>Install the following dependencies:</SectionContent>
|
|
||||||
<InstallPackage>add @zag-js/vue @zag-js/checkbox</InstallPackage>
|
|
||||||
<SectionContent>
|
|
||||||
Copy and paste the following code into your project.
|
|
||||||
</SectionContent>
|
|
||||||
<PreviewCode title="components/ui/checkbox/CheckboxRoot.vue">
|
|
||||||
{{`
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { checkboxRoot } from "@mykopkb/core/styles/checkbox.styles";
|
|
||||||
import * as checkbox from "@zag-js/checkbox";
|
|
||||||
import { useMachine, normalizeProps } from "@zag-js/vue";
|
|
||||||
import type { Props } from "@zag-js/checkbox";
|
|
||||||
import { CheckboxHiddenInput } from ".";
|
|
||||||
import { computed, provide } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
checked = undefined,
|
|
||||||
...props
|
|
||||||
} = defineProps<Partial<Props> & { class?: string }>();
|
|
||||||
|
|
||||||
const service = useMachine(
|
|
||||||
checkbox.machine,
|
|
||||||
computed(() => ({
|
|
||||||
...props,
|
|
||||||
checked,
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = computed(() => checkbox.connect(service, normalizeProps));
|
|
||||||
|
|
||||||
provide("checkboxApi", api);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<label :class="cn(checkboxRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
|
|
||||||
<slot />
|
|
||||||
<CheckboxHiddenInput />
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/checkbox/CheckboxLabel.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import type { Api } from "@zag-js/checkbox";
|
|
||||||
import { checkboxLabel } from "@mykopkb/core/styles/checkbox.styles";
|
|
||||||
import { label } from "@mykopkb/core/styles/label.styles";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
} = defineProps<{
|
|
||||||
asChild?: boolean;
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = inject<Api>("checkboxApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot :class="cn([label, checkboxLabel, className])" v-bind="{ ...props, ...$attrs, ...api?.getLabelProps() }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<span v-else>
|
|
||||||
<slot />
|
|
||||||
</span>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/checkbox/CheckboxControl.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
import { CheckIcon } from "@lucide/vue";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { checkboxControl } from "@mykopkb/core/styles/checkbox.styles";
|
|
||||||
import { CheckboxIndicator } from ".";
|
|
||||||
import type { Api } from "@zag-js/checkbox";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
} = defineProps<{
|
|
||||||
asChild?: boolean;
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = inject<Api>("checkboxApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot :class="cn(checkboxControl, className)" v-bind="{ ...props, ...$attrs, ...api?.getControlProps() }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<div v-else>
|
|
||||||
<CheckboxIndicator>
|
|
||||||
<CheckIcon />
|
|
||||||
</CheckboxIndicator>
|
|
||||||
</div>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/checkbox/CheckboxIndicator.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Slot } from "@/components/ui/slot";
|
|
||||||
import type { Api } from "@zag-js/checkbox";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { checkboxIndicator } from "@mykopkb/core/styles/checkbox.styles";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
} = defineProps<{
|
|
||||||
asChild?: boolean;
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = inject<Api>("checkboxApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Slot :class="cn(checkboxIndicator, className)" v-bind="{ ...props, ...$attrs, ...api?.getIndicatorProps() }">
|
|
||||||
<slot v-if="asChild" />
|
|
||||||
<div v-else>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</Slot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/checkbox/CheckboxHiddenInput.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { Api } from "@zag-js/checkbox";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { checkboxHiddenInput } from "@mykopkb/core/styles/checkbox.styles";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const { class: className, ...props } = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = inject<Api>("checkboxApi");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<input :class="cn(checkboxHiddenInput, className)"
|
|
||||||
v-bind="{ ...props, ...$attrs, ...api?.getHiddenInputProps() }" />
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/checkbox/index.ts">
|
|
||||||
{{ `
|
|
||||||
export { default as CheckboxRoot } from "./CheckboxRoot.vue";
|
|
||||||
export { default as CheckboxLabel } from "./CheckboxLabel.vue";
|
|
||||||
export { default as CheckboxControl } from "./CheckboxControl.vue";
|
|
||||||
export { default as CheckboxIndicator } from "./CheckboxIndicator.vue";
|
|
||||||
export { default as CheckboxHiddenInput } from "./CheckboxHiddenInput.vue";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<SectionContent>
|
|
||||||
Update the import paths to match your project setup.
|
|
||||||
</SectionContent>
|
|
||||||
</div>
|
|
||||||
<div id="usage">
|
|
||||||
<SectionTitle>Usage</SectionTitle>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
import {
|
|
||||||
CheckboxRoot,
|
|
||||||
CheckboxLabel,
|
|
||||||
CheckboxControl,
|
|
||||||
} from "@/components/ui/checkbox";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<CheckboxRoot>
|
|
||||||
<CheckboxControl />
|
|
||||||
<CheckboxLabel>Accept terms and conditions</CheckboxLabel>
|
|
||||||
</CheckboxRoot>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Preview,
|
|
||||||
SectionTitle,
|
|
||||||
SectionContent,
|
|
||||||
PreviewCode,
|
|
||||||
} from "@/components/docs";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="preview" class="-mt-20">
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Input class="w-84" type="email" placeholder="Email" />
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Input class="w-84" type="email" placeholder="Email" />
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
<div id="installation">
|
|
||||||
<SectionTitle>Installation</SectionTitle>
|
|
||||||
<SectionContent>
|
|
||||||
Copy and paste the following code into your project.
|
|
||||||
</SectionContent>
|
|
||||||
<PreviewCode title="components/ui/input/Input.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { input } from "@mykopkb/core/styles/input.styles";
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
type,
|
|
||||||
...props
|
|
||||||
} = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
type?: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<input :type="type" :class="cn(input, className)" v-bind="{ ...props }" />
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/input/index.ts">
|
|
||||||
{{ `
|
|
||||||
export { default as Input } from "./Input.vue";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<SectionContent>
|
|
||||||
Update the import paths to match your project setup.
|
|
||||||
</SectionContent>
|
|
||||||
</div>
|
|
||||||
<div id="usage">
|
|
||||||
<SectionTitle>Usage</SectionTitle>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Input class="w-84" type="email" placeholder="Email" />
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { Map } from "@/components/ui/map";
|
|
||||||
import {
|
|
||||||
Preview,
|
|
||||||
SectionTitle,
|
|
||||||
SectionContent,
|
|
||||||
InstallPackage,
|
|
||||||
PreviewCode,
|
|
||||||
} from "@/components/docs";
|
|
||||||
const markersData = {
|
|
||||||
type: "FeatureCollection" as const,
|
|
||||||
features: [
|
|
||||||
{
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: { name: "Cafe Berlin", type: "cafe" },
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: [13.388, 52.517],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: { name: "Restaurant Alex", type: "restaurant" },
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: [13.39, 52.518],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: { name: "Coffee House", type: "cafe" },
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: [13.385, 52.515],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: { name: "Pizza Place", type: "restaurant" },
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: [13.392, 52.519],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: { name: "Burger Joint", type: "restaurant" },
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: [13.387, 52.516],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: { name: "Starbucks", type: "cafe" },
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: [13.391, 52.52],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: { name: "Sushi Bar", type: "restaurant" },
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: [13.395, 52.522],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: { name: "Bakery", type: "cafe" },
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: [13.383, 52.514],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: { name: "Italian Restaurant", type: "restaurant" },
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: [13.398, 52.525],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: { name: "Tea House", type: "cafe" },
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: [13.38, 52.512],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="preview" class="-mt-20">
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Map class="w-full h-96" :center="[13.388, 52.517]" :zoom="9.5" :markers="markersData" />
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Map class="w-full h-96" :center="[13.388, 52.517]" :zoom="9.5" :markers="markersData" />
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
<div id="installation">
|
|
||||||
<SectionTitle>Installation</SectionTitle>
|
|
||||||
<SectionContent>Install the following dependencies:</SectionContent>
|
|
||||||
<InstallPackage>add maplibre-gl</InstallPackage>
|
|
||||||
<SectionContent>
|
|
||||||
Copy and paste the following code into your project.
|
|
||||||
</SectionContent>
|
|
||||||
<PreviewCode title="components/ui/map/Map.vue">
|
|
||||||
{{`
|
|
||||||
<${""}script setup lang="ts">
|
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
|
||||||
import maplibregl, { type MapOptions } from "maplibre-gl";
|
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Plus, Minus, MapPin, Compass, Expand, X } from "@lucide/vue";
|
|
||||||
import type { FeatureCollection, Point } from "geojson";
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { map } from "@mykopkb/core/styles/map.styles";
|
|
||||||
|
|
||||||
const mapRef = ref();
|
|
||||||
const mapInstance = ref<maplibregl.Map>();
|
|
||||||
const isFullscreen = ref(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
markers,
|
|
||||||
...props
|
|
||||||
} = defineProps<
|
|
||||||
Partial<MapOptions> & {
|
|
||||||
class?: string;
|
|
||||||
markers?: FeatureCollection<Point>;
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
mapInstance.value = new maplibregl.Map({
|
|
||||||
...Object.fromEntries(Object.entries(props).filter(([_, value]) => value)),
|
|
||||||
style: "https://tiles.openfreemap.org/styles/positron",
|
|
||||||
container: mapRef.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
markers &&
|
|
||||||
mapInstance.value.on("load", () => {
|
|
||||||
// Add source with clustering
|
|
||||||
mapInstance.value!.addSource("markers", {
|
|
||||||
type: "geojson",
|
|
||||||
data: markers,
|
|
||||||
cluster: true,
|
|
||||||
clusterMaxZoom: 14, // Max zoom for clustering
|
|
||||||
clusterRadius: 50, // Cluster radius in pixels
|
|
||||||
});
|
|
||||||
|
|
||||||
// Layer for clusters (circles)
|
|
||||||
mapInstance.value!.addLayer({
|
|
||||||
id: "clusters",
|
|
||||||
type: "circle",
|
|
||||||
source: "markers",
|
|
||||||
filter: ["has", "point_count"],
|
|
||||||
paint: {
|
|
||||||
"circle-color": [
|
|
||||||
"step",
|
|
||||||
["get", "point_count"],
|
|
||||||
"#cccccc", // Color for < 5 points
|
|
||||||
5,
|
|
||||||
"#cccccc", // Color for 5-10 points
|
|
||||||
10,
|
|
||||||
"#cccccc", // Color for > 10 points
|
|
||||||
],
|
|
||||||
"circle-radius": [
|
|
||||||
"step",
|
|
||||||
["get", "point_count"],
|
|
||||||
20, // Radius for < 5 points
|
|
||||||
5,
|
|
||||||
30, // Radius for 5-10 points
|
|
||||||
10,
|
|
||||||
40, // Radius for > 10 points
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Layer for cluster count numbers
|
|
||||||
mapInstance.value!.addLayer({
|
|
||||||
id: "cluster-count",
|
|
||||||
type: "symbol",
|
|
||||||
source: "markers",
|
|
||||||
filter: ["has", "point_count"],
|
|
||||||
layout: {
|
|
||||||
"text-field": "{point_count_abbreviated}",
|
|
||||||
"text-font": ["Open Sans Bold"],
|
|
||||||
"text-size": 12,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Layer for individual points (unclustered)
|
|
||||||
mapInstance.value!.addLayer({
|
|
||||||
id: "unclustered-point",
|
|
||||||
type: "circle",
|
|
||||||
source: "markers",
|
|
||||||
filter: ["!", ["has", "point_count"]],
|
|
||||||
paint: {
|
|
||||||
"circle-color": "#333333",
|
|
||||||
"circle-radius": 8,
|
|
||||||
"circle-stroke-width": 2,
|
|
||||||
"circle-stroke-color": "#ffffff",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click on cluster to zoom in
|
|
||||||
mapInstance.value!.on("click", "clusters", async (e) => {
|
|
||||||
const features = mapInstance.value!.queryRenderedFeatures(e.point, {
|
|
||||||
layers: ["clusters"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const clusterId = features[0]?.properties.cluster_id;
|
|
||||||
const source = mapInstance.value!.getSource(
|
|
||||||
"markers"
|
|
||||||
) as maplibregl.GeoJSONSource;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const zoom = await source.getClusterExpansionZoom(clusterId);
|
|
||||||
|
|
||||||
mapInstance.value!.easeTo({
|
|
||||||
center: (features[0]?.geometry as any).coordinates,
|
|
||||||
zoom: zoom,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error getting cluster expansion zoom:", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click on individual point to show popup
|
|
||||||
mapInstance.value!.on("click", "unclustered-point", (e) => {
|
|
||||||
const coordinates = (
|
|
||||||
e.features![0]?.geometry as any
|
|
||||||
).coordinates.slice();
|
|
||||||
const { name, type } = (e.features![0]?.properties || {}) as any;
|
|
||||||
|
|
||||||
new maplibregl.Popup()
|
|
||||||
.setLngLat(coordinates)
|
|
||||||
.setHTML(\`<h3>\${name}</h3><p>\${type}</p>\`)
|
|
||||||
.addTo(mapInstance.value!);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Change cursor on hover cluster/point
|
|
||||||
mapInstance.value!.on("mouseenter", "clusters", () => {
|
|
||||||
mapInstance.value!.getCanvas().style.cursor = "pointer";
|
|
||||||
});
|
|
||||||
mapInstance.value!.on("mouseleave", "clusters", () => {
|
|
||||||
mapInstance.value!.getCanvas().style.cursor = "";
|
|
||||||
});
|
|
||||||
mapInstance.value!.on("mouseenter", "unclustered-point", () => {
|
|
||||||
mapInstance.value!.getCanvas().style.cursor = "pointer";
|
|
||||||
});
|
|
||||||
mapInstance.value!.on("mouseleave", "unclustered-point", () => {
|
|
||||||
mapInstance.value!.getCanvas().style.cursor = "";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const zoomIn = () => {
|
|
||||||
mapInstance.value?.zoomIn();
|
|
||||||
};
|
|
||||||
|
|
||||||
const zoomOut = () => {
|
|
||||||
mapInstance.value?.zoomOut();
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetNorth = () => {
|
|
||||||
mapInstance.value?.easeTo({ bearing: 0, pitch: 0 });
|
|
||||||
};
|
|
||||||
|
|
||||||
const locateMe = () => {
|
|
||||||
if (!navigator.geolocation) {
|
|
||||||
alert("Geolocation is not supported by your browser");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
(position) => {
|
|
||||||
const { longitude, latitude } = position.coords;
|
|
||||||
mapInstance.value?.flyTo({
|
|
||||||
center: [longitude, latitude],
|
|
||||||
zoom: 14,
|
|
||||||
});
|
|
||||||
|
|
||||||
new maplibregl.Marker({ color: "var(--color-foreground)" })
|
|
||||||
.setLngLat([longitude, latitude])
|
|
||||||
.addTo(mapInstance.value!);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
alert("Failed to get location: " + error.message);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleFullscreen = () => {
|
|
||||||
if (!mapRef.value) return;
|
|
||||||
|
|
||||||
if (!isFullscreen.value) {
|
|
||||||
if (mapRef.value.requestFullscreen) {
|
|
||||||
mapRef.value.requestFullscreen();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (document.exitFullscreen) {
|
|
||||||
document.exitFullscreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isFullscreen.value = !isFullscreen.value;
|
|
||||||
};
|
|
||||||
</${""}script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div data-scope="map" data-part="root" ref="mapRef" :class="cn(map, className)">
|
|
||||||
<div data-scope="map" data-part="controls">
|
|
||||||
<Button data-scope="map" data-part="zoom-in" @click="zoomIn" variant="ghost" size="sm">
|
|
||||||
<Plus />
|
|
||||||
</Button>
|
|
||||||
<Button data-scope="map" data-part="zoom-out" @click="zoomOut" variant="ghost" size="sm">
|
|
||||||
<Minus />
|
|
||||||
</Button>
|
|
||||||
<Button data-scope="map" data-part="reset-north" @click="resetNorth" variant="ghost" size="sm">
|
|
||||||
<Compass />
|
|
||||||
</Button>
|
|
||||||
<Button data-scope="map" data-part="locate" @click="locateMe" variant="ghost" size="sm">
|
|
||||||
<MapPin />
|
|
||||||
</Button>
|
|
||||||
<Button data-scope="map" data-part="toggle-fullscreen" @click="toggleFullscreen" variant="ghost" size="sm">
|
|
||||||
<Expand v-if="!isFullscreen" />
|
|
||||||
<X v-else />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/map/index.ts">
|
|
||||||
{{ `
|
|
||||||
export { default as Map } from "./Map.vue";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<SectionContent>
|
|
||||||
Update the import paths to match your project setup.
|
|
||||||
</SectionContent>
|
|
||||||
</div>
|
|
||||||
<div id="usage">
|
|
||||||
<SectionTitle>Usage</SectionTitle>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
import { Map } from "@/components/ui/map";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Map class="w-full h-96" :center="[13.388, 52.517]" :zoom="9.5" :markers="markersData" />
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,653 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableHeader,
|
|
||||||
TableBody,
|
|
||||||
TableFooter,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableCell,
|
|
||||||
TableCaption,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Preview,
|
|
||||||
SectionTitle,
|
|
||||||
SectionContent,
|
|
||||||
PreviewCode,
|
|
||||||
} from "@/components/docs";
|
|
||||||
const invoices: {
|
|
||||||
invoice: string;
|
|
||||||
paymentStatus: string;
|
|
||||||
badge: "success" | "pending" | "danger";
|
|
||||||
totalAmount: string;
|
|
||||||
paymentMethod: string;
|
|
||||||
}[] = [
|
|
||||||
{
|
|
||||||
invoice: "INV001",
|
|
||||||
paymentStatus: "Paid",
|
|
||||||
badge: "success",
|
|
||||||
totalAmount: "$250.00",
|
|
||||||
paymentMethod: "Credit Card",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
invoice: "INV002",
|
|
||||||
paymentStatus: "Pending",
|
|
||||||
badge: "pending",
|
|
||||||
totalAmount: "$150.00",
|
|
||||||
paymentMethod: "PayPal",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
invoice: "INV003",
|
|
||||||
paymentStatus: "Unpaid",
|
|
||||||
badge: "danger",
|
|
||||||
totalAmount: "$350.00",
|
|
||||||
paymentMethod: "Bank Transfer",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
invoice: "INV004",
|
|
||||||
paymentStatus: "Paid",
|
|
||||||
badge: "success",
|
|
||||||
totalAmount: "$450.00",
|
|
||||||
paymentMethod: "Credit Card",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
invoice: "INV005",
|
|
||||||
paymentStatus: "Paid",
|
|
||||||
badge: "success",
|
|
||||||
totalAmount: "$550.00",
|
|
||||||
paymentMethod: "PayPal",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
invoice: "INV006",
|
|
||||||
paymentStatus: "Pending",
|
|
||||||
badge: "pending",
|
|
||||||
totalAmount: "$200.00",
|
|
||||||
paymentMethod: "Bank Transfer",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
invoice: "INV007",
|
|
||||||
paymentStatus: "Unpaid",
|
|
||||||
badge: "danger",
|
|
||||||
totalAmount: "$300.00",
|
|
||||||
paymentMethod: "Credit Card",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="preview" class="-mt-20">
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Table>
|
|
||||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead class="w-[100px]">Invoice</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Method</TableHead>
|
|
||||||
<TableHead class="text-right">Amount</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
|
|
||||||
<TableCell class="font-medium">
|
|
||||||
{{ invoice.invoice }}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge :variant="invoice.badge">
|
|
||||||
{{ invoice.paymentStatus }}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{{ invoice.paymentMethod }}</TableCell>
|
|
||||||
<TableCell class="text-right">
|
|
||||||
{{ invoice.totalAmount }}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell :colspan="3">Total</TableCell>
|
|
||||||
<TableCell class="text-right">$2,500.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableFooter>
|
|
||||||
</Table>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Table>
|
|
||||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead class="w-[100px]">Invoice</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Method</TableHead>
|
|
||||||
<TableHead class="text-right">Amount</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
|
|
||||||
<TableCell class="font-medium">
|
|
||||||
\{\{ invoice.invoice \}\}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge :variant="invoice.badge">
|
|
||||||
\{\{ invoice.paymentStatus \}\}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>\{\{ invoice.paymentMethod \}\}</TableCell>
|
|
||||||
<TableCell class="text-right">
|
|
||||||
\{\{ invoice.totalAmount \}\}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell :colspan="3">Total</TableCell>
|
|
||||||
<TableCell class="text-right">$2,500.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableFooter>
|
|
||||||
</Table>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
<div id="installation">
|
|
||||||
<SectionTitle>Installation</SectionTitle>
|
|
||||||
<SectionContent>
|
|
||||||
Copy and paste the following code into your project.
|
|
||||||
</SectionContent>
|
|
||||||
<PreviewCode title="components/ui/table/Table.vue">
|
|
||||||
{{`
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import {
|
|
||||||
tableVariants,
|
|
||||||
type TableVariants,
|
|
||||||
} from "@mykopkb/core/styles/table.styles";
|
|
||||||
import { provide, computed } from "vue";
|
|
||||||
import TableContainer from "./TableContainer.vue";
|
|
||||||
|
|
||||||
const { class: className, ...props } = defineProps<
|
|
||||||
TableVariants & {
|
|
||||||
class?: string;
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
|
|
||||||
provide(
|
|
||||||
"tableVariant",
|
|
||||||
computed(() => ({
|
|
||||||
variant: props.variant,
|
|
||||||
raised: props.raised,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<TableContainer>
|
|
||||||
<table :class="cn(tableVariants({ variant, raised, className }), className)" v-bind="{ ...props }">
|
|
||||||
<slot />
|
|
||||||
</table>
|
|
||||||
</TableContainer>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/table/TableContainer.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { tableContainer } from "@mykopkb/core/styles/table.styles";
|
|
||||||
|
|
||||||
const { class: className, ...props } = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="cn(tableContainer, className)" v-bind="{ ...props }">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/table/TableHeader.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { tableHeader } from "@mykopkb/core/styles/table.styles";
|
|
||||||
|
|
||||||
const { class: className, ...props } = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<thead :class="cn(tableHeader, className)" v-bind="{ ...props }">
|
|
||||||
<slot />
|
|
||||||
</thead>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/table/TableBody.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { tableBody } from "@mykopkb/core/styles/table.styles";
|
|
||||||
|
|
||||||
const { class: className, ...props } = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<tbody :class="cn(tableBody, className)" v-bind="{ ...props }">
|
|
||||||
<slot />
|
|
||||||
</tbody>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/table/TableFooter.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { tableFooter } from "@mykopkb/core/styles/table.styles";
|
|
||||||
|
|
||||||
const { class: className, ...props } = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<tfoot :class="cn(tableFooter, className)" v-bind="{ ...props }">
|
|
||||||
<slot />
|
|
||||||
</tfoot>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/table/TableHead.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { tableHead } from "@mykopkb/core/styles/table.styles";
|
|
||||||
|
|
||||||
const { class: className, ...props } = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<th :class="cn(tableHead, className)" v-bind="{ ...props }">
|
|
||||||
<slot />
|
|
||||||
</th>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/table/TableRow.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { tableRow } from "@mykopkb/core/styles/table.styles";
|
|
||||||
|
|
||||||
const { class: className, ...props } = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<tr :class="cn(tableRow, className)" v-bind="{ ...props }">
|
|
||||||
<slot />
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/table/TableCell.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { tableCellVariants } from "@mykopkb/core/styles/table.styles";
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const { class: className, ...props } = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const variant = inject<
|
|
||||||
| {
|
|
||||||
variant?: "default" | "boxed" | null;
|
|
||||||
raised?: "single" | "double" | null;
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
>("tableVariant", undefined);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<td :class="cn(tableCellVariants({ ...variant, className }), className)" v-bind="{ ...props }">
|
|
||||||
<slot />
|
|
||||||
</td>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/table/TableCaption.vue">
|
|
||||||
{{ `
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@mykopkb/core/utils/cn";
|
|
||||||
import { tableCaption } from "@mykopkb/core/styles/table.styles";
|
|
||||||
|
|
||||||
const { class: className, ...props } = defineProps<{
|
|
||||||
class?: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<caption :class="cn(tableCaption, className)" v-bind="{ ...props }">
|
|
||||||
<slot />
|
|
||||||
</caption>
|
|
||||||
</template>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode title="components/ui/table/index.ts">
|
|
||||||
{{ `
|
|
||||||
export { default as Table } from "./Table.vue";
|
|
||||||
export { default as TableContainer } from "./TableContainer.vue";
|
|
||||||
export { default as TableHeader } from "./TableHeader.vue";
|
|
||||||
export { default as TableBody } from "./TableBody.vue";
|
|
||||||
export { default as TableFooter } from "./TableFooter.vue";
|
|
||||||
export { default as TableHead } from "./TableHead.vue";
|
|
||||||
export { default as TableRow } from "./TableRow.vue";
|
|
||||||
export { default as TableCell } from "./TableCell.vue";
|
|
||||||
export { default as TableCaption } from "./TableCaption.vue";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<SectionContent>
|
|
||||||
Update the import paths to match your project setup.
|
|
||||||
</SectionContent>
|
|
||||||
</div>
|
|
||||||
<div id="usage">
|
|
||||||
<SectionTitle>Usage</SectionTitle>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableHeader,
|
|
||||||
TableBody,
|
|
||||||
TableFooter,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableCell,
|
|
||||||
TableCaption,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Table>
|
|
||||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead class="w-[100px]">Invoice</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Method</TableHead>
|
|
||||||
<TableHead class="text-right">Amount</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
|
|
||||||
<TableCell class="font-medium">
|
|
||||||
\{\{ invoice.invoice \}\}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge :variant="invoice.badge">
|
|
||||||
\{\{ invoice.paymentStatus \}\}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>\{\{ invoice.paymentMethod \}\}</TableCell>
|
|
||||||
<TableCell class="text-right">
|
|
||||||
\{\{ invoice.totalAmount \}\}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell :colspan="3">Total</TableCell>
|
|
||||||
<TableCell class="text-right">$2,500.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableFooter>
|
|
||||||
</Table>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</div>
|
|
||||||
<div id="variants">
|
|
||||||
<SectionTitle>Variants</SectionTitle>
|
|
||||||
<SectionContent>A collection of components you can use.</SectionContent>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Table variant="boxed">
|
|
||||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead class="w-[100px]">Invoice</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Method</TableHead>
|
|
||||||
<TableHead class="text-right">Amount</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
|
|
||||||
<TableCell class="font-medium">
|
|
||||||
{{ invoice.invoice }}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge :variant="invoice.badge">
|
|
||||||
{{ invoice.paymentStatus }}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{{ invoice.paymentMethod }}</TableCell>
|
|
||||||
<TableCell class="text-right">
|
|
||||||
{{ invoice.totalAmount }}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell :colspan="3">Total</TableCell>
|
|
||||||
<TableCell class="text-right">$2,500.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableFooter>
|
|
||||||
</Table>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Table variant="boxed">
|
|
||||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead class="w-[100px]">Invoice</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Method</TableHead>
|
|
||||||
<TableHead class="text-right">Amount</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
|
|
||||||
<TableCell class="font-medium">
|
|
||||||
\{\{ invoice.invoice \}\}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge :variant="invoice.badge">
|
|
||||||
\{\{ invoice.paymentStatus \}\}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>\{\{ invoice.paymentMethod \}\}</TableCell>
|
|
||||||
<TableCell class="text-right">
|
|
||||||
\{\{ invoice.totalAmount \}\}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell :colspan="3">Total</TableCell>
|
|
||||||
<TableCell class="text-right">$2,500.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableFooter>
|
|
||||||
</Table>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Table variant="boxed" raised="single">
|
|
||||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead class="w-[100px]">Invoice</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Method</TableHead>
|
|
||||||
<TableHead class="text-right">Amount</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
|
|
||||||
<TableCell class="font-medium">
|
|
||||||
{{ invoice.invoice }}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge :variant="invoice.badge">
|
|
||||||
{{ invoice.paymentStatus }}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{{ invoice.paymentMethod }}</TableCell>
|
|
||||||
<TableCell class="text-right">
|
|
||||||
{{ invoice.totalAmount }}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell :colspan="3">Total</TableCell>
|
|
||||||
<TableCell class="text-right">$2,500.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableFooter>
|
|
||||||
</Table>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Table variant="boxed" raised="single">
|
|
||||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead class="w-[100px]">Invoice</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Method</TableHead>
|
|
||||||
<TableHead class="text-right">Amount</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
|
|
||||||
<TableCell class="font-medium">
|
|
||||||
\{\{ invoice.invoice \}\}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge :variant="invoice.badge">
|
|
||||||
\{\{ invoice.paymentStatus \}\}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>\{\{ invoice.paymentMethod \}\}</TableCell>
|
|
||||||
<TableCell class="text-right">
|
|
||||||
\{\{ invoice.totalAmount \}\}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell :colspan="3">Total</TableCell>
|
|
||||||
<TableCell class="text-right">$2,500.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableFooter>
|
|
||||||
</Table>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
<Preview>
|
|
||||||
<template #preview>
|
|
||||||
<Table variant="boxed" raised="double">
|
|
||||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead class="w-[100px]">Invoice</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Method</TableHead>
|
|
||||||
<TableHead class="text-right">Amount</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
|
|
||||||
<TableCell class="font-medium">
|
|
||||||
{{ invoice.invoice }}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge :variant="invoice.badge">
|
|
||||||
{{ invoice.paymentStatus }}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{{ invoice.paymentMethod }}</TableCell>
|
|
||||||
<TableCell class="text-right">
|
|
||||||
{{ invoice.totalAmount }}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell :colspan="3">Total</TableCell>
|
|
||||||
<TableCell class="text-right">$2,500.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableFooter>
|
|
||||||
</Table>
|
|
||||||
</template>
|
|
||||||
<template #code>
|
|
||||||
<PreviewCode>
|
|
||||||
{{ `
|
|
||||||
<Table variant="boxed" raised="double">
|
|
||||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead class="w-[100px]">Invoice</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Method</TableHead>
|
|
||||||
<TableHead class="text-right">Amount</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
|
|
||||||
<TableCell class="font-medium">
|
|
||||||
\{\{ invoice.invoice \}\}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge :variant="invoice.badge">
|
|
||||||
\{\{ invoice.paymentStatus \}\}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>\{\{ invoice.paymentMethod \}\}</TableCell>
|
|
||||||
<TableCell class="text-right">
|
|
||||||
\{\{ invoice.totalAmount \}\}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell :colspan="3">Total</TableCell>
|
|
||||||
<TableCell class="text-right">$2,500.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableFooter>
|
|
||||||
</Table>
|
|
||||||
` }}
|
|
||||||
</PreviewCode>
|
|
||||||
</template>
|
|
||||||
</Preview>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
+7
-277
@@ -1,5 +1,4 @@
|
|||||||
import type { Menu } from '@/core/types/menu'
|
import type { Menu } from '@/core/types/menu'
|
||||||
import { authMenu } from '@/modules/auth'
|
|
||||||
import { userMenu } from '@/modules/user'
|
import { userMenu } from '@/modules/user'
|
||||||
import { roleMenu } from '@/modules/role'
|
import { roleMenu } from '@/modules/role'
|
||||||
import { membershipApplicationMenu } from '@/modules/membership-application'
|
import { membershipApplicationMenu } from '@/modules/membership-application'
|
||||||
@@ -8,6 +7,12 @@ import { activityMenu } from '@/modules/activity'
|
|||||||
export type { Menu }
|
export type { Menu }
|
||||||
|
|
||||||
const mainMenu: (string | Menu)[] = [
|
const mainMenu: (string | Menu)[] = [
|
||||||
|
'Dashboard',
|
||||||
|
{
|
||||||
|
icon: 'CircleGauge',
|
||||||
|
title: 'Dashboards',
|
||||||
|
route_name: 'dashboard-overview-1',
|
||||||
|
},
|
||||||
'Umum',
|
'Umum',
|
||||||
...activityMenu,
|
...activityMenu,
|
||||||
'Teknologi Maklumat',
|
'Teknologi Maklumat',
|
||||||
@@ -15,19 +20,7 @@ const mainMenu: (string | Menu)[] = [
|
|||||||
'Pentadbiran',
|
'Pentadbiran',
|
||||||
...membershipApplicationMenu,
|
...membershipApplicationMenu,
|
||||||
...userMenu,
|
...userMenu,
|
||||||
'GENERAL REPORTS',
|
'E-Commerce',
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'Dashboards',
|
|
||||||
badge: 4,
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'PanelBottomClose',
|
|
||||||
route_name: 'dashboard-overview-1',
|
|
||||||
title: 'Overview 1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: 'SquareKanban',
|
icon: 'SquareKanban',
|
||||||
title: 'E-Commerce',
|
title: 'E-Commerce',
|
||||||
@@ -99,11 +92,6 @@ const mainMenu: (string | Menu)[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
'APPS',
|
'APPS',
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'inbox',
|
|
||||||
title: 'Inbox',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: 'CircleGauge',
|
icon: 'CircleGauge',
|
||||||
route_name: 'file-manager',
|
route_name: 'file-manager',
|
||||||
@@ -115,218 +103,11 @@ const mainMenu: (string | Menu)[] = [
|
|||||||
route_name: 'point-of-sale',
|
route_name: 'point-of-sale',
|
||||||
title: 'Point of Sale',
|
title: 'Point of Sale',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'chat',
|
|
||||||
title: 'Chat',
|
|
||||||
badge: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'post',
|
|
||||||
title: 'Post',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// icon: 'CircleGauge',
|
|
||||||
// route_name: 'calendar',
|
|
||||||
// title: 'Calendar',
|
|
||||||
// },
|
|
||||||
'PAGES',
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'Crud',
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'crud-data-list',
|
|
||||||
title: 'Data List',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'Users',
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'users-layout-1',
|
|
||||||
title: 'Layout 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'users-layout-2',
|
|
||||||
title: 'Layout 2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'users-layout-3',
|
|
||||||
title: 'Layout 3',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'Pages',
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'Wizards',
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'wizard-layout-1',
|
|
||||||
title: 'Layout 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'wizard-layout-2',
|
|
||||||
title: 'Layout 2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'wizard-layout-3',
|
|
||||||
title: 'Layout 3',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'Blog',
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'blog-layout-1',
|
|
||||||
title: 'Layout 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'blog-layout-2',
|
|
||||||
title: 'Layout 2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'blog-layout-3',
|
|
||||||
title: 'Layout 3',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'Pricing',
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'pricing-layout-1',
|
|
||||||
title: 'Layout 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'pricing-layout-2',
|
|
||||||
title: 'Layout 2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'Invoice',
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'invoice-layout-1',
|
|
||||||
title: 'Layout 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'invoice-layout-2',
|
|
||||||
title: 'Layout 2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'FAQ',
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'faq-layout-1',
|
|
||||||
title: 'Layout 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'faq-layout-2',
|
|
||||||
title: 'Layout 2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'faq-layout-3',
|
|
||||||
title: 'Layout 3',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'UI COMPONENTS',
|
'UI COMPONENTS',
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'Base',
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'slot',
|
|
||||||
title: 'Slot',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'box',
|
|
||||||
title: 'Box',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'scroll-area',
|
|
||||||
title: 'Scroll Area',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'Navigation',
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'breadcrumb',
|
|
||||||
title: 'Breadcrumb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'menu',
|
|
||||||
title: 'Menu',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'pagination',
|
|
||||||
title: 'Pagination',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'tabs',
|
|
||||||
title: 'Tabs',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: 'CircleGauge',
|
icon: 'CircleGauge',
|
||||||
title: 'Forms',
|
title: 'Forms',
|
||||||
sub_menu: [
|
sub_menu: [
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'button',
|
|
||||||
title: 'Button',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'checkbox',
|
|
||||||
title: 'Checkbox',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: 'CircleGauge',
|
icon: 'CircleGauge',
|
||||||
route_name: 'combobox',
|
route_name: 'combobox',
|
||||||
@@ -337,21 +118,11 @@ const mainMenu: (string | Menu)[] = [
|
|||||||
route_name: 'datepicker',
|
route_name: 'datepicker',
|
||||||
title: 'Datepicker',
|
title: 'Datepicker',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'field',
|
|
||||||
title: 'Field',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: 'CircleGauge',
|
icon: 'CircleGauge',
|
||||||
route_name: 'input',
|
route_name: 'input',
|
||||||
title: 'Input',
|
title: 'Input',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'native-select',
|
|
||||||
title: 'Native Select',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: 'CircleGauge',
|
icon: 'CircleGauge',
|
||||||
route_name: 'radio-group',
|
route_name: 'radio-group',
|
||||||
@@ -379,46 +150,10 @@ const mainMenu: (string | Menu)[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
title: 'Data Display',
|
|
||||||
sub_menu: [
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'accordion',
|
|
||||||
title: 'Accordion',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'avatar',
|
|
||||||
title: 'Avatar',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'badge',
|
|
||||||
title: 'Badge',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'carousel',
|
|
||||||
title: 'Carousel',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'table',
|
|
||||||
title: 'Table',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: 'CircleGauge',
|
icon: 'CircleGauge',
|
||||||
title: 'Feedback',
|
title: 'Feedback',
|
||||||
sub_menu: [
|
sub_menu: [
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'alert',
|
|
||||||
title: 'Alert',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: 'CircleGauge',
|
icon: 'CircleGauge',
|
||||||
route_name: 'progress-circular',
|
route_name: 'progress-circular',
|
||||||
@@ -471,11 +206,6 @@ const mainMenu: (string | Menu)[] = [
|
|||||||
route_name: 'chart',
|
route_name: 'chart',
|
||||||
title: 'Chart',
|
title: 'Chart',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: 'CircleGauge',
|
|
||||||
route_name: 'map',
|
|
||||||
title: 'Map',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||||
import { logout, resolvePostAuthRoute } from '@/modules/auth'
|
import { logout, resolvePostAuthRoute } from '@/modules/auth'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import logoUrl from '@/assets/images/logo-kopkb.svg'
|
import logoUrl from '@/assets/images/logo.svg'
|
||||||
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
|
import illustrationUrl from '@/assets/images/logo.svg'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||||
import { getForgotPasswordErrorMessage, requestForgotPassword } from '@/modules/auth'
|
import { getForgotPasswordErrorMessage, requestForgotPassword } from '@/modules/auth'
|
||||||
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
|
import illustrationUrl from '@/assets/images/logo.svg'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
resolvePostAuthRoute,
|
resolvePostAuthRoute,
|
||||||
} from '@/modules/auth'
|
} from '@/modules/auth'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
|
import illustrationUrl from '@/assets/images/logo.svg'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -125,11 +125,8 @@ const appVersion = import.meta.env.VITE_APP_VERSION
|
|||||||
<CheckboxLabel>Ingat saya</CheckboxLabel>
|
<CheckboxLabel>Ingat saya</CheckboxLabel>
|
||||||
</CheckboxRoot>
|
</CheckboxRoot>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button type="button" class="opacity-70 hover:opacity-100"
|
||||||
type="button"
|
@click="router.push({ name: 'forgot-password' })">
|
||||||
class="opacity-70 hover:opacity-100"
|
|
||||||
@click="router.push({ name: 'forgot-password' })"
|
|
||||||
>
|
|
||||||
Lupa Password?
|
Lupa Password?
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import logoUrl from '@/assets/images/logo.svg'
|
import logoUrl from '@/assets/images/logo.svg'
|
||||||
import illustrationUrl from '@/assets/images/illustration.svg'
|
|
||||||
import { Box } from '@/components/ui/box'
|
import { Box } from '@/components/ui/box'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME
|
const appName = import.meta.env.VITE_APP_NAME
|
||||||
@@ -12,7 +10,7 @@ const sections = [
|
|||||||
{
|
{
|
||||||
id: 'pengenalan',
|
id: 'pengenalan',
|
||||||
title: '1. Pengenalan',
|
title: '1. Pengenalan',
|
||||||
placeholder: 'Selamat datang ke SUTERA 3.0. Kami amat menghargai kepercayaan yang anda berikan untuk mengendalikan maklumat peribadi anda. Dasar Privasi ini digubal untuk membantu anda memahami bagaimana kami mengumpul, menggunakan, mendedahkan, dan melindungi data peribadi anda selaras dengan Akta Perlindungan Data Peribadi 2010 (PDPA) dan piawaian keselamatan global.\n\nDasar ini terpakai kepada semua TDM, dan mana-mana pihak yang mengakses atau menggunakan perkhidmatan, laman web, dan aplikasi kami.'
|
placeholder: 'Selamat datang ke MyKOPKB. Kami amat menghargai kepercayaan yang anda berikan untuk mengendalikan maklumat peribadi anda. Dasar Privasi ini digubal untuk membantu anda memahami bagaimana kami mengumpul, menggunakan, mendedahkan, dan melindungi data peribadi anda selaras dengan Akta Perlindungan Data Peribadi 2010 (PDPA) dan piawaian keselamatan global.\n\nDasar ini terpakai kepada semua Koperasi Permodalan Kelantan Berhad (KOPKB), dan mana-mana pihak yang mengakses atau menggunakan perkhidmatan, laman web, dan aplikasi kami.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'data-dikumpul',
|
id: 'data-dikumpul',
|
||||||
@@ -60,7 +58,7 @@ const sections = [
|
|||||||
id: 'hubungi',
|
id: 'hubungi',
|
||||||
title: '10. Hubungi Kami',
|
title: '10. Hubungi Kami',
|
||||||
placeholder:
|
placeholder:
|
||||||
'Jika anda ada pertanyaan, komen atau permintaan tentang dasar ini, hubungi kami di\n- KAPT MOHAMMAD EFANDY BIN JAFFARI (effandy.jaffari@army.mil.my)'
|
'Jika anda ada pertanyaan, komen atau permintaan tentang dasar ini, hubungi kami di\n- ISMAIL BIN MASSERAN (ismail@koppkb.com)'
|
||||||
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||||
import { PasswordInput } from '@/components/ui/password-input'
|
import { PasswordInput } from '@/components/ui/password-input'
|
||||||
import { getRegisterErrorMessage, register } from '@/modules/auth'
|
import { getRegisterErrorMessage, register } from '@/modules/auth'
|
||||||
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
|
import illustrationUrl from '@/assets/images/logo.svg'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
requestForgotPassword,
|
requestForgotPassword,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
} from '@/modules/auth'
|
} from '@/modules/auth'
|
||||||
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
|
import illustrationUrl from '@/assets/images/logo.svg'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -133,61 +133,28 @@ const onOtpInput = (event: Event) => {
|
|||||||
</AlertRoot>
|
</AlertRoot>
|
||||||
|
|
||||||
<form class="mt-8 flex flex-col gap-5" @submit.prevent="handleReset">
|
<form class="mt-8 flex flex-col gap-5" @submit.prevent="handleReset">
|
||||||
<Input
|
<Input v-model="email" class="box block min-w-full px-5 py-6 xl:min-w-md" type="email"
|
||||||
v-model="email"
|
placeholder="Email" autocomplete="email" required />
|
||||||
class="box block min-w-full px-5 py-6 xl:min-w-md"
|
<Input :model-value="otp"
|
||||||
type="email"
|
class="box block min-w-full px-5 py-6 xl:min-w-md text-center tracking-[0.5em] text-lg" type="text"
|
||||||
placeholder="Email"
|
inputmode="numeric" pattern="[0-9]*" maxlength="6" placeholder="000000" autocomplete="one-time-code"
|
||||||
autocomplete="email"
|
required @input="onOtpInput" />
|
||||||
required
|
<PasswordInput v-model="password" class="box block min-w-full px-5 py-6 xl:min-w-md"
|
||||||
/>
|
placeholder="Kata laluan baharu" autocomplete="new-password" required />
|
||||||
<Input
|
<PasswordInput v-model="passwordConfirmation" class="box block min-w-full px-5 py-6 xl:min-w-md"
|
||||||
:model-value="otp"
|
placeholder="Sahkan kata laluan baharu" autocomplete="new-password" required />
|
||||||
class="box block min-w-full px-5 py-6 xl:min-w-md text-center tracking-[0.5em] text-lg"
|
|
||||||
type="text"
|
|
||||||
inputmode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
maxlength="6"
|
|
||||||
placeholder="000000"
|
|
||||||
autocomplete="one-time-code"
|
|
||||||
required
|
|
||||||
@input="onOtpInput"
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
v-model="password"
|
|
||||||
class="box block min-w-full px-5 py-6 xl:min-w-md"
|
|
||||||
placeholder="Kata laluan baharu"
|
|
||||||
autocomplete="new-password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
v-model="passwordConfirmation"
|
|
||||||
class="box block min-w-full px-5 py-6 xl:min-w-md"
|
|
||||||
placeholder="Sahkan kata laluan baharu"
|
|
||||||
autocomplete="new-password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="mt-5 text-center xl:mt-10 xl:text-left">
|
<div class="mt-5 text-center xl:mt-10 xl:text-left">
|
||||||
<Button class="box w-full px-4 py-5" variant="primary" type="submit"
|
<Button class="box w-full px-4 py-5" variant="primary" type="submit"
|
||||||
:disabled="loading || !canSubmit">
|
:disabled="loading || !canSubmit">
|
||||||
{{ loading ? 'Menyimpan...' : 'Tetapkan Semula Kata Laluan' }}
|
{{ loading ? 'Menyimpan...' : 'Tetapkan Semula Kata Laluan' }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button class="box mt-4 w-full px-4 py-5" look="outline" type="button"
|
||||||
class="box mt-4 w-full px-4 py-5"
|
:disabled="resendLoading || !email" @click="handleResend">
|
||||||
look="outline"
|
|
||||||
type="button"
|
|
||||||
:disabled="resendLoading || !email"
|
|
||||||
@click="handleResend"
|
|
||||||
>
|
|
||||||
{{ resendLoading ? 'Menghantar...' : 'Hantar Semula Kod OTP' }}
|
{{ resendLoading ? 'Menghantar...' : 'Hantar Semula Kod OTP' }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button class="box mt-4 w-full px-4 py-5" look="outline" type="button"
|
||||||
class="box mt-4 w-full px-4 py-5"
|
@click="router.push({ name: 'login' })">
|
||||||
look="outline"
|
|
||||||
type="button"
|
|
||||||
@click="router.push({ name: 'login' })"
|
|
||||||
>
|
|
||||||
Kembali ke Log Masuk
|
Kembali ke Log Masuk
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import logoUrl from '@/assets/images/logo.svg'
|
import logoUrl from '@/assets/images/logo.svg'
|
||||||
import { Box } from '@/components/ui/box'
|
import { Box } from '@/components/ui/box'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME
|
const appName = import.meta.env.VITE_APP_NAME
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
verifyEmail,
|
verifyEmail,
|
||||||
} from '@/modules/auth'
|
} from '@/modules/auth'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import logoUrl from '@/assets/images/logo-kopkb.svg'
|
import logoUrl from '@/assets/images/logo.svg'
|
||||||
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
|
import illustrationUrl from '@/assets/images/logo.svg'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as select from '@zag-js/select'
|
|||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { Box } from '@/components/ui/box'
|
import { Box } from '@/components/ui/box'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import {
|
import {
|
||||||
SelectRoot,
|
SelectRoot,
|
||||||
@@ -27,16 +27,19 @@ import type {
|
|||||||
MembershipApplicationHeirForm,
|
MembershipApplicationHeirForm,
|
||||||
MembershipApplicationReferenceForm,
|
MembershipApplicationReferenceForm,
|
||||||
} from '../types/membership-application.types'
|
} from '../types/membership-application.types'
|
||||||
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
|
import illustrationUrl from '@/assets/images/logo.svg'
|
||||||
|
|
||||||
const MAX_FILE_SIZE_MB = 10
|
const MAX_FILE_SIZE_MB = 10
|
||||||
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024
|
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024
|
||||||
|
const MIN_STOCK_MONTHLY_CONTRIBUTION = 50
|
||||||
|
const INITIAL_MANDATORY_STOCK_MONTHLY_CONTRIBUTION = 84
|
||||||
|
const MIN_FEE_MONTHLY_CONTRIBUTION = 30
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{ id: 1, label: 'Maklumat Peribadi' },
|
{ id: 1, label: 'Maklumat Peribadi' },
|
||||||
{ id: 2, label: 'Hubungan & Alamat' },
|
{ id: 2, label: 'Hubungan & Alamat' },
|
||||||
{ id: 3, label: 'Maklumat Pekerjaan' },
|
{ id: 3, label: 'Maklumat Pekerjaan' },
|
||||||
{ id: 4, label: 'Maklumat Waris' },
|
{ id: 4, label: 'Maklumat Penama' },
|
||||||
{ id: 5, label: 'Dokumen & Hantar' },
|
{ id: 5, label: 'Dokumen & Hantar' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
@@ -73,6 +76,31 @@ const RELATIONSHIP_OPTIONS: SelectOption[] = [
|
|||||||
{ label: 'Lain-lain', value: 'Lain-lain' },
|
{ label: 'Lain-lain', value: 'Lain-lain' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// TODO: replace with API lookup
|
||||||
|
const EMPLOYERS = [
|
||||||
|
{
|
||||||
|
name: 'INFRA QUEST SDN BHD',
|
||||||
|
address: 'Lot 1045, Jalan Dato’ Lundang, 15200 Kota Bharu, Kelantan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Permodalan Kelantan Berhad',
|
||||||
|
address: 'Permodalan Kelantan Berhad, Tingkat 4, Wisma Permodalan Kelantan Berhad, Jalan Maju, 15000 Kota Bharu Kelantan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Koperasi Permodalan Kelantan Berhad',
|
||||||
|
address: 'Lot Pt 448, Tingkat 1,Jalan Kuala Krai, Batu 3, Wakaf Che Yeh, 15150 Kota Bharu, Kelantan.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "An-Nisa'",
|
||||||
|
address: 'Jln Sultan Ibrahim, Bandar Kota Bharu, 15050 Kota Bharu, Kelantan.',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const EMPLOYER_OPTIONS: SelectOption[] = EMPLOYERS.map((employer) => ({
|
||||||
|
label: employer.name,
|
||||||
|
value: employer.name,
|
||||||
|
}))
|
||||||
|
|
||||||
function createSelectCollection(options: SelectOption[]) {
|
function createSelectCollection(options: SelectOption[]) {
|
||||||
return select.collection({
|
return select.collection({
|
||||||
items: options,
|
items: options,
|
||||||
@@ -94,6 +122,11 @@ function apiValueToLabel(options: SelectOption[], value: string | undefined): st
|
|||||||
const genderCollection = createSelectCollection(GENDER_OPTIONS)
|
const genderCollection = createSelectCollection(GENDER_OPTIONS)
|
||||||
const marriageStatusCollection = createSelectCollection(MARRIAGE_STATUS_OPTIONS)
|
const marriageStatusCollection = createSelectCollection(MARRIAGE_STATUS_OPTIONS)
|
||||||
const relationshipCollection = createSelectCollection(RELATIONSHIP_OPTIONS)
|
const relationshipCollection = createSelectCollection(RELATIONSHIP_OPTIONS)
|
||||||
|
const employerCollection = createSelectCollection(EMPLOYER_OPTIONS)
|
||||||
|
|
||||||
|
function getEmployerAddress(name: string): string {
|
||||||
|
return EMPLOYERS.find((employer) => employer.name === name)?.address ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
function createEmptyReference(): MembershipApplicationReferenceForm {
|
function createEmptyReference(): MembershipApplicationReferenceForm {
|
||||||
return {
|
return {
|
||||||
@@ -149,6 +182,7 @@ const genderInitial = computed(() => apiValueToLabel(GENDER_OPTIONS, form.applic
|
|||||||
const marriageStatusInitial = computed(() =>
|
const marriageStatusInitial = computed(() =>
|
||||||
apiValueToLabel(MARRIAGE_STATUS_OPTIONS, form.applicant.marriage_status),
|
apiValueToLabel(MARRIAGE_STATUS_OPTIONS, form.applicant.marriage_status),
|
||||||
)
|
)
|
||||||
|
const employerInitial = computed(() => apiValueToLabel(EMPLOYER_OPTIONS, form.applicant.employer_name))
|
||||||
|
|
||||||
function setGenderValue(details: { value: string[] }) {
|
function setGenderValue(details: { value: string[] }) {
|
||||||
form.applicant.gender = labelToApiValue(GENDER_OPTIONS, details.value[0])
|
form.applicant.gender = labelToApiValue(GENDER_OPTIONS, details.value[0])
|
||||||
@@ -160,6 +194,14 @@ function setMarriageStatusValue(details: { value: string[] }) {
|
|||||||
delete fieldErrors['applicant.marriage_status']
|
delete fieldErrors['applicant.marriage_status']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setEmployerValue(details: { value: string[] }) {
|
||||||
|
const employerName = labelToApiValue(EMPLOYER_OPTIONS, details.value[0])
|
||||||
|
form.applicant.employer_name = employerName
|
||||||
|
form.applicant.employer_address = getEmployerAddress(employerName)
|
||||||
|
delete fieldErrors['applicant.employer_name']
|
||||||
|
delete fieldErrors['applicant.employer_address']
|
||||||
|
}
|
||||||
|
|
||||||
function setHeirRelationshipValue(index: number, details: { value: string[] }) {
|
function setHeirRelationshipValue(index: number, details: { value: string[] }) {
|
||||||
const heir = form.heirs[index]
|
const heir = form.heirs[index]
|
||||||
if (!heir) return
|
if (!heir) return
|
||||||
@@ -254,7 +296,7 @@ const stepTitle = computed(() => {
|
|||||||
case 3:
|
case 3:
|
||||||
return 'Maklumat Pekerjaan & Caruman'
|
return 'Maklumat Pekerjaan & Caruman'
|
||||||
case 4:
|
case 4:
|
||||||
return 'Maklumat Waris'
|
return 'Maklumat Penama'
|
||||||
default:
|
default:
|
||||||
return 'Dokumen & Pengesahan'
|
return 'Dokumen & Pengesahan'
|
||||||
}
|
}
|
||||||
@@ -269,7 +311,7 @@ const stepDescription = computed(() => {
|
|||||||
case 3:
|
case 3:
|
||||||
return 'Masukkan maklumat pekerjaan dan caruman bulanan.'
|
return 'Masukkan maklumat pekerjaan dan caruman bulanan.'
|
||||||
case 4:
|
case 4:
|
||||||
return 'Tambah sekurang-kurangnya satu waris.'
|
return 'Tambah sekurang-kurangnya satu Penama.'
|
||||||
default:
|
default:
|
||||||
return 'Muat naik dokumen sokongan dan semak maklumat sebelum hantar.'
|
return 'Muat naik dokumen sokongan dan semak maklumat sebelum hantar.'
|
||||||
}
|
}
|
||||||
@@ -324,21 +366,45 @@ function validateStep(step: number): boolean {
|
|||||||
requireField(
|
requireField(
|
||||||
'applicant.stock_monthly_contribution',
|
'applicant.stock_monthly_contribution',
|
||||||
form.applicant.stock_monthly_contribution,
|
form.applicant.stock_monthly_contribution,
|
||||||
'Caruman saham bulanan',
|
'Potongan modal syer minima',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const stockContribution = Number(form.applicant.stock_monthly_contribution)
|
||||||
|
if (
|
||||||
|
form.applicant.stock_monthly_contribution &&
|
||||||
|
(Number.isNaN(stockContribution) || stockContribution < MIN_STOCK_MONTHLY_CONTRIBUTION)
|
||||||
|
) {
|
||||||
|
setError(
|
||||||
|
'applicant.stock_monthly_contribution',
|
||||||
|
`Potongan modal syer minima mestilah sekurang-kurangnya RM${MIN_STOCK_MONTHLY_CONTRIBUTION}.`,
|
||||||
|
)
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
requireField(
|
requireField(
|
||||||
'applicant.fee_monthly_contribution',
|
'applicant.fee_monthly_contribution',
|
||||||
form.applicant.fee_monthly_contribution,
|
form.applicant.fee_monthly_contribution,
|
||||||
'Caruman yuran bulanan',
|
'Potongan yuran bulanan',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const feeContribution = Number(form.applicant.fee_monthly_contribution)
|
||||||
|
if (
|
||||||
|
form.applicant.fee_monthly_contribution &&
|
||||||
|
(Number.isNaN(feeContribution) || feeContribution < MIN_FEE_MONTHLY_CONTRIBUTION)
|
||||||
|
) {
|
||||||
|
setError(
|
||||||
|
'applicant.fee_monthly_contribution',
|
||||||
|
`Potongan yuran bulanan mestilah sekurang-kurangnya RM${MIN_FEE_MONTHLY_CONTRIBUTION}.`,
|
||||||
|
)
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step === 4) {
|
if (step === 4) {
|
||||||
form.heirs.forEach((heir, index) => {
|
form.heirs.forEach((heir, index) => {
|
||||||
requireField(`heirs.${index}.name`, heir.name, `Nama waris ${index + 1}`)
|
requireField(`heirs.${index}.name`, heir.name, `Nama Penama ${index + 1}`)
|
||||||
requireField(`heirs.${index}.ic_number`, heir.ic_number, `No. KP waris ${index + 1}`)
|
requireField(`heirs.${index}.ic_number`, heir.ic_number, `No. KP Penama ${index + 1}`)
|
||||||
requireField(`heirs.${index}.relationship`, heir.relationship, `Hubungan waris ${index + 1}`)
|
requireField(`heirs.${index}.relationship`, heir.relationship, `Hubungan Penama ${index + 1}`)
|
||||||
requireField(`heirs.${index}.phone_number`, heir.phone_number, `No. telefon waris ${index + 1}`)
|
requireField(`heirs.${index}.phone_number`, heir.phone_number, `No. telefon Penama ${index + 1}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,6 +421,11 @@ function validateStep(step: number): boolean {
|
|||||||
setError('documents.ic_copy', 'Salinan kad pengenalan diperlukan.')
|
setError('documents.ic_copy', 'Salinan kad pengenalan diperlukan.')
|
||||||
valid = false
|
valid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!form.documents.employer_letter) {
|
||||||
|
setError('documents.employer_letter', 'Surat pengesahan majikan diperlukan.')
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return valid
|
return valid
|
||||||
@@ -638,8 +709,23 @@ function stepLabelClass(stepId: number) {
|
|||||||
|
|
||||||
<template v-else-if="currentStep === 3">
|
<template v-else-if="currentStep === 3">
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
<Field class="col-span-12 sm:col-span-6">
|
||||||
<FieldLabel for="employer_name">Nama Majikan</FieldLabel>
|
<FieldLabel>Nama Majikan</FieldLabel>
|
||||||
<Input id="employer_name" v-model="form.applicant.employer_name" type="text" />
|
<SelectRoot :key="`employer-${form.applicant.employer_name}`" class="w-full"
|
||||||
|
:collection="employerCollection" :default-value="employerInitial" @value-change="setEmployerValue">
|
||||||
|
<SelectControl>
|
||||||
|
<SelectTrigger :aria-invalid="!!fieldErrors['applicant.employer_name']">
|
||||||
|
<SelectValueText placeholder="Pilih majikan" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</SelectControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItemGroup>
|
||||||
|
<SelectItemGroupLabel>Nama Majikan</SelectItemGroupLabel>
|
||||||
|
<SelectItem v-for="item in employerCollection.items" :key="item.label" :item="item">
|
||||||
|
<SelectItemText>{{ item.label }}</SelectItemText>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectItemGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</SelectRoot>
|
||||||
<FieldError v-if="fieldErrors['applicant.employer_name']">
|
<FieldError v-if="fieldErrors['applicant.employer_name']">
|
||||||
{{ fieldErrors['applicant.employer_name'] }}
|
{{ fieldErrors['applicant.employer_name'] }}
|
||||||
</FieldError>
|
</FieldError>
|
||||||
@@ -653,7 +739,7 @@ function stepLabelClass(stepId: number) {
|
|||||||
</Field>
|
</Field>
|
||||||
<Field class="col-span-12">
|
<Field class="col-span-12">
|
||||||
<FieldLabel for="employer_address">Alamat Majikan</FieldLabel>
|
<FieldLabel for="employer_address">Alamat Majikan</FieldLabel>
|
||||||
<Textarea id="employer_address" v-model="form.applicant.employer_address" rows="3" />
|
<Textarea id="employer_address" v-model="form.applicant.employer_address" rows="3" disabled />
|
||||||
<FieldError v-if="fieldErrors['applicant.employer_address']">
|
<FieldError v-if="fieldErrors['applicant.employer_address']">
|
||||||
{{ fieldErrors['applicant.employer_address'] }}
|
{{ fieldErrors['applicant.employer_address'] }}
|
||||||
</FieldError>
|
</FieldError>
|
||||||
@@ -666,17 +752,27 @@ function stepLabelClass(stepId: number) {
|
|||||||
</FieldError>
|
</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field class="col-span-12 sm:col-span-4">
|
<Field class="col-span-12 sm:col-span-4">
|
||||||
<FieldLabel for="stock_monthly_contribution">Caruman Saham (RM)</FieldLabel>
|
<FieldLabel for="stock_monthly_contribution">Potongan Modal Syer Minima</FieldLabel>
|
||||||
|
<FieldDescription>
|
||||||
|
Minima RM{{ MIN_STOCK_MONTHLY_CONTRIBUTION }} setiap bulan.
|
||||||
|
</FieldDescription>
|
||||||
<Input id="stock_monthly_contribution" v-model="form.applicant.stock_monthly_contribution" type="number"
|
<Input id="stock_monthly_contribution" v-model="form.applicant.stock_monthly_contribution" type="number"
|
||||||
min="0" step="0.01" />
|
:min="MIN_STOCK_MONTHLY_CONTRIBUTION" step="0.01" />
|
||||||
|
<FieldDescription class="mt-2">
|
||||||
|
Potongan RM{{ INITIAL_MANDATORY_STOCK_MONTHLY_CONTRIBUTION }} setiap bulan adalah wajib bagi 6 bulan
|
||||||
|
pertama bagi menjelaskan modal syer minimum RM500. Anda boleh memilih potongan lebih tinggi. Selepas
|
||||||
|
RM500 dijelaskan, potongan boleh dikekalkan atau dikurangkan sehingga minima
|
||||||
|
RM{{ MIN_STOCK_MONTHLY_CONTRIBUTION }} setiap bulan.
|
||||||
|
</FieldDescription>
|
||||||
<FieldError v-if="fieldErrors['applicant.stock_monthly_contribution']">
|
<FieldError v-if="fieldErrors['applicant.stock_monthly_contribution']">
|
||||||
{{ fieldErrors['applicant.stock_monthly_contribution'] }}
|
{{ fieldErrors['applicant.stock_monthly_contribution'] }}
|
||||||
</FieldError>
|
</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field class="col-span-12 sm:col-span-4">
|
<Field class="col-span-12 sm:col-span-4">
|
||||||
<FieldLabel for="fee_monthly_contribution">Caruman Yuran (RM)</FieldLabel>
|
<FieldLabel for="fee_monthly_contribution">Potongan Yuran Bulanan</FieldLabel>
|
||||||
|
<FieldDescription>Minima RM{{ MIN_FEE_MONTHLY_CONTRIBUTION }} setiap bulan.</FieldDescription>
|
||||||
<Input id="fee_monthly_contribution" v-model="form.applicant.fee_monthly_contribution" type="number"
|
<Input id="fee_monthly_contribution" v-model="form.applicant.fee_monthly_contribution" type="number"
|
||||||
min="0" step="0.01" />
|
:min="MIN_FEE_MONTHLY_CONTRIBUTION" step="0.01" />
|
||||||
<FieldError v-if="fieldErrors['applicant.fee_monthly_contribution']">
|
<FieldError v-if="fieldErrors['applicant.fee_monthly_contribution']">
|
||||||
{{ fieldErrors['applicant.fee_monthly_contribution'] }}
|
{{ fieldErrors['applicant.fee_monthly_contribution'] }}
|
||||||
</FieldError>
|
</FieldError>
|
||||||
@@ -688,7 +784,7 @@ function stepLabelClass(stepId: number) {
|
|||||||
<div v-for="(heir, index) in form.heirs" :key="index"
|
<div v-for="(heir, index) in form.heirs" :key="index"
|
||||||
class="rounded-lg border border-foreground/10 p-4">
|
class="rounded-lg border border-foreground/10 p-4">
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<div class="font-medium">Waris {{ index + 1 }}</div>
|
<div class="font-medium">Penama</div>
|
||||||
<Button v-if="form.heirs.length > 1" type="button" look="outline" size="sm"
|
<Button v-if="form.heirs.length > 1" type="button" look="outline" size="sm"
|
||||||
@click="removeHeir(index)">
|
@click="removeHeir(index)">
|
||||||
Buang
|
Buang
|
||||||
@@ -742,10 +838,11 @@ function stepLabelClass(stepId: number) {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" look="outline" @click="addHeir">
|
<!-- Penama hanya boleh 1 orang (disable in frontend)-->
|
||||||
|
<!-- <Button type="button" look="outline" @click="addHeir">
|
||||||
<Lucide icon="Plus" class="mr-2 size-4" />
|
<Lucide icon="Plus" class="mr-2 size-4" />
|
||||||
Tambah Waris
|
Tambah Penama
|
||||||
</Button>
|
</Button> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -806,7 +903,7 @@ function stepLabelClass(stepId: number) {
|
|||||||
</FieldError>
|
</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
<Field class="col-span-12 sm:col-span-6">
|
||||||
<FieldLabel for="employer_letter">Surat Pengesahan Majikan (Pilihan)</FieldLabel>
|
<FieldLabel for="employer_letter">Surat Pengesahan Majikan *</FieldLabel>
|
||||||
<Input id="employer_letter" type="file" accept=".pdf,.jpg,.jpeg,.png"
|
<Input id="employer_letter" type="file" accept=".pdf,.jpg,.jpeg,.png"
|
||||||
@change="handleFileChange('employer_letter', $event)" />
|
@change="handleFileChange('employer_letter', $event)" />
|
||||||
<FieldError v-if="fieldErrors['documents.employer_letter']">{{ fieldErrors['documents.employer_letter']
|
<FieldError v-if="fieldErrors['documents.employer_letter']">{{ fieldErrors['documents.employer_letter']
|
||||||
@@ -820,7 +917,7 @@ function stepLabelClass(stepId: number) {
|
|||||||
<div><span class="opacity-70">Emel:</span> {{ form.applicant.email }}</div>
|
<div><span class="opacity-70">Emel:</span> {{ form.applicant.email }}</div>
|
||||||
<div><span class="opacity-70">No. KP:</span> {{ form.applicant.ic_number }}</div>
|
<div><span class="opacity-70">No. KP:</span> {{ form.applicant.ic_number }}</div>
|
||||||
<div><span class="opacity-70">Majikan:</span> {{ form.applicant.employer_name }}</div>
|
<div><span class="opacity-70">Majikan:</span> {{ form.applicant.employer_name }}</div>
|
||||||
<div><span class="opacity-70">Bil. Waris:</span> {{ form.heirs.length }}</div>
|
<div><span class="opacity-70">Bil. Penama:</span> {{ form.heirs.length }}</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="opacity-70">Pencadang:</span>
|
<span class="opacity-70">Pencadang:</span>
|
||||||
{{ form.references.proposer.name || '-' }}
|
{{ form.references.proposer.name || '-' }}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { CircleAlert, CircleCheck, Download, Eye, FileText, Pencil } from '@lucide/vue'
|
import { CircleAlert, CircleCheck, Download, Eye, Pencil, Trash2 } from '@lucide/vue'
|
||||||
import {
|
import {
|
||||||
AlertRoot,
|
AlertRoot,
|
||||||
AlertTitle,
|
AlertTitle,
|
||||||
@@ -21,9 +21,9 @@ import { usePermissions } from '@/composables/usePermissions'
|
|||||||
import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage'
|
import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage'
|
||||||
import {
|
import {
|
||||||
completeMembershipApplication,
|
completeMembershipApplication,
|
||||||
|
deleteMembershipApplicationDocument,
|
||||||
downloadMembershipApplicationDocument,
|
downloadMembershipApplicationDocument,
|
||||||
fetchMembershipApplicationDocument,
|
fetchMembershipApplicationDocument,
|
||||||
generateMembershipApplicationResultLetter,
|
|
||||||
getMembershipApplication,
|
getMembershipApplication,
|
||||||
submitBoardReview,
|
submitBoardReview,
|
||||||
submitManagementReview,
|
submitManagementReview,
|
||||||
@@ -43,12 +43,15 @@ import type {
|
|||||||
MembershipApplicationReviewDetail,
|
MembershipApplicationReviewDetail,
|
||||||
MembershipApplicationStatus,
|
MembershipApplicationStatus,
|
||||||
} from '../types/membership-application.types'
|
} from '../types/membership-application.types'
|
||||||
import { RESULT_LETTER_DOCUMENT_TYPE } from '../types/membership-application.types'
|
import {
|
||||||
|
ADMIN_ATTACHMENT_DOCUMENT_TYPE,
|
||||||
|
RESULT_LETTER_DOCUMENT_TYPE,
|
||||||
|
} from '../types/membership-application.types'
|
||||||
|
|
||||||
const WORKFLOW_STEPS = [
|
const WORKFLOW_STEPS = [
|
||||||
{ id: 1, label: 'Dihantar' },
|
{ id: 1, label: 'Dihantar' },
|
||||||
{ id: 2, label: 'Semakan Pentadbiran' },
|
{ id: 2, label: 'Semakan Pentadbiran' },
|
||||||
{ id: 3, label: 'Semakan Lembaga' },
|
{ id: 3, label: 'Keputusan Ahli Lembaga Koperasi (ALK)' },
|
||||||
{ id: 4, label: 'Makluman Keputusan' },
|
{ id: 4, label: 'Makluman Keputusan' },
|
||||||
{ id: 5, label: 'Selesai' },
|
{ id: 5, label: 'Selesai' },
|
||||||
] as const
|
] as const
|
||||||
@@ -58,6 +61,7 @@ const DOCUMENT_TYPE_LABELS: Record<string, string> = {
|
|||||||
photo: 'Gambar Passport',
|
photo: 'Gambar Passport',
|
||||||
salary_slip: 'Slip Gaji',
|
salary_slip: 'Slip Gaji',
|
||||||
employer_letter: 'Surat Pengesahan Majikan',
|
employer_letter: 'Surat Pengesahan Majikan',
|
||||||
|
admin_attachment: 'Lampiran Pentadbir',
|
||||||
[RESULT_LETTER_DOCUMENT_TYPE]: 'Surat Keputusan',
|
[RESULT_LETTER_DOCUMENT_TYPE]: 'Surat Keputusan',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,19 +86,20 @@ const pendingAction = ref<
|
|||||||
| null
|
| null
|
||||||
>(null)
|
>(null)
|
||||||
const downloadingDocumentId = ref<string | null>(null)
|
const downloadingDocumentId = ref<string | null>(null)
|
||||||
|
const deletingDocumentId = ref<string | null>(null)
|
||||||
const previewOpen = ref(false)
|
const previewOpen = ref(false)
|
||||||
const previewLoading = ref(false)
|
const previewLoading = ref(false)
|
||||||
const previewUrl = ref<string | null>(null)
|
const previewUrl = ref<string | null>(null)
|
||||||
const previewDocument = ref<MembershipApplicationDocumentDetail | null>(null)
|
const previewDocument = ref<MembershipApplicationDocumentDetail | null>(null)
|
||||||
const generateDialogOpen = ref(false)
|
|
||||||
const generateSubmitting = ref(false)
|
|
||||||
const boardMeetingReference = ref('')
|
const boardMeetingReference = ref('')
|
||||||
const boardMeetingReferenceError = ref<string | null>(null)
|
const boardMeetingReferenceError = ref<string | null>(null)
|
||||||
|
const deleteConfirmDialogOpen = ref(false)
|
||||||
|
const pendingDeleteDocument = ref<MembershipApplicationDocumentDetail | null>(null)
|
||||||
|
|
||||||
function statusLabel(status: MembershipApplicationStatus): string {
|
function statusLabel(status: MembershipApplicationStatus): string {
|
||||||
const labels: Record<MembershipApplicationStatus, string> = {
|
const labels: Record<MembershipApplicationStatus, string> = {
|
||||||
SUBMITTED: 'Dihantar',
|
SUBMITTED: 'Dihantar',
|
||||||
PENDING_BOARD: 'Menunggu Lembaga',
|
PENDING_BOARD: 'Menunggu Keputusan Mesyuarat ALK',
|
||||||
MANAGEMENT_REJECTED: 'Ditolak Pentadbiran',
|
MANAGEMENT_REJECTED: 'Ditolak Pentadbiran',
|
||||||
PENDING_NOTIFICATION: 'Menunggu Makluman',
|
PENDING_NOTIFICATION: 'Menunggu Makluman',
|
||||||
COMPLETED: 'Selesai',
|
COMPLETED: 'Selesai',
|
||||||
@@ -142,16 +147,14 @@ const showCompleteAction = computed(
|
|||||||
application.value?.status === 'PENDING_NOTIFICATION',
|
application.value?.status === 'PENDING_NOTIFICATION',
|
||||||
)
|
)
|
||||||
|
|
||||||
const resultLetterDocument = computed(() =>
|
const canEditAdminAttachments = computed(
|
||||||
application.value?.documents.find((document) => document.type === RESULT_LETTER_DOCUMENT_TYPE) ?? null,
|
() =>
|
||||||
|
hasPermission('kemaskini permohonan keahlian') &&
|
||||||
|
application.value?.status !== 'COMPLETED',
|
||||||
)
|
)
|
||||||
|
|
||||||
const showGenerateResultLetter = computed(
|
const resultLetterDocument = computed(() =>
|
||||||
() =>
|
application.value?.documents.find((document) => document.type === RESULT_LETTER_DOCUMENT_TYPE) ?? null,
|
||||||
hasPermission('jana surat keputusan keahlian') &&
|
|
||||||
application.value?.status === 'COMPLETED' &&
|
|
||||||
!resultLetterDocument.value &&
|
|
||||||
(application.value.board_result === 'PASS' || application.value.board_result === 'FAIL'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const applicant = computed(() => application.value?.applicant ?? null)
|
const applicant = computed(() => application.value?.applicant ?? null)
|
||||||
@@ -167,8 +170,8 @@ const confirmDialogTitle = computed(() => {
|
|||||||
|
|
||||||
if (pendingAction.value.type === 'board') {
|
if (pendingAction.value.type === 'board') {
|
||||||
return pendingAction.value.decision === 'PASS'
|
return pendingAction.value.decision === 'PASS'
|
||||||
? 'Luluskan Semakan Lembaga?'
|
? 'Luluskan?'
|
||||||
: 'Gagalkan Semakan Lembaga?'
|
: 'Gagalkan?'
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Selesaikan Permohonan?'
|
return 'Selesaikan Permohonan?'
|
||||||
@@ -179,17 +182,17 @@ const confirmDialogDescription = computed(() => {
|
|||||||
|
|
||||||
if (pendingAction.value.type === 'management') {
|
if (pendingAction.value.type === 'management') {
|
||||||
return pendingAction.value.decision === 'APPROVED'
|
return pendingAction.value.decision === 'APPROVED'
|
||||||
? 'Permohonan akan dihantar ke semakan lembaga.'
|
? 'Permohonan akan dihantar ke mesyuarat ALK.'
|
||||||
: 'Permohonan akan ditolak pada peringkat pentadbiran.'
|
: 'Permohonan akan ditolak pada peringkat pentadbiran.'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingAction.value.type === 'board') {
|
if (pendingAction.value.type === 'board') {
|
||||||
return pendingAction.value.decision === 'PASS'
|
return pendingAction.value.decision === 'PASS'
|
||||||
? 'Permohonan akan dihantar ke peringkat makluman keputusan.'
|
? 'Permohonan akan dihantar ke peringkat makluman keputusan.'
|
||||||
: 'Permohonan akan ditandakan gagal semakan lembaga.'
|
: 'Permohonan akan ditandakan gagal mesyuarat ALK.'
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'E-mel keputusan akan dihantar kepada pemohon. Akaun ahli akan dicipta jika permohonan lulus.'
|
return 'Surat keputusan akan dijana, dilampirkan dalam e-mel keputusan, dan dihantar kepada pemohon. Akaun ahli akan dicipta jika permohonan lulus.'
|
||||||
})
|
})
|
||||||
|
|
||||||
function workflowStepButtonClass(stepId: number) {
|
function workflowStepButtonClass(stepId: number) {
|
||||||
@@ -242,6 +245,14 @@ const uploadedDocuments = computed(() =>
|
|||||||
application.value?.documents.filter((document) => document.type !== RESULT_LETTER_DOCUMENT_TYPE) ?? [],
|
application.value?.documents.filter((document) => document.type !== RESULT_LETTER_DOCUMENT_TYPE) ?? [],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const applicantDocuments = computed(() =>
|
||||||
|
uploadedDocuments.value.filter((document) => document.type !== ADMIN_ATTACHMENT_DOCUMENT_TYPE),
|
||||||
|
)
|
||||||
|
|
||||||
|
const adminAttachments = computed(() =>
|
||||||
|
uploadedDocuments.value.filter((document) => document.type === ADMIN_ATTACHMENT_DOCUMENT_TYPE),
|
||||||
|
)
|
||||||
|
|
||||||
function displayValue(value: string | number | null | undefined): string {
|
function displayValue(value: string | number | null | undefined): string {
|
||||||
if (value === null || value === undefined || value === '') return '-'
|
if (value === null || value === undefined || value === '') return '-'
|
||||||
return String(value)
|
return String(value)
|
||||||
@@ -309,7 +320,7 @@ function getReference(type: 'PROPOSER' | 'SUPPORTER'): MembershipApplicationRefe
|
|||||||
|
|
||||||
function reviewStageLabel(stage: string): string {
|
function reviewStageLabel(stage: string): string {
|
||||||
if (stage === 'MANAGEMENT') return 'Semakan Pentadbiran'
|
if (stage === 'MANAGEMENT') return 'Semakan Pentadbiran'
|
||||||
if (stage === 'BOARD') return 'Semakan Lembaga'
|
if (stage === 'BOARD') return 'Keputusan Ahli Lembaga Koperasi (ALK)'
|
||||||
return stage
|
return stage
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +369,11 @@ function openConfirmAction(
|
|||||||
| { type: 'board'; decision: BoardReviewDecision }
|
| { type: 'board'; decision: BoardReviewDecision }
|
||||||
| { type: 'complete' },
|
| { type: 'complete' },
|
||||||
) {
|
) {
|
||||||
|
if (action.type === 'complete') {
|
||||||
|
boardMeetingReference.value = ''
|
||||||
|
boardMeetingReferenceError.value = null
|
||||||
|
}
|
||||||
|
|
||||||
pendingAction.value = action
|
pendingAction.value = action
|
||||||
confirmDialogOpen.value = true
|
confirmDialogOpen.value = true
|
||||||
}
|
}
|
||||||
@@ -365,6 +381,8 @@ function openConfirmAction(
|
|||||||
function closeConfirmDialog() {
|
function closeConfirmDialog() {
|
||||||
confirmDialogOpen.value = false
|
confirmDialogOpen.value = false
|
||||||
pendingAction.value = null
|
pendingAction.value = null
|
||||||
|
boardMeetingReference.value = ''
|
||||||
|
boardMeetingReferenceError.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmPendingAction() {
|
async function confirmPendingAction() {
|
||||||
@@ -372,8 +390,14 @@ async function confirmPendingAction() {
|
|||||||
|
|
||||||
error.value = null
|
error.value = null
|
||||||
successMessage.value = null
|
successMessage.value = null
|
||||||
|
boardMeetingReferenceError.value = null
|
||||||
|
|
||||||
if (pendingAction.value.type === 'complete') {
|
if (pendingAction.value.type === 'complete') {
|
||||||
|
const reference = boardMeetingReference.value.trim()
|
||||||
|
if (!reference) {
|
||||||
|
boardMeetingReferenceError.value = 'Rujukan mesyuarat lembaga diperlukan.'
|
||||||
|
return
|
||||||
|
}
|
||||||
completeSubmitting.value = true
|
completeSubmitting.value = true
|
||||||
} else {
|
} else {
|
||||||
reviewSubmitting.value = true
|
reviewSubmitting.value = true
|
||||||
@@ -395,7 +419,9 @@ async function confirmPendingAction() {
|
|||||||
})
|
})
|
||||||
boardRemarks.value = ''
|
boardRemarks.value = ''
|
||||||
} else {
|
} else {
|
||||||
response = await completeMembershipApplication(applicationId.value)
|
response = await completeMembershipApplication(applicationId.value, {
|
||||||
|
board_meeting_reference: boardMeetingReference.value.trim(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
application.value = response.data
|
application.value = response.data
|
||||||
@@ -407,55 +433,15 @@ async function confirmPendingAction() {
|
|||||||
if (validationErrors?.remarks?.[0]) {
|
if (validationErrors?.remarks?.[0]) {
|
||||||
error.value = validationErrors.remarks[0]
|
error.value = validationErrors.remarks[0]
|
||||||
}
|
}
|
||||||
|
if (validationErrors?.board_meeting_reference?.[0]) {
|
||||||
|
boardMeetingReferenceError.value = validationErrors.board_meeting_reference[0]
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
reviewSubmitting.value = false
|
reviewSubmitting.value = false
|
||||||
completeSubmitting.value = false
|
completeSubmitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openGenerateDialog() {
|
|
||||||
boardMeetingReference.value = ''
|
|
||||||
boardMeetingReferenceError.value = null
|
|
||||||
generateDialogOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeGenerateDialog() {
|
|
||||||
if (generateSubmitting.value) return
|
|
||||||
generateDialogOpen.value = false
|
|
||||||
boardMeetingReference.value = ''
|
|
||||||
boardMeetingReferenceError.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmGenerateLetter() {
|
|
||||||
if (!application.value || generateSubmitting.value) return
|
|
||||||
|
|
||||||
const reference = boardMeetingReference.value.trim()
|
|
||||||
if (!reference) {
|
|
||||||
boardMeetingReferenceError.value = 'Rujukan mesyuarat lembaga diperlukan.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
generateSubmitting.value = true
|
|
||||||
boardMeetingReferenceError.value = null
|
|
||||||
error.value = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await generateMembershipApplicationResultLetter(application.value.id, {
|
|
||||||
board_meeting_reference: reference,
|
|
||||||
})
|
|
||||||
|
|
||||||
application.value = response.data.application
|
|
||||||
successMessage.value = response.message
|
|
||||||
closeGenerateDialog()
|
|
||||||
} catch (err) {
|
|
||||||
const validationErrors = getApiValidationErrors(err)
|
|
||||||
boardMeetingReferenceError.value = validationErrors?.board_meeting_reference?.[0] ?? null
|
|
||||||
error.value = getApiErrorMessage(err, 'Gagal menjana surat keputusan.')
|
|
||||||
} finally {
|
|
||||||
generateSubmitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDownloadDocument(document: MembershipApplicationDocumentDetail) {
|
async function handleDownloadDocument(document: MembershipApplicationDocumentDetail) {
|
||||||
if (!application.value || downloadingDocumentId.value) return
|
if (!application.value || downloadingDocumentId.value) return
|
||||||
|
|
||||||
@@ -475,6 +461,43 @@ async function handleDownloadDocument(document: MembershipApplicationDocumentDet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeleteAdminAttachment(document: MembershipApplicationDocumentDetail) {
|
||||||
|
if (!application.value || !canEditAdminAttachments.value || deletingDocumentId.value) return
|
||||||
|
|
||||||
|
pendingDeleteDocument.value = document
|
||||||
|
deleteConfirmDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteConfirmDialog() {
|
||||||
|
deleteConfirmDialogOpen.value = false
|
||||||
|
pendingDeleteDocument.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDeleteAdminAttachment() {
|
||||||
|
const document = pendingDeleteDocument.value
|
||||||
|
if (!application.value || !document || deletingDocumentId.value) return
|
||||||
|
|
||||||
|
deletingDocumentId.value = document.id
|
||||||
|
error.value = null
|
||||||
|
successMessage.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await deleteMembershipApplicationDocument(application.value.id, document.id)
|
||||||
|
application.value = response.data
|
||||||
|
|
||||||
|
if (previewDocument.value?.id === document.id) {
|
||||||
|
handlePreviewOpenChange(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
successMessage.value = response.message || 'Lampiran pentadbir berjaya dipadam.'
|
||||||
|
closeDeleteConfirmDialog()
|
||||||
|
} catch (err) {
|
||||||
|
error.value = getApiErrorMessage(err, 'Gagal memadam lampiran pentadbir.')
|
||||||
|
} finally {
|
||||||
|
deletingDocumentId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleViewDocument(document: MembershipApplicationDocumentDetail) {
|
async function handleViewDocument(document: MembershipApplicationDocumentDetail) {
|
||||||
if (!application.value) return
|
if (!application.value) return
|
||||||
|
|
||||||
@@ -649,51 +672,31 @@ onUnmounted(() => {
|
|||||||
<Box v-if="showCompleteAction" class="p-5 sm:p-6">
|
<Box v-if="showCompleteAction" class="p-5 sm:p-6">
|
||||||
<div class="font-medium">Makluman Keputusan</div>
|
<div class="font-medium">Makluman Keputusan</div>
|
||||||
<p class="mt-1 text-sm opacity-70">
|
<p class="mt-1 text-sm opacity-70">
|
||||||
Hantar e-mel keputusan kepada pemohon
|
Jana surat keputusan, hantar e-mel dengan lampiran surat kepada pemohon
|
||||||
<span v-if="application.board_result === 'PASS'"> dan cipta akaun ahli</span>.
|
<span v-if="application.board_result === 'PASS'">, dan cipta akaun ahli</span>.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
<Button type="button" variant="primary" :disabled="reviewSubmitting || completeSubmitting"
|
<Button type="button" variant="primary" :disabled="reviewSubmitting || completeSubmitting"
|
||||||
@click="openConfirmAction({ type: 'complete' })">
|
@click="openConfirmAction({ type: 'complete' })">
|
||||||
Selesaikan & Hantar Makluman
|
Selesaikan, Jana Surat & Hantar E-mel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box v-if="showGenerateResultLetter" class="p-5 sm:p-6">
|
<Box v-if="resultLetterDocument" class="p-5 sm:p-6">
|
||||||
<div class="font-medium">Surat Keputusan</div>
|
|
||||||
<p class="mt-1 text-sm opacity-70">
|
|
||||||
Jana surat keputusan lembaga untuk permohonan ini. Surat hanya boleh dijana sekali.
|
|
||||||
</p>
|
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
|
||||||
<Button type="button" variant="primary" @click="openGenerateDialog">
|
|
||||||
<FileText class="mr-2 size-4" />
|
|
||||||
Jana Surat Keputusan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box v-else-if="resultLetterDocument" class="p-5 sm:p-6">
|
|
||||||
<div class="font-medium">Surat Keputusan</div>
|
<div class="font-medium">Surat Keputusan</div>
|
||||||
<p class="mt-1 text-sm opacity-70">
|
<p class="mt-1 text-sm opacity-70">
|
||||||
{{ resultLetterDocument.name }} · {{ formatFileSize(resultLetterDocument.file_size) }}
|
{{ resultLetterDocument.name }} · {{ formatFileSize(resultLetterDocument.file_size) }}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
<Button
|
<Button type="button" look="outline"
|
||||||
type="button"
|
|
||||||
look="outline"
|
|
||||||
:disabled="previewLoading && previewDocument?.id === resultLetterDocument.id"
|
:disabled="previewLoading && previewDocument?.id === resultLetterDocument.id"
|
||||||
@click="handleViewDocument(resultLetterDocument)"
|
@click="handleViewDocument(resultLetterDocument)">
|
||||||
>
|
|
||||||
<Eye class="mr-2 size-4" />
|
<Eye class="mr-2 size-4" />
|
||||||
Lihat
|
Lihat
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="button" look="outline" :disabled="downloadingDocumentId === resultLetterDocument.id"
|
||||||
type="button"
|
@click="handleDownloadDocument(resultLetterDocument)">
|
||||||
look="outline"
|
|
||||||
:disabled="downloadingDocumentId === resultLetterDocument.id"
|
|
||||||
@click="handleDownloadDocument(resultLetterDocument)"
|
|
||||||
>
|
|
||||||
<Download class="mr-2 size-4" />
|
<Download class="mr-2 size-4" />
|
||||||
{{ downloadingDocumentId === resultLetterDocument.id ? 'Memuat turun...' : 'Muat Turun' }}
|
{{ downloadingDocumentId === resultLetterDocument.id ? 'Memuat turun...' : 'Muat Turun' }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -722,7 +725,7 @@ onUnmounted(() => {
|
|||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
||||||
value="heirs">
|
value="heirs">
|
||||||
Waris
|
Penama
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
||||||
@@ -834,7 +837,7 @@ onUnmounted(() => {
|
|||||||
<div v-else class="space-y-4">
|
<div v-else class="space-y-4">
|
||||||
<div v-for="(heir, index) in application.heirs" :key="heir.id"
|
<div v-for="(heir, index) in application.heirs" :key="heir.id"
|
||||||
class="rounded-lg border border-foreground/10 p-4">
|
class="rounded-lg border border-foreground/10 p-4">
|
||||||
<div class="mb-4 font-medium">Waris {{ index + 1 }}</div>
|
<div class="mb-4 font-medium">Penama</div>
|
||||||
<div class="grid grid-cols-12 gap-4">
|
<div class="grid grid-cols-12 gap-4">
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
<Field class="col-span-12 sm:col-span-6">
|
||||||
<FieldLabel>Nama</FieldLabel>
|
<FieldLabel>Nama</FieldLabel>
|
||||||
@@ -890,30 +893,62 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<TabsContent value="documents" class="mt-6">
|
<TabsContent value="documents" class="mt-6">
|
||||||
<div v-if="!uploadedDocuments.length" class="opacity-70">Tiada dokumen dimuat naik.</div>
|
<div v-if="!uploadedDocuments.length" class="opacity-70">Tiada dokumen dimuat naik.</div>
|
||||||
<div v-else class="space-y-3">
|
<template v-else>
|
||||||
<div v-for="document in uploadedDocuments" :key="document.id"
|
<div v-if="applicantDocuments.length" class="space-y-3">
|
||||||
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-foreground/10 p-4">
|
<div class="font-medium">Dokumen Pemohon</div>
|
||||||
<div>
|
<div v-for="document in applicantDocuments" :key="document.id"
|
||||||
<div class="font-medium">{{ documentLabel(document.type, document.name) }}</div>
|
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-foreground/10 p-4">
|
||||||
<div class="mt-1 text-sm opacity-70">
|
<div>
|
||||||
{{ document.name }} · {{ formatFileSize(document.file_size) }}
|
<div class="font-medium">{{ documentLabel(document.type, document.name) }}</div>
|
||||||
|
<div class="mt-1 text-sm opacity-70">
|
||||||
|
{{ document.name }} · {{ formatFileSize(document.file_size) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Button type="button" look="outline" size="sm"
|
||||||
|
:disabled="previewLoading && previewDocument?.id === document.id"
|
||||||
|
@click="handleViewDocument(document)">
|
||||||
|
<Eye class="mr-2 size-4" />
|
||||||
|
{{ previewLoading && previewDocument?.id === document.id ? 'Memuatkan...' : 'Lihat' }}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" look="outline" size="sm" :disabled="downloadingDocumentId === document.id"
|
||||||
|
@click="handleDownloadDocument(document)">
|
||||||
|
<Download class="mr-2 size-4" />
|
||||||
|
{{ downloadingDocumentId === document.id ? 'Memuat turun...' : 'Muat Turun' }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
</div>
|
||||||
<Button type="button" look="outline" size="sm"
|
|
||||||
:disabled="previewLoading && previewDocument?.id === document.id"
|
<div v-if="adminAttachments.length" class="mt-8 space-y-3">
|
||||||
@click="handleViewDocument(document)">
|
<div class="font-medium">Lampiran Pentadbir</div>
|
||||||
<Eye class="mr-2 size-4" />
|
<div v-for="document in adminAttachments" :key="document.id"
|
||||||
{{ previewLoading && previewDocument?.id === document.id ? 'Memuatkan...' : 'Lihat' }}
|
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-foreground/10 p-4">
|
||||||
</Button>
|
<div>
|
||||||
<Button type="button" look="outline" size="sm" :disabled="downloadingDocumentId === document.id"
|
<div class="font-medium">{{ document.name }}</div>
|
||||||
@click="handleDownloadDocument(document)">
|
<div class="mt-1 text-sm opacity-70">{{ formatFileSize(document.file_size) }}</div>
|
||||||
<Download class="mr-2 size-4" />
|
</div>
|
||||||
{{ downloadingDocumentId === document.id ? 'Memuat turun...' : 'Muat Turun' }}
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
</Button>
|
<Button type="button" look="outline" size="sm"
|
||||||
|
:disabled="previewLoading && previewDocument?.id === document.id"
|
||||||
|
@click="handleViewDocument(document)">
|
||||||
|
<Eye class="mr-2 size-4" />
|
||||||
|
{{ previewLoading && previewDocument?.id === document.id ? 'Memuatkan...' : 'Lihat' }}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" look="outline" size="sm" :disabled="downloadingDocumentId === document.id"
|
||||||
|
@click="handleDownloadDocument(document)">
|
||||||
|
<Download class="mr-2 size-4" />
|
||||||
|
{{ downloadingDocumentId === document.id ? 'Memuat turun...' : 'Muat Turun' }}
|
||||||
|
</Button>
|
||||||
|
<Button v-if="canEditAdminAttachments" type="button" look="outline" size="sm" variant="danger"
|
||||||
|
:disabled="deletingDocumentId === document.id" @click="handleDeleteAdminAttachment(document)">
|
||||||
|
<Trash2 class="mr-2 size-4" />
|
||||||
|
{{ deletingDocumentId === document.id ? 'Memadam...' : 'Padam' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent v-if="application.reviews.length" value="reviews" class="mt-6">
|
<TabsContent v-if="application.reviews.length" value="reviews" class="mt-6">
|
||||||
@@ -932,11 +967,8 @@ onUnmounted(() => {
|
|||||||
{{ formatDateTime(review.reviewed_at) }}
|
{{ formatDateTime(review.reviewed_at) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<Badge :variant="reviewDecisionBadgeVariant(review.decision, review.stage)"
|
||||||
:variant="reviewDecisionBadgeVariant(review.decision, review.stage)"
|
:look="reviewDecisionBadgeLook(review.decision, review.stage)" class="whitespace-nowrap">
|
||||||
:look="reviewDecisionBadgeLook(review.decision, review.stage)"
|
|
||||||
class="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{{ reviewDecisionLabel(review.decision, review.stage) }}
|
{{ reviewDecisionLabel(review.decision, review.stage) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -959,11 +991,18 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<DialogRoot :open="confirmDialogOpen"
|
<DialogRoot :open="confirmDialogOpen"
|
||||||
@openChange="(details) => { confirmDialogOpen = details.open; if (!details.open) pendingAction = null }">
|
@openChange="(details) => { if (!details.open) closeConfirmDialog(); else confirmDialogOpen = details.open }">
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div class="p-5 text-center">
|
<div class="p-5 text-center">
|
||||||
<div class="mt-2 text-2xl font-medium">{{ confirmDialogTitle }}</div>
|
<div class="mt-2 text-2xl font-medium">{{ confirmDialogTitle }}</div>
|
||||||
<div class="mt-2 opacity-70">{{ confirmDialogDescription }}</div>
|
<div class="mt-2 opacity-70">{{ confirmDialogDescription }}</div>
|
||||||
|
<Field v-if="pendingAction?.type === 'complete'" class="mt-5 text-left">
|
||||||
|
<FieldLabel for="detail-board-meeting-reference">Rujukan Mesyuarat Lembaga</FieldLabel>
|
||||||
|
<Input id="detail-board-meeting-reference" v-model="boardMeetingReference" type="text"
|
||||||
|
placeholder="Contoh: Mesyuarat Lembaga Bil. 3/2026" :disabled="completeSubmitting"
|
||||||
|
@input="boardMeetingReferenceError = null" />
|
||||||
|
<FieldError v-if="boardMeetingReferenceError">{{ boardMeetingReferenceError }}</FieldError>
|
||||||
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-5 pb-8 text-center">
|
<div class="px-5 pb-8 text-center">
|
||||||
<DialogCloseTrigger class="mr-2 w-28" :disabled="reviewSubmitting || completeSubmitting">
|
<DialogCloseTrigger class="mr-2 w-28" :disabled="reviewSubmitting || completeSubmitting">
|
||||||
@@ -977,38 +1016,22 @@ onUnmounted(() => {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogRoot>
|
</DialogRoot>
|
||||||
|
|
||||||
<DialogRoot :open="generateDialogOpen" @openChange="(details) => { if (!details.open) closeGenerateDialog() }">
|
<DialogRoot :open="deleteConfirmDialogOpen"
|
||||||
|
@openChange="(details) => { if (!details.open) closeDeleteConfirmDialog() }">
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div class="p-5">
|
<div class="p-5 text-center">
|
||||||
<div class="text-2xl font-medium">Jana Surat Keputusan</div>
|
<div class="mt-2 text-2xl font-medium">Padam Lampiran Pentadbir?</div>
|
||||||
<p v-if="application" class="mt-2 text-sm opacity-70">
|
<div class="mt-2 opacity-70">
|
||||||
{{ application.application_number }} · {{ application.applicant?.name ?? '-' }}
|
{{ pendingDeleteDocument?.name ?? 'Lampiran pentadbir' }} akan dipadam secara kekal.
|
||||||
</p>
|
</div>
|
||||||
<Field class="mt-5">
|
|
||||||
<FieldLabel for="detail-board-meeting-reference">Rujukan Mesyuarat Lembaga</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="detail-board-meeting-reference"
|
|
||||||
v-model="boardMeetingReference"
|
|
||||||
type="text"
|
|
||||||
placeholder="Contoh: Mesyuarat Lembaga Bil. 3/2026"
|
|
||||||
:disabled="generateSubmitting"
|
|
||||||
@input="boardMeetingReferenceError = null"
|
|
||||||
/>
|
|
||||||
<FieldError v-if="boardMeetingReferenceError">{{ boardMeetingReferenceError }}</FieldError>
|
|
||||||
</Field>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="px-5 pb-8 text-center">
|
<div class="px-5 pb-8 text-center">
|
||||||
<DialogCloseTrigger class="mr-2 w-32" :disabled="generateSubmitting" @click="closeGenerateDialog">
|
<DialogCloseTrigger class="mr-2 w-28" :disabled="!!deletingDocumentId" @click="closeDeleteConfirmDialog">
|
||||||
Batal
|
Batal
|
||||||
</DialogCloseTrigger>
|
</DialogCloseTrigger>
|
||||||
<Button
|
<Button class="w-28" type="button" variant="danger" :disabled="!!deletingDocumentId"
|
||||||
class="w-32"
|
@click="confirmDeleteAdminAttachment">
|
||||||
type="button"
|
{{ deletingDocumentId ? 'Memadam...' : 'Padam' }}
|
||||||
variant="primary"
|
|
||||||
:disabled="generateSubmitting"
|
|
||||||
@click="confirmGenerateLetter"
|
|
||||||
>
|
|
||||||
{{ generateSubmitting ? 'Menjana...' : 'Jana Surat' }}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { CircleAlert, CircleCheck, Download, Eye, Plus } from '@lucide/vue'
|
import { CircleAlert, CircleCheck, Download, Eye, Plus, Trash2 } from '@lucide/vue'
|
||||||
import {
|
import {
|
||||||
AlertRoot,
|
AlertRoot,
|
||||||
AlertTitle,
|
AlertTitle,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Box } from '@/components/ui/box'
|
import { Box } from '@/components/ui/box'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { DialogRoot, DialogContent, DialogCloseTrigger } from '@/components/ui/dialog'
|
||||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +32,7 @@ import { usePermissions } from '@/composables/usePermissions'
|
|||||||
import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage'
|
import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage'
|
||||||
import {
|
import {
|
||||||
downloadMembershipApplicationDocument,
|
downloadMembershipApplicationDocument,
|
||||||
|
deleteMembershipApplicationDocument,
|
||||||
fetchMembershipApplicationDocument,
|
fetchMembershipApplicationDocument,
|
||||||
getMembershipApplication,
|
getMembershipApplication,
|
||||||
lookupMemberByIcNumber,
|
lookupMemberByIcNumber,
|
||||||
@@ -52,9 +54,10 @@ import type {
|
|||||||
MembershipApplicationReviewDetail,
|
MembershipApplicationReviewDetail,
|
||||||
MembershipApplicationStatus,
|
MembershipApplicationStatus,
|
||||||
} from '../types/membership-application.types'
|
} from '../types/membership-application.types'
|
||||||
|
import { ADMIN_ATTACHMENT_DOCUMENT_TYPE } from '../types/membership-application.types'
|
||||||
import {
|
import {
|
||||||
|
APPLICANT_DOCUMENT_UPLOAD_TYPES,
|
||||||
DOCUMENT_TYPE_LABELS,
|
DOCUMENT_TYPE_LABELS,
|
||||||
DOCUMENT_UPLOAD_TYPES,
|
|
||||||
GENDER_OPTIONS,
|
GENDER_OPTIONS,
|
||||||
MARRIAGE_STATUS_OPTIONS,
|
MARRIAGE_STATUS_OPTIONS,
|
||||||
RELATIONSHIP_OPTIONS,
|
RELATIONSHIP_OPTIONS,
|
||||||
@@ -73,7 +76,7 @@ const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024
|
|||||||
const WORKFLOW_STEPS = [
|
const WORKFLOW_STEPS = [
|
||||||
{ id: 1, label: 'Dihantar' },
|
{ id: 1, label: 'Dihantar' },
|
||||||
{ id: 2, label: 'Semakan Pentadbiran' },
|
{ id: 2, label: 'Semakan Pentadbiran' },
|
||||||
{ id: 3, label: 'Semakan Lembaga' },
|
{ id: 3, label: 'Keputusan Ahli Lembaga Koperasi (ALK)' },
|
||||||
{ id: 4, label: 'Makluman Keputusan' },
|
{ id: 4, label: 'Makluman Keputusan' },
|
||||||
{ id: 5, label: 'Selesai' },
|
{ id: 5, label: 'Selesai' },
|
||||||
] as const
|
] as const
|
||||||
@@ -91,11 +94,15 @@ const application = ref<MembershipApplicationDetail | null>(null)
|
|||||||
const form = reactive<MembershipApplicationFormState>(createEmptyFormState())
|
const form = reactive<MembershipApplicationFormState>(createEmptyFormState())
|
||||||
const fieldErrors = reactive<Record<string, string>>({})
|
const fieldErrors = reactive<Record<string, string>>({})
|
||||||
const downloadingDocumentId = ref<string | null>(null)
|
const downloadingDocumentId = ref<string | null>(null)
|
||||||
|
const deletingDocumentId = ref<string | null>(null)
|
||||||
const uploadingDocumentType = ref<DocumentUploadType | null>(null)
|
const uploadingDocumentType = ref<DocumentUploadType | null>(null)
|
||||||
|
const uploadingAdminAttachment = ref(false)
|
||||||
const previewOpen = ref(false)
|
const previewOpen = ref(false)
|
||||||
const previewLoading = ref(false)
|
const previewLoading = ref(false)
|
||||||
const previewUrl = ref<string | null>(null)
|
const previewUrl = ref<string | null>(null)
|
||||||
const previewDocument = ref<MembershipApplicationDocumentDetail | null>(null)
|
const previewDocument = ref<MembershipApplicationDocumentDetail | null>(null)
|
||||||
|
const deleteConfirmDialogOpen = ref(false)
|
||||||
|
const pendingDeleteDocument = ref<MembershipApplicationDocumentDetail | null>(null)
|
||||||
|
|
||||||
const referenceLookupLoading = reactive({
|
const referenceLookupLoading = reactive({
|
||||||
proposer: false,
|
proposer: false,
|
||||||
@@ -131,17 +138,22 @@ const sortedReviews = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const documentsByType = computed(() => {
|
function buildDocumentsByType(types: readonly DocumentUploadType[]) {
|
||||||
const map: Partial<Record<DocumentUploadType, MembershipApplicationDocumentDetail>> = {}
|
const map: Partial<Record<DocumentUploadType, MembershipApplicationDocumentDetail>> = {}
|
||||||
|
|
||||||
application.value?.documents.forEach((document) => {
|
application.value?.documents.forEach((document) => {
|
||||||
if (DOCUMENT_UPLOAD_TYPES.includes(document.type as DocumentUploadType)) {
|
if (types.includes(document.type as DocumentUploadType)) {
|
||||||
map[document.type as DocumentUploadType] = document
|
map[document.type as DocumentUploadType] = document
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return map
|
return map
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const applicantDocumentsByType = computed(() => buildDocumentsByType(APPLICANT_DOCUMENT_UPLOAD_TYPES))
|
||||||
|
const adminAttachments = computed(() =>
|
||||||
|
application.value?.documents.filter((document) => document.type === ADMIN_ATTACHMENT_DOCUMENT_TYPE) ?? [],
|
||||||
|
)
|
||||||
|
|
||||||
function getWorkflowProgress(status: MembershipApplicationStatus) {
|
function getWorkflowProgress(status: MembershipApplicationStatus) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -163,7 +175,7 @@ function getWorkflowProgress(status: MembershipApplicationStatus) {
|
|||||||
function statusLabel(status: MembershipApplicationStatus): string {
|
function statusLabel(status: MembershipApplicationStatus): string {
|
||||||
const labels: Record<MembershipApplicationStatus, string> = {
|
const labels: Record<MembershipApplicationStatus, string> = {
|
||||||
SUBMITTED: 'Dihantar',
|
SUBMITTED: 'Dihantar',
|
||||||
PENDING_BOARD: 'Menunggu Lembaga',
|
PENDING_BOARD: 'Menunggu Keputusan Mesyuarat ALK',
|
||||||
MANAGEMENT_REJECTED: 'Ditolak Pentadbiran',
|
MANAGEMENT_REJECTED: 'Ditolak Pentadbiran',
|
||||||
PENDING_NOTIFICATION: 'Menunggu Makluman',
|
PENDING_NOTIFICATION: 'Menunggu Makluman',
|
||||||
COMPLETED: 'Selesai',
|
COMPLETED: 'Selesai',
|
||||||
@@ -247,7 +259,7 @@ const isPreviewPdf = computed(() => (previewDocument.value ? isPdfDocument(previ
|
|||||||
|
|
||||||
function reviewStageLabel(stage: string): string {
|
function reviewStageLabel(stage: string): string {
|
||||||
if (stage === 'MANAGEMENT') return 'Semakan Pentadbiran'
|
if (stage === 'MANAGEMENT') return 'Semakan Pentadbiran'
|
||||||
if (stage === 'BOARD') return 'Semakan Lembaga'
|
if (stage === 'BOARD') return 'Keputusan Ahli Lembaga Koperasi (ALK)'
|
||||||
return stage
|
return stage
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,6 +480,91 @@ async function handleDocumentUpload(type: DocumentUploadType, event: Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleAdminAttachmentUpload(event: Event) {
|
||||||
|
if (!application.value || !canEdit.value || uploadingAdminAttachment.value) return
|
||||||
|
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const files = input.files ? Array.from(input.files) : []
|
||||||
|
input.value = ''
|
||||||
|
|
||||||
|
if (!files.length) return
|
||||||
|
|
||||||
|
const oversizedFile = files.find((file) => file.size > MAX_FILE_SIZE_BYTES)
|
||||||
|
if (oversizedFile) {
|
||||||
|
fieldErrors[`documents.${ADMIN_ATTACHMENT_DOCUMENT_TYPE}`] =
|
||||||
|
`Saiz fail melebihi ${MAX_FILE_SIZE_MB}MB.`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete fieldErrors[`documents.${ADMIN_ATTACHMENT_DOCUMENT_TYPE}`]
|
||||||
|
uploadingAdminAttachment.value = true
|
||||||
|
error.value = null
|
||||||
|
successMessage.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
let latestResponse = null
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
latestResponse = await uploadMembershipApplicationDocument(
|
||||||
|
applicationId.value,
|
||||||
|
ADMIN_ATTACHMENT_DOCUMENT_TYPE,
|
||||||
|
file,
|
||||||
|
)
|
||||||
|
application.value = latestResponse.data
|
||||||
|
}
|
||||||
|
|
||||||
|
successMessage.value =
|
||||||
|
files.length > 1
|
||||||
|
? `${files.length} lampiran pentadbir berjaya dimuat naik.`
|
||||||
|
: latestResponse?.message || 'Lampiran pentadbir berjaya dimuat naik.'
|
||||||
|
} catch (err) {
|
||||||
|
const validationErrors = getApiValidationErrors(err)
|
||||||
|
if (validationErrors) {
|
||||||
|
setFieldErrors(validationErrors)
|
||||||
|
}
|
||||||
|
error.value = getApiErrorMessage(err, 'Gagal memuat naik lampiran pentadbir.')
|
||||||
|
} finally {
|
||||||
|
uploadingAdminAttachment.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteAdminAttachment(document: MembershipApplicationDocumentDetail) {
|
||||||
|
if (!application.value || !canEdit.value || deletingDocumentId.value) return
|
||||||
|
|
||||||
|
pendingDeleteDocument.value = document
|
||||||
|
deleteConfirmDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteConfirmDialog() {
|
||||||
|
deleteConfirmDialogOpen.value = false
|
||||||
|
pendingDeleteDocument.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDeleteAdminAttachment() {
|
||||||
|
const document = pendingDeleteDocument.value
|
||||||
|
if (!application.value || !document || deletingDocumentId.value) return
|
||||||
|
|
||||||
|
deletingDocumentId.value = document.id
|
||||||
|
error.value = null
|
||||||
|
successMessage.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await deleteMembershipApplicationDocument(applicationId.value, document.id)
|
||||||
|
application.value = response.data
|
||||||
|
|
||||||
|
if (previewDocument.value?.id === document.id) {
|
||||||
|
handlePreviewOpenChange(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
successMessage.value = response.message || 'Lampiran pentadbir berjaya dipadam.'
|
||||||
|
closeDeleteConfirmDialog()
|
||||||
|
} catch (err) {
|
||||||
|
error.value = getApiErrorMessage(err, 'Gagal memadam lampiran pentadbir.')
|
||||||
|
} finally {
|
||||||
|
deletingDocumentId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleDownloadDocument(document: MembershipApplicationDocumentDetail) {
|
async function handleDownloadDocument(document: MembershipApplicationDocumentDetail) {
|
||||||
if (!application.value || downloadingDocumentId.value) return
|
if (!application.value || downloadingDocumentId.value) return
|
||||||
|
|
||||||
@@ -635,7 +732,7 @@ onUnmounted(() => {
|
|||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
||||||
value="heirs">
|
value="heirs">
|
||||||
Waris
|
Penama
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
||||||
@@ -770,13 +867,13 @@ onUnmounted(() => {
|
|||||||
<FieldLabel for="current_position">Jawatan Semasa</FieldLabel>
|
<FieldLabel for="current_position">Jawatan Semasa</FieldLabel>
|
||||||
<Input id="current_position" v-model="form.applicant.current_position" type="text" :disabled="!canEdit" />
|
<Input id="current_position" v-model="form.applicant.current_position" type="text" :disabled="!canEdit" />
|
||||||
<FieldError v-if="fieldErrors['applicant.current_position']">{{ fieldErrors['applicant.current_position']
|
<FieldError v-if="fieldErrors['applicant.current_position']">{{ fieldErrors['applicant.current_position']
|
||||||
}}</FieldError>
|
}}</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field class="col-span-12">
|
<Field class="col-span-12">
|
||||||
<FieldLabel for="employer_address">Alamat Majikan</FieldLabel>
|
<FieldLabel for="employer_address">Alamat Majikan</FieldLabel>
|
||||||
<Textarea id="employer_address" v-model="form.applicant.employer_address" rows="3" :disabled="!canEdit" />
|
<Textarea id="employer_address" v-model="form.applicant.employer_address" rows="3" :disabled="!canEdit" />
|
||||||
<FieldError v-if="fieldErrors['applicant.employer_address']">{{ fieldErrors['applicant.employer_address']
|
<FieldError v-if="fieldErrors['applicant.employer_address']">{{ fieldErrors['applicant.employer_address']
|
||||||
}}</FieldError>
|
}}</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field class="col-span-12 sm:col-span-4">
|
<Field class="col-span-12 sm:col-span-4">
|
||||||
<FieldLabel for="start_work_date">Tarikh Mula Berkhidmat</FieldLabel>
|
<FieldLabel for="start_work_date">Tarikh Mula Berkhidmat</FieldLabel>
|
||||||
@@ -805,7 +902,7 @@ onUnmounted(() => {
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div v-for="(heir, index) in form.heirs" :key="index" class="rounded-lg border border-foreground/10 p-4">
|
<div v-for="(heir, index) in form.heirs" :key="index" class="rounded-lg border border-foreground/10 p-4">
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<div class="font-medium">Waris {{ index + 1 }}</div>
|
<div class="font-medium">Penama</div>
|
||||||
<Button v-if="canEdit && form.heirs.length > 1" type="button" look="outline" size="sm"
|
<Button v-if="canEdit && form.heirs.length > 1" type="button" look="outline" size="sm"
|
||||||
@click="removeHeir(index)">
|
@click="removeHeir(index)">
|
||||||
Buang
|
Buang
|
||||||
@@ -822,7 +919,7 @@ onUnmounted(() => {
|
|||||||
<FieldLabel :for="`heir-ic-${index}`">No. Kad Pengenalan</FieldLabel>
|
<FieldLabel :for="`heir-ic-${index}`">No. Kad Pengenalan</FieldLabel>
|
||||||
<Input :id="`heir-ic-${index}`" v-model="heir.ic_number" type="text" :disabled="!canEdit" />
|
<Input :id="`heir-ic-${index}`" v-model="heir.ic_number" type="text" :disabled="!canEdit" />
|
||||||
<FieldError v-if="fieldErrors[`heirs.${index}.ic_number`]">{{ fieldErrors[`heirs.${index}.ic_number`]
|
<FieldError v-if="fieldErrors[`heirs.${index}.ic_number`]">{{ fieldErrors[`heirs.${index}.ic_number`]
|
||||||
}}</FieldError>
|
}}</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
<Field class="col-span-12 sm:col-span-6">
|
||||||
<FieldLabel>Hubungan</FieldLabel>
|
<FieldLabel>Hubungan</FieldLabel>
|
||||||
@@ -855,10 +952,10 @@ onUnmounted(() => {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button v-if="canEdit" type="button" look="outline" @click="addHeir">
|
<!-- <Button v-if="canEdit" type="button" look="outline" @click="addHeir">
|
||||||
<Plus class="mr-2 size-4" />
|
<Plus class="mr-2 size-4" />
|
||||||
Tambah Waris
|
Tambah Penama
|
||||||
</Button>
|
</Button> -->
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -900,25 +997,27 @@ onUnmounted(() => {
|
|||||||
Muat naik fail baharu untuk menggantikan dokumen sedia ada.
|
Muat naik fail baharu untuk menggantikan dokumen sedia ada.
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div v-for="type in DOCUMENT_UPLOAD_TYPES" :key="type" class="rounded-lg border border-foreground/10 p-4">
|
<div v-for="type in APPLICANT_DOCUMENT_UPLOAD_TYPES" :key="type"
|
||||||
|
class="rounded-lg border border-foreground/10 p-4">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium">{{ DOCUMENT_TYPE_LABELS[type] }}</div>
|
<div class="font-medium">{{ DOCUMENT_TYPE_LABELS[type] }}</div>
|
||||||
<div v-if="documentsByType[type]" class="mt-1 text-sm opacity-70">
|
<div v-if="applicantDocumentsByType[type]" class="mt-1 text-sm opacity-70">
|
||||||
{{ documentsByType[type]?.name }} · {{ formatFileSize(documentsByType[type]?.file_size) }}
|
{{ applicantDocumentsByType[type]?.name }} ·
|
||||||
|
{{ formatFileSize(applicantDocumentsByType[type]?.file_size) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="mt-1 text-sm opacity-70">Tiada dokumen dimuat naik.</div>
|
<div v-else class="mt-1 text-sm opacity-70">Tiada dokumen dimuat naik.</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="documentsByType[type]" class="flex flex-wrap items-center gap-2">
|
<div v-if="applicantDocumentsByType[type]" class="flex flex-wrap items-center gap-2">
|
||||||
<Button type="button" look="outline" size="sm"
|
<Button type="button" look="outline" size="sm"
|
||||||
:disabled="previewLoading && previewDocument?.id === documentsByType[type]?.id"
|
:disabled="previewLoading && previewDocument?.id === applicantDocumentsByType[type]?.id"
|
||||||
@click="documentsByType[type] && handleViewDocument(documentsByType[type]!)">
|
@click="applicantDocumentsByType[type] && handleViewDocument(applicantDocumentsByType[type]!)">
|
||||||
<Eye class="mr-2 size-4" />
|
<Eye class="mr-2 size-4" />
|
||||||
Lihat
|
Lihat
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" look="outline" size="sm"
|
<Button type="button" look="outline" size="sm"
|
||||||
:disabled="downloadingDocumentId === documentsByType[type]?.id"
|
:disabled="downloadingDocumentId === applicantDocumentsByType[type]?.id"
|
||||||
@click="documentsByType[type] && handleDownloadDocument(documentsByType[type]!)">
|
@click="applicantDocumentsByType[type] && handleDownloadDocument(applicantDocumentsByType[type]!)">
|
||||||
<Download class="mr-2 size-4" />
|
<Download class="mr-2 size-4" />
|
||||||
Muat Turun
|
Muat Turun
|
||||||
</Button>
|
</Button>
|
||||||
@@ -926,7 +1025,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<Field v-if="canEdit" class="mt-4">
|
<Field v-if="canEdit" class="mt-4">
|
||||||
<FieldLabel :for="`document-${type}`">
|
<FieldLabel :for="`document-${type}`">
|
||||||
{{ documentsByType[type] ? 'Ganti Dokumen' : 'Muat Naik Dokumen' }}
|
{{ applicantDocumentsByType[type] ? 'Ganti Dokumen' : 'Muat Naik Dokumen' }}
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<Input :id="`document-${type}`" type="file" :accept="documentAccept(type)"
|
<Input :id="`document-${type}`" type="file" :accept="documentAccept(type)"
|
||||||
:disabled="uploadingDocumentType === type" @change="handleDocumentUpload(type, $event)" />
|
:disabled="uploadingDocumentType === type" @change="handleDocumentUpload(type, $event)" />
|
||||||
@@ -935,6 +1034,51 @@ onUnmounted(() => {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<div class="mb-4 font-medium">Lampiran Pentadbir</div>
|
||||||
|
<p class="mb-4 text-sm opacity-70">
|
||||||
|
Muat naik satu atau lebih dokumen tambahan semasa semakan permohonan. Setiap muat naik akan
|
||||||
|
menambah lampiran baharu.
|
||||||
|
</p>
|
||||||
|
<div v-if="adminAttachments.length" class="mb-4 space-y-3">
|
||||||
|
<div v-for="document in adminAttachments" :key="document.id"
|
||||||
|
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-foreground/10 p-4">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ document.name }}</div>
|
||||||
|
<div class="mt-1 text-sm opacity-70">{{ formatFileSize(document.file_size) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Button type="button" look="outline" size="sm"
|
||||||
|
:disabled="previewLoading && previewDocument?.id === document.id"
|
||||||
|
@click="handleViewDocument(document)">
|
||||||
|
<Eye class="mr-2 size-4" />
|
||||||
|
Lihat
|
||||||
|
</Button>
|
||||||
|
<Button type="button" look="outline" size="sm" :disabled="downloadingDocumentId === document.id"
|
||||||
|
@click="handleDownloadDocument(document)">
|
||||||
|
<Download class="mr-2 size-4" />
|
||||||
|
Muat Turun
|
||||||
|
</Button>
|
||||||
|
<Button v-if="canEdit" type="button" look="outline" size="sm" variant="danger"
|
||||||
|
:disabled="deletingDocumentId === document.id" @click="handleDeleteAdminAttachment(document)">
|
||||||
|
<Trash2 class="mr-2 size-4" />
|
||||||
|
{{ deletingDocumentId === document.id ? 'Memadam...' : 'Padam' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mb-4 text-sm opacity-70">Tiada lampiran pentadbir dimuat naik.</div>
|
||||||
|
<Field v-if="canEdit" class="rounded-lg border border-foreground/10 p-4">
|
||||||
|
<FieldLabel for="document-admin-attachment">Tambah Lampiran Pentadbir</FieldLabel>
|
||||||
|
<Input id="document-admin-attachment" type="file" accept=".pdf,.jpg,.jpeg,.png" multiple
|
||||||
|
:disabled="uploadingAdminAttachment" @change="handleAdminAttachmentUpload" />
|
||||||
|
<FieldError v-if="fieldErrors[`documents.${ADMIN_ATTACHMENT_DOCUMENT_TYPE}`]">
|
||||||
|
{{ fieldErrors[`documents.${ADMIN_ATTACHMENT_DOCUMENT_TYPE}`] }}
|
||||||
|
</FieldError>
|
||||||
|
<p v-if="uploadingAdminAttachment" class="mt-1 text-sm opacity-70">Memuat naik...</p>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent v-if="application.reviews.length" value="reviews" class="mt-6">
|
<TabsContent v-if="application.reviews.length" value="reviews" class="mt-6">
|
||||||
@@ -998,6 +1142,27 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<DialogRoot :open="deleteConfirmDialogOpen"
|
||||||
|
@openChange="(details) => { if (!details.open) closeDeleteConfirmDialog() }">
|
||||||
|
<DialogContent>
|
||||||
|
<div class="p-5 text-center">
|
||||||
|
<div class="mt-2 text-2xl font-medium">Padam Lampiran Pentadbir?</div>
|
||||||
|
<div class="mt-2 opacity-70">
|
||||||
|
{{ pendingDeleteDocument?.name ?? 'Lampiran pentadbir' }} akan dipadam secara kekal.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-5 pb-8 text-center">
|
||||||
|
<DialogCloseTrigger class="mr-2 w-28" :disabled="!!deletingDocumentId" @click="closeDeleteConfirmDialog">
|
||||||
|
Batal
|
||||||
|
</DialogCloseTrigger>
|
||||||
|
<Button class="w-28" type="button" variant="danger" :disabled="!!deletingDocumentId"
|
||||||
|
@click="confirmDeleteAdminAttachment">
|
||||||
|
{{ deletingDocumentId ? 'Memadam...' : 'Padam' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogRoot>
|
||||||
|
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div v-if="previewOpen" class="fixed inset-0 z-70 flex items-center justify-center p-4 sm:p-6" role="dialog"
|
<div v-if="previewOpen" class="fixed inset-0 z-70 flex items-center justify-center p-4 sm:p-6" role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { CircleAlert, CircleCheck, Search, Eye, Pencil, FileText, Download } from '@lucide/vue'
|
import { CircleAlert, CircleCheck, Search, Eye, Pencil, Download } from '@lucide/vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import * as select from '@zag-js/select'
|
import * as select from '@zag-js/select'
|
||||||
import {
|
import {
|
||||||
@@ -36,7 +36,6 @@ import { usePermissions } from '@/composables/usePermissions'
|
|||||||
import {
|
import {
|
||||||
batchCompleteMembershipApplications,
|
batchCompleteMembershipApplications,
|
||||||
downloadMembershipApplicationDocument,
|
downloadMembershipApplicationDocument,
|
||||||
generateMembershipApplicationResultLetter,
|
|
||||||
} from '../services/membership-application.service'
|
} from '../services/membership-application.service'
|
||||||
import {
|
import {
|
||||||
boardResultBadgeVariant,
|
boardResultBadgeVariant,
|
||||||
@@ -46,7 +45,6 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
BatchCompleteFailedItem,
|
BatchCompleteFailedItem,
|
||||||
BatchCompleteResponse,
|
BatchCompleteResponse,
|
||||||
GenerateResultLetterResponse,
|
|
||||||
MembershipApplicationBoardResult,
|
MembershipApplicationBoardResult,
|
||||||
MembershipApplicationListItem,
|
MembershipApplicationListItem,
|
||||||
MembershipApplicationStatus,
|
MembershipApplicationStatus,
|
||||||
@@ -57,7 +55,7 @@ type SelectOption = { label: string; value: string }
|
|||||||
const STATUS_FILTER_OPTIONS: SelectOption[] = [
|
const STATUS_FILTER_OPTIONS: SelectOption[] = [
|
||||||
{ label: 'Semua Status', value: '' },
|
{ label: 'Semua Status', value: '' },
|
||||||
{ label: 'Dihantar', value: 'SUBMITTED' },
|
{ label: 'Dihantar', value: 'SUBMITTED' },
|
||||||
{ label: 'Menunggu Lembaga', value: 'PENDING_BOARD' },
|
{ label: 'Menunggu Keputusan ALK', value: 'PENDING_BOARD' },
|
||||||
{ label: 'Ditolak Pentadbiran', value: 'MANAGEMENT_REJECTED' },
|
{ label: 'Ditolak Pentadbiran', value: 'MANAGEMENT_REJECTED' },
|
||||||
{ label: 'Menunggu Makluman', value: 'PENDING_NOTIFICATION' },
|
{ label: 'Menunggu Makluman', value: 'PENDING_NOTIFICATION' },
|
||||||
{ label: 'Selesai', value: 'COMPLETED' },
|
{ label: 'Selesai', value: 'COMPLETED' },
|
||||||
@@ -105,18 +103,13 @@ const {
|
|||||||
} = useMembershipApplicationList()
|
} = useMembershipApplicationList()
|
||||||
|
|
||||||
const canBatchComplete = computed(() => hasPermission('selesaikan permohonan keahlian'))
|
const canBatchComplete = computed(() => hasPermission('selesaikan permohonan keahlian'))
|
||||||
const canGenerateResultLetter = computed(() => hasPermission('jana surat keputusan keahlian'))
|
|
||||||
const selectedIds = ref<string[]>([])
|
const selectedIds = ref<string[]>([])
|
||||||
const batchSubmitting = ref(false)
|
const batchSubmitting = ref(false)
|
||||||
const batchConfirmOpen = ref(false)
|
const batchConfirmOpen = ref(false)
|
||||||
const batchSuccessMessage = ref<string | null>(null)
|
const batchSuccessMessage = ref<string | null>(null)
|
||||||
const letterSuccessMessage = ref<string | null>(null)
|
|
||||||
const batchFailedItems = ref<BatchCompleteFailedItem[]>([])
|
const batchFailedItems = ref<BatchCompleteFailedItem[]>([])
|
||||||
const generateDialogOpen = ref(false)
|
|
||||||
const generateSubmitting = ref(false)
|
|
||||||
const boardMeetingReference = ref('')
|
const boardMeetingReference = ref('')
|
||||||
const boardMeetingReferenceError = ref<string | null>(null)
|
const boardMeetingReferenceError = ref<string | null>(null)
|
||||||
const generateTarget = ref<MembershipApplicationListItem | null>(null)
|
|
||||||
const downloadingResultLetterId = ref<string | null>(null)
|
const downloadingResultLetterId = ref<string | null>(null)
|
||||||
|
|
||||||
const selectableApplications = computed(() =>
|
const selectableApplications = computed(() =>
|
||||||
@@ -167,19 +160,28 @@ function toggleSelectAllOnPage(checked: boolean) {
|
|||||||
|
|
||||||
function openBatchConfirm() {
|
function openBatchConfirm() {
|
||||||
if (!selectedIds.value.length) return
|
if (!selectedIds.value.length) return
|
||||||
|
boardMeetingReference.value = ''
|
||||||
|
boardMeetingReferenceError.value = null
|
||||||
batchConfirmOpen.value = true
|
batchConfirmOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmBatchComplete() {
|
async function confirmBatchComplete() {
|
||||||
if (!selectedIds.value.length || batchSubmitting.value) return
|
if (!selectedIds.value.length || batchSubmitting.value) return
|
||||||
|
|
||||||
|
const reference = boardMeetingReference.value.trim()
|
||||||
|
if (!reference) {
|
||||||
|
boardMeetingReferenceError.value = 'Rujukan mesyuarat lembaga diperlukan.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
batchSubmitting.value = true
|
batchSubmitting.value = true
|
||||||
batchSuccessMessage.value = null
|
batchSuccessMessage.value = null
|
||||||
batchFailedItems.value = []
|
batchFailedItems.value = []
|
||||||
|
boardMeetingReferenceError.value = null
|
||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await batchCompleteMembershipApplications(selectedIds.value)
|
const response = await batchCompleteMembershipApplications(selectedIds.value, reference)
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
batchSuccessMessage.value = response.message
|
batchSuccessMessage.value = response.message
|
||||||
@@ -196,6 +198,8 @@ async function confirmBatchComplete() {
|
|||||||
const responseData = err.response.data as BatchCompleteResponse
|
const responseData = err.response.data as BatchCompleteResponse
|
||||||
batchFailedItems.value = responseData.data?.failed ?? []
|
batchFailedItems.value = responseData.data?.failed ?? []
|
||||||
error.value = responseData.message ?? getApiErrorMessage(err, 'Gagal menyelesaikan permohonan.')
|
error.value = responseData.message ?? getApiErrorMessage(err, 'Gagal menyelesaikan permohonan.')
|
||||||
|
const validationErrors = getApiValidationErrors(err)
|
||||||
|
boardMeetingReferenceError.value = validationErrors?.board_meeting_reference?.[0] ?? null
|
||||||
} else {
|
} else {
|
||||||
error.value = getApiErrorMessage(err, 'Gagal menyelesaikan permohonan.')
|
error.value = getApiErrorMessage(err, 'Gagal menyelesaikan permohonan.')
|
||||||
}
|
}
|
||||||
@@ -213,7 +217,7 @@ const statusFilterInitial = computed(() => apiValueToLabel(STATUS_FILTER_OPTIONS
|
|||||||
function statusLabel(status: MembershipApplicationStatus): string {
|
function statusLabel(status: MembershipApplicationStatus): string {
|
||||||
const labels: Record<MembershipApplicationStatus, string> = {
|
const labels: Record<MembershipApplicationStatus, string> = {
|
||||||
SUBMITTED: 'Dihantar',
|
SUBMITTED: 'Dihantar',
|
||||||
PENDING_BOARD: 'Menunggu Lembaga',
|
PENDING_BOARD: 'Menunggu Keputusan ALK',
|
||||||
MANAGEMENT_REJECTED: 'Ditolak Pentadbiran',
|
MANAGEMENT_REJECTED: 'Ditolak Pentadbiran',
|
||||||
PENDING_NOTIFICATION: 'Menunggu Makluman',
|
PENDING_NOTIFICATION: 'Menunggu Makluman',
|
||||||
COMPLETED: 'Selesai',
|
COMPLETED: 'Selesai',
|
||||||
@@ -241,70 +245,10 @@ function goToApplicationEdit(id: string) {
|
|||||||
router.push({ name: 'edit-membership-application', params: { id } })
|
router.push({ name: 'edit-membership-application', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
function canGenerateLetter(item: MembershipApplicationListItem): boolean {
|
|
||||||
return (
|
|
||||||
canGenerateResultLetter.value &&
|
|
||||||
item.status === 'COMPLETED' &&
|
|
||||||
!item.has_result_letter &&
|
|
||||||
(item.board_result === 'PASS' || item.board_result === 'FAIL')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function canDownloadLetter(item: MembershipApplicationListItem): boolean {
|
function canDownloadLetter(item: MembershipApplicationListItem): boolean {
|
||||||
return !!item.has_result_letter && !!item.result_letter_document
|
return !!item.has_result_letter && !!item.result_letter_document
|
||||||
}
|
}
|
||||||
|
|
||||||
function openGenerateDialog(item: MembershipApplicationListItem) {
|
|
||||||
generateTarget.value = item
|
|
||||||
boardMeetingReference.value = ''
|
|
||||||
boardMeetingReferenceError.value = null
|
|
||||||
generateDialogOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeGenerateDialog() {
|
|
||||||
if (generateSubmitting.value) return
|
|
||||||
generateDialogOpen.value = false
|
|
||||||
generateTarget.value = null
|
|
||||||
boardMeetingReference.value = ''
|
|
||||||
boardMeetingReferenceError.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmGenerateLetter() {
|
|
||||||
if (!generateTarget.value || generateSubmitting.value) return
|
|
||||||
|
|
||||||
const reference = boardMeetingReference.value.trim()
|
|
||||||
if (!reference) {
|
|
||||||
boardMeetingReferenceError.value = 'Rujukan mesyuarat lembaga diperlukan.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
generateSubmitting.value = true
|
|
||||||
boardMeetingReferenceError.value = null
|
|
||||||
error.value = null
|
|
||||||
letterSuccessMessage.value = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await generateMembershipApplicationResultLetter(generateTarget.value.id, {
|
|
||||||
board_meeting_reference: reference,
|
|
||||||
})
|
|
||||||
|
|
||||||
letterSuccessMessage.value = response.message
|
|
||||||
closeGenerateDialog()
|
|
||||||
await fetchApplications(page.value)
|
|
||||||
} catch (err) {
|
|
||||||
if (axios.isAxiosError(err) && err.response?.data) {
|
|
||||||
const responseData = err.response.data as GenerateResultLetterResponse
|
|
||||||
const validationErrors = getApiValidationErrors(err)
|
|
||||||
boardMeetingReferenceError.value = validationErrors?.board_meeting_reference?.[0] ?? null
|
|
||||||
error.value = responseData.message ?? getApiErrorMessage(err, 'Gagal menjana surat keputusan.')
|
|
||||||
} else {
|
|
||||||
error.value = getApiErrorMessage(err, 'Gagal menjana surat keputusan.')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
generateSubmitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDownloadResultLetter(item: MembershipApplicationListItem) {
|
async function handleDownloadResultLetter(item: MembershipApplicationListItem) {
|
||||||
const document = item.result_letter_document
|
const document = item.result_letter_document
|
||||||
if (!document) return
|
if (!document) return
|
||||||
@@ -384,13 +328,6 @@ const headers = computed<TableHeader[]>(() => {
|
|||||||
<p class="mt-1 text-sm opacity-70">Urus dan semak permohonan keahlian koperasi.</p>
|
<p class="mt-1 text-sm opacity-70">Urus dan semak permohonan keahlian koperasi.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AlertRoot v-if="letterSuccessMessage" variant="success">
|
|
||||||
<CircleCheck />
|
|
||||||
<AlertTitle>Berjaya</AlertTitle>
|
|
||||||
<AlertDescription>{{ letterSuccessMessage }}</AlertDescription>
|
|
||||||
<AlertCloseTrigger @click="letterSuccessMessage = null" />
|
|
||||||
</AlertRoot>
|
|
||||||
|
|
||||||
<AlertRoot v-if="batchSuccessMessage" variant="success">
|
<AlertRoot v-if="batchSuccessMessage" variant="success">
|
||||||
<CircleCheck />
|
<CircleCheck />
|
||||||
<AlertTitle>Berjaya</AlertTitle>
|
<AlertTitle>Berjaya</AlertTitle>
|
||||||
@@ -482,11 +419,8 @@ const headers = computed<TableHeader[]>(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #item.status="{ item }">
|
<template #item.status="{ item }">
|
||||||
<Badge
|
<Badge :variant="statusBadgeVariant((item as MembershipApplicationListItem).status)"
|
||||||
:variant="statusBadgeVariant((item as MembershipApplicationListItem).status)"
|
:look="statusBadgeLook((item as MembershipApplicationListItem).status)" class="whitespace-nowrap">
|
||||||
:look="statusBadgeLook((item as MembershipApplicationListItem).status)"
|
|
||||||
class="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{{ statusLabel((item as MembershipApplicationListItem).status) }}
|
{{ statusLabel((item as MembershipApplicationListItem).status) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</template>
|
</template>
|
||||||
@@ -516,27 +450,10 @@ const headers = computed<TableHeader[]>(() => {
|
|||||||
@click="goToApplicationEdit((item as MembershipApplicationListItem).id)">
|
@click="goToApplicationEdit((item as MembershipApplicationListItem).id)">
|
||||||
<Pencil class="size-4" aria-hidden="true" />
|
<Pencil class="size-4" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button v-if="canDownloadLetter(item as MembershipApplicationListItem)" type="button" variant="ghost"
|
||||||
v-if="canGenerateLetter(item as MembershipApplicationListItem)"
|
size="sm" class="bg-purple-600 text-white" title="Muat turun surat keputusan"
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="bg-amber-600 text-white"
|
|
||||||
title="Jana surat keputusan"
|
|
||||||
@click="openGenerateDialog(item as MembershipApplicationListItem)"
|
|
||||||
>
|
|
||||||
<FileText class="size-4" aria-hidden="true" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="canDownloadLetter(item as MembershipApplicationListItem)"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="bg-purple-600 text-white"
|
|
||||||
title="Muat turun surat keputusan"
|
|
||||||
:disabled="downloadingResultLetterId === (item as MembershipApplicationListItem).id"
|
:disabled="downloadingResultLetterId === (item as MembershipApplicationListItem).id"
|
||||||
@click="handleDownloadResultLetter(item as MembershipApplicationListItem)"
|
@click="handleDownloadResultLetter(item as MembershipApplicationListItem)">
|
||||||
>
|
|
||||||
<Download class="size-4" aria-hidden="true" />
|
<Download class="size-4" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -548,8 +465,16 @@ const headers = computed<TableHeader[]>(() => {
|
|||||||
<div class="p-5 text-center">
|
<div class="p-5 text-center">
|
||||||
<div class="mt-2 text-2xl font-medium">Selesaikan Permohonan Terpilih?</div>
|
<div class="mt-2 text-2xl font-medium">Selesaikan Permohonan Terpilih?</div>
|
||||||
<div class="mt-2 opacity-70">
|
<div class="mt-2 opacity-70">
|
||||||
{{ selectedIds.length }} permohonan akan diselesaikan dan e-mel keputusan dihantar.
|
{{ selectedIds.length }} permohonan akan diselesaikan. Surat keputusan dijana dan e-mel dengan lampiran
|
||||||
|
surat dihantar.
|
||||||
</div>
|
</div>
|
||||||
|
<Field class="mt-5 text-left">
|
||||||
|
<FieldLabel for="batch-board-meeting-reference">Rujukan Mesyuarat Lembaga</FieldLabel>
|
||||||
|
<Input id="batch-board-meeting-reference" v-model="boardMeetingReference" type="text"
|
||||||
|
placeholder="Contoh: Mesyuarat Lembaga Bil. 3/2026" :disabled="batchSubmitting"
|
||||||
|
@input="boardMeetingReferenceError = null" />
|
||||||
|
<FieldError v-if="boardMeetingReferenceError">{{ boardMeetingReferenceError }}</FieldError>
|
||||||
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-5 pb-8 text-center">
|
<div class="px-5 pb-8 text-center">
|
||||||
<DialogCloseTrigger class="mr-2 w-32" :disabled="batchSubmitting">
|
<DialogCloseTrigger class="mr-2 w-32" :disabled="batchSubmitting">
|
||||||
@@ -562,45 +487,5 @@ const headers = computed<TableHeader[]>(() => {
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogRoot>
|
</DialogRoot>
|
||||||
|
|
||||||
<DialogRoot :open="generateDialogOpen" @openChange="(details) => { if (!details.open) closeGenerateDialog() }">
|
|
||||||
<DialogContent>
|
|
||||||
<div class="p-5">
|
|
||||||
<div class="text-2xl font-medium">Jana Surat Keputusan</div>
|
|
||||||
<p v-if="generateTarget" class="mt-2 text-sm opacity-70">
|
|
||||||
{{ generateTarget.application_number }} · {{ generateTarget.applicant?.name ?? '-' }}
|
|
||||||
</p>
|
|
||||||
<Field class="mt-5">
|
|
||||||
<FieldLabel for="board-meeting-reference">Rujukan Mesyuarat Lembaga</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="board-meeting-reference"
|
|
||||||
v-model="boardMeetingReference"
|
|
||||||
type="text"
|
|
||||||
placeholder="Contoh: Mesyuarat Lembaga Bil. 3/2026"
|
|
||||||
:disabled="generateSubmitting"
|
|
||||||
@input="boardMeetingReferenceError = null"
|
|
||||||
/>
|
|
||||||
<FieldError v-if="boardMeetingReferenceError">{{ boardMeetingReferenceError }}</FieldError>
|
|
||||||
</Field>
|
|
||||||
<p class="mt-3 text-sm opacity-70">
|
|
||||||
Surat hanya boleh dijana sekali dan akan disimpan sebagai dokumen permohonan.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="px-5 pb-8 text-center">
|
|
||||||
<DialogCloseTrigger class="mr-2 w-32" :disabled="generateSubmitting" @click="closeGenerateDialog">
|
|
||||||
Batal
|
|
||||||
</DialogCloseTrigger>
|
|
||||||
<Button
|
|
||||||
class="w-32"
|
|
||||||
type="button"
|
|
||||||
variant="primary"
|
|
||||||
:disabled="generateSubmitting"
|
|
||||||
@click="confirmGenerateLetter"
|
|
||||||
>
|
|
||||||
{{ generateSubmitting ? 'Menjana...' : 'Jana Surat' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</DialogRoot>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -157,6 +157,21 @@ export async function uploadMembershipApplicationDocument(
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteMembershipApplicationDocument(
|
||||||
|
id: string,
|
||||||
|
documentId: string,
|
||||||
|
): Promise<MembershipApplicationUpdateResponse> {
|
||||||
|
const { data } = await api.delete<MembershipApplicationUpdateResponse>(
|
||||||
|
`/v1/membership-applications/${id}/documents/${documentId}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.message ?? 'Failed to delete document')
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
// Submit management review
|
// Submit management review
|
||||||
export async function submitManagementReview(
|
export async function submitManagementReview(
|
||||||
id: string,
|
id: string,
|
||||||
@@ -194,9 +209,11 @@ export async function submitBoardReview(
|
|||||||
// Complete membership application
|
// Complete membership application
|
||||||
export async function completeMembershipApplication(
|
export async function completeMembershipApplication(
|
||||||
id: string,
|
id: string,
|
||||||
|
payload: GenerateResultLetterPayload,
|
||||||
): Promise<MembershipApplicationReviewResponse> {
|
): Promise<MembershipApplicationReviewResponse> {
|
||||||
const { data } = await api.post<MembershipApplicationReviewResponse>(
|
const { data } = await api.post<MembershipApplicationReviewResponse>(
|
||||||
`/v1/membership-applications/${id}/complete`,
|
`/v1/membership-applications/${id}/complete`,
|
||||||
|
payload,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
@@ -208,10 +225,14 @@ export async function completeMembershipApplication(
|
|||||||
|
|
||||||
export async function batchCompleteMembershipApplications(
|
export async function batchCompleteMembershipApplications(
|
||||||
applicationIds: string[],
|
applicationIds: string[],
|
||||||
|
boardMeetingReference: string,
|
||||||
): Promise<BatchCompleteResponse> {
|
): Promise<BatchCompleteResponse> {
|
||||||
const { data } = await api.post<BatchCompleteResponse>(
|
const { data } = await api.post<BatchCompleteResponse>(
|
||||||
'/v1/membership-applications/batch-complete',
|
'/v1/membership-applications/batch-complete',
|
||||||
{ application_ids: applicationIds },
|
{
|
||||||
|
application_ids: applicationIds,
|
||||||
|
board_meeting_reference: boardMeetingReference,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export type MembershipApplicationStatus =
|
|||||||
export type MembershipApplicationBoardResult = 'PASS' | 'FAIL'
|
export type MembershipApplicationBoardResult = 'PASS' | 'FAIL'
|
||||||
|
|
||||||
export const RESULT_LETTER_DOCUMENT_TYPE = 'result_letter' as const
|
export const RESULT_LETTER_DOCUMENT_TYPE = 'result_letter' as const
|
||||||
|
export const ADMIN_ATTACHMENT_DOCUMENT_TYPE = 'admin_attachment' as const
|
||||||
|
|
||||||
export interface MembershipApplicationApplicantSummary {
|
export interface MembershipApplicationApplicantSummary {
|
||||||
name: string
|
name: string
|
||||||
@@ -208,7 +209,12 @@ export type MembershipApplicationUpdateResponse = MembershipApplicationApiRespon
|
|||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocumentUploadType = 'ic_copy' | 'photo' | 'salary_slip' | 'employer_letter'
|
export type DocumentUploadType =
|
||||||
|
| 'ic_copy'
|
||||||
|
| 'photo'
|
||||||
|
| 'salary_slip'
|
||||||
|
| 'employer_letter'
|
||||||
|
| 'admin_attachment'
|
||||||
|
|
||||||
export type ManagementReviewDecision = 'APPROVED' | 'REJECTED'
|
export type ManagementReviewDecision = 'APPROVED' | 'REJECTED'
|
||||||
export type BoardReviewDecision = 'PASS' | 'FAIL'
|
export type BoardReviewDecision = 'PASS' | 'FAIL'
|
||||||
|
|||||||
@@ -40,10 +40,26 @@ export const DOCUMENT_TYPE_LABELS: Record<string, string> = {
|
|||||||
photo: 'Gambar Passport',
|
photo: 'Gambar Passport',
|
||||||
salary_slip: 'Slip Gaji',
|
salary_slip: 'Slip Gaji',
|
||||||
employer_letter: 'Surat Pengesahan Majikan',
|
employer_letter: 'Surat Pengesahan Majikan',
|
||||||
|
admin_attachment: 'Lampiran Pentadbir',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DOCUMENT_UPLOAD_TYPES = ['ic_copy', 'photo', 'salary_slip', 'employer_letter'] as const
|
export const APPLICANT_DOCUMENT_UPLOAD_TYPES = [
|
||||||
export type DocumentUploadType = (typeof DOCUMENT_UPLOAD_TYPES)[number]
|
'ic_copy',
|
||||||
|
'photo',
|
||||||
|
'salary_slip',
|
||||||
|
'employer_letter',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const ADMIN_DOCUMENT_UPLOAD_TYPES = ['admin_attachment'] as const
|
||||||
|
|
||||||
|
export const DOCUMENT_UPLOAD_TYPES = [
|
||||||
|
...APPLICANT_DOCUMENT_UPLOAD_TYPES,
|
||||||
|
...ADMIN_DOCUMENT_UPLOAD_TYPES,
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type ApplicantDocumentUploadType = (typeof APPLICANT_DOCUMENT_UPLOAD_TYPES)[number]
|
||||||
|
export type AdminDocumentUploadType = (typeof ADMIN_DOCUMENT_UPLOAD_TYPES)[number]
|
||||||
|
export type DocumentUploadType = ApplicantDocumentUploadType | AdminDocumentUploadType
|
||||||
|
|
||||||
export function createSelectCollection(options: SelectOption[]) {
|
export function createSelectCollection(options: SelectOption[]) {
|
||||||
return select.collection({
|
return select.collection({
|
||||||
|
|||||||
@@ -69,6 +69,33 @@ const EMPLOYMENT_TYPE_OPTIONS: SelectOption[] = [
|
|||||||
{ label: 'Freelance', value: 'Freelance' },
|
{ label: 'Freelance', value: 'Freelance' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// TODO: replace with API lookup
|
||||||
|
const EMPLOYERS = [
|
||||||
|
{
|
||||||
|
name: 'Infra Quest Sdn Bhd (IQSB)',
|
||||||
|
address: 'Lot 1045, Jalan Dato’ Lundang, 15200 Kota Bharu, Kelantan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Permodalan Kelantan Berhad (PKB)',
|
||||||
|
address:
|
||||||
|
'Permodalan Kelantan Berhad, Tingkat 4, Wisma Permodalan Kelantan Berhad, Jalan Maju, 15000 Kota Bharu Kelantan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Koperasi Permodalan Kelantan Berhad (KOPKB)',
|
||||||
|
address:
|
||||||
|
'Lot Pt 448, Tingkat 1,Jalan Kuala Krai, Batu 3, Wakaf Che Yeh, 15150 Kota Bharu, Kelantan.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "An-Nisa'",
|
||||||
|
address: 'Jln Sultan Ibrahim, Bandar Kota Bharu, 15050 Kota Bharu, Kelantan.',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const COMPANY_OPTIONS: SelectOption[] = EMPLOYERS.map((employer) => ({
|
||||||
|
label: employer.name,
|
||||||
|
value: employer.name,
|
||||||
|
}))
|
||||||
|
|
||||||
function createSelectCollection(options: SelectOption[]) {
|
function createSelectCollection(options: SelectOption[]) {
|
||||||
return select.collection({
|
return select.collection({
|
||||||
items: options,
|
items: options,
|
||||||
@@ -88,9 +115,12 @@ function apiValueToLabel(options: SelectOption[], value: string | null | undefin
|
|||||||
}
|
}
|
||||||
|
|
||||||
const employmentTypeCollection = createSelectCollection(EMPLOYMENT_TYPE_OPTIONS)
|
const employmentTypeCollection = createSelectCollection(EMPLOYMENT_TYPE_OPTIONS)
|
||||||
|
const companyNameCollection = createSelectCollection(COMPANY_OPTIONS)
|
||||||
|
|
||||||
const employmentTypeValue = ref<string[]>([])
|
const employmentTypeValue = ref<string[]>([])
|
||||||
const employmentTypeInitial = ref<string[]>([])
|
const employmentTypeInitial = ref<string[]>([])
|
||||||
|
const companyNameValue = ref<string[]>([])
|
||||||
|
const companyNameInitial = ref<string[]>([])
|
||||||
|
|
||||||
function clearEmploymentFieldError(field: EmploymentFieldKey) {
|
function clearEmploymentFieldError(field: EmploymentFieldKey) {
|
||||||
delete employmentErrors[field]
|
delete employmentErrors[field]
|
||||||
@@ -140,6 +170,10 @@ const employmentTypeLabel = computed(() =>
|
|||||||
|
|
||||||
const isEditingEmployment = computed(() => editingEmploymentId.value !== null)
|
const isEditingEmployment = computed(() => editingEmploymentId.value !== null)
|
||||||
|
|
||||||
|
const canAddEmployment = computed(() => !loadingEmployments.value && employments.value.length === 0)
|
||||||
|
|
||||||
|
const showEmploymentForm = computed(() => isEditingEmployment.value || canAddEmployment.value)
|
||||||
|
|
||||||
function setEmploymentTypeValue(details: { value: string[] }) {
|
function setEmploymentTypeValue(details: { value: string[] }) {
|
||||||
employmentTypeValue.value = details.value
|
employmentTypeValue.value = details.value
|
||||||
clearEmploymentFieldError('employment_type')
|
clearEmploymentFieldError('employment_type')
|
||||||
@@ -147,9 +181,17 @@ function setEmploymentTypeValue(details: { value: string[] }) {
|
|||||||
labelToApiValue(EMPLOYMENT_TYPE_OPTIONS, details.value[0]) ?? ''
|
labelToApiValue(EMPLOYMENT_TYPE_OPTIONS, details.value[0]) ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCompanyNameValue(details: { value: string[] }) {
|
||||||
|
companyNameValue.value = details.value
|
||||||
|
clearEmploymentFieldError('company_name')
|
||||||
|
employmentForm.company_name = details.value[0] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
function syncEmploymentSelectValues() {
|
function syncEmploymentSelectValues() {
|
||||||
employmentTypeValue.value = apiValueToLabel(EMPLOYMENT_TYPE_OPTIONS, employmentForm.employment_type)
|
employmentTypeValue.value = apiValueToLabel(EMPLOYMENT_TYPE_OPTIONS, employmentForm.employment_type)
|
||||||
employmentTypeInitial.value = [...employmentTypeValue.value]
|
employmentTypeInitial.value = [...employmentTypeValue.value]
|
||||||
|
companyNameValue.value = apiValueToLabel(COMPANY_OPTIONS, employmentForm.company_name)
|
||||||
|
companyNameInitial.value = [...companyNameValue.value]
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetEmploymentForm() {
|
function resetEmploymentForm() {
|
||||||
@@ -194,7 +236,7 @@ function validateEmploymentForm(): boolean {
|
|||||||
|
|
||||||
let valid = true
|
let valid = true
|
||||||
|
|
||||||
if (!employmentForm.company_name.trim()) {
|
if (!companyNameValue.value[0]?.trim()) {
|
||||||
employmentErrors.company_name = 'Nama syarikat diperlukan.'
|
employmentErrors.company_name = 'Nama syarikat diperlukan.'
|
||||||
valid = false
|
valid = false
|
||||||
}
|
}
|
||||||
@@ -441,7 +483,8 @@ onMounted(async () => {
|
|||||||
Tiada pekerjaan direkodkan.
|
Tiada pekerjaan direkodkan.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="space-y-6 border-t border-foreground/10 pt-6" @submit.prevent="onSaveEmployment">
|
<form v-if="showEmploymentForm" class="space-y-6 border-t border-foreground/10 pt-6"
|
||||||
|
@submit.prevent="onSaveEmployment">
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-base font-semibold text-slate-900">
|
<h4 class="text-base font-semibold text-slate-900">
|
||||||
@@ -470,10 +513,24 @@ onMounted(async () => {
|
|||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="employment-company">Nama Syarikat</FieldLabel>
|
<FieldLabel>Nama Syarikat</FieldLabel>
|
||||||
<Input id="employment-company" v-model="employmentForm.company_name" type="text"
|
<SelectRoot :key="`company-name-${editingEmploymentId ?? 'new'}`" class="w-full"
|
||||||
placeholder="Nama syarikat" :aria-invalid="!!employmentErrors.company_name"
|
:collection="companyNameCollection" :default-value="companyNameInitial" :disabled="savingEmployment"
|
||||||
@input="clearEmploymentFieldError('company_name')" />
|
@value-change="setCompanyNameValue">
|
||||||
|
<SelectControl>
|
||||||
|
<SelectTrigger :aria-invalid="!!employmentErrors.company_name">
|
||||||
|
<SelectValueText placeholder="Pilih syarikat" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</SelectControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItemGroup>
|
||||||
|
<SelectItemGroupLabel>Nama Syarikat</SelectItemGroupLabel>
|
||||||
|
<SelectItem v-for="item in companyNameCollection.items" :key="item.label" :item="item">
|
||||||
|
<SelectItemText>{{ item.label }}</SelectItemText>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectItemGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</SelectRoot>
|
||||||
<FieldError v-if="employmentErrors.company_name">{{ employmentErrors.company_name }}</FieldError>
|
<FieldError v-if="employmentErrors.company_name">{{ employmentErrors.company_name }}</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
|
|||||||
@@ -90,7 +90,10 @@ const relationshipCollection = createSelectCollection(RELATIONSHIP_OPTIONS)
|
|||||||
const relationshipValue = ref<string[]>([])
|
const relationshipValue = ref<string[]>([])
|
||||||
const relationshipInitial = ref<string[]>([])
|
const relationshipInitial = ref<string[]>([])
|
||||||
|
|
||||||
|
const MAX_HEIRS = 1
|
||||||
const isEditingHeir = computed(() => editingHeirId.value !== null)
|
const isEditingHeir = computed(() => editingHeirId.value !== null)
|
||||||
|
const hasReachedHeirLimit = computed(() => heirs.value.length >= MAX_HEIRS)
|
||||||
|
const showHeirForm = computed(() => isEditingHeir.value || !hasReachedHeirLimit.value)
|
||||||
|
|
||||||
function emptyHeirForm() {
|
function emptyHeirForm() {
|
||||||
return {
|
return {
|
||||||
@@ -204,7 +207,7 @@ async function fetchHeirs() {
|
|||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Ralat',
|
title: 'Ralat',
|
||||||
text: getApiErrorMessage(error, 'Gagal memuatkan pewaris.'),
|
text: getApiErrorMessage(error, 'Gagal memuatkan penama.'),
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
loadingHeirs.value = false
|
loadingHeirs.value = false
|
||||||
@@ -224,6 +227,15 @@ function startEditHeir(heir: Heir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onSaveHeir() {
|
async function onSaveHeir() {
|
||||||
|
if (!isEditingHeir.value && hasReachedHeirLimit.value) {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: 'info',
|
||||||
|
title: 'Had penama',
|
||||||
|
text: 'Hanya satu penama dibenarkan.',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!validateHeirForm()) {
|
if (!validateHeirForm()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -238,7 +250,7 @@ async function onSaveHeir() {
|
|||||||
: await createHeir(payload)
|
: await createHeir(payload)
|
||||||
|
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
throw new Error(res.message ?? 'Gagal menyimpan pewaris.')
|
throw new Error(res.message ?? 'Gagal menyimpan penama.')
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchHeirs()
|
await fetchHeirs()
|
||||||
@@ -248,7 +260,7 @@ async function onSaveHeir() {
|
|||||||
toast: true,
|
toast: true,
|
||||||
position: 'top-end',
|
position: 'top-end',
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: wasEditing ? 'Pewaris berjaya dikemas kini.' : 'Pewaris berjaya ditambah.',
|
title: wasEditing ? 'Penama berjaya dikemas kini.' : 'Penama berjaya ditambah.',
|
||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
timer: 3000,
|
timer: 3000,
|
||||||
})
|
})
|
||||||
@@ -257,7 +269,7 @@ async function onSaveHeir() {
|
|||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Ralat',
|
title: 'Ralat',
|
||||||
text: getApiErrorMessage(error, 'Gagal menyimpan pewaris.'),
|
text: getApiErrorMessage(error, 'Gagal menyimpan penama.'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -268,7 +280,7 @@ async function onSaveHeir() {
|
|||||||
async function onDeleteHeir(heir: Heir) {
|
async function onDeleteHeir(heir: Heir) {
|
||||||
const result = await Swal.fire({
|
const result = await Swal.fire({
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
title: 'Padam pewaris?',
|
title: 'Padam penama?',
|
||||||
text: 'Tindakan ini tidak boleh dibatalkan.',
|
text: 'Tindakan ini tidak boleh dibatalkan.',
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonText: 'Padam',
|
confirmButtonText: 'Padam',
|
||||||
@@ -283,7 +295,7 @@ async function onDeleteHeir(heir: Heir) {
|
|||||||
const res = await deleteHeir(heir.id)
|
const res = await deleteHeir(heir.id)
|
||||||
|
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
throw new Error(res.message ?? 'Gagal memadam pewaris.')
|
throw new Error(res.message ?? 'Gagal memadam penama.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editingHeirId.value === heir.id) {
|
if (editingHeirId.value === heir.id) {
|
||||||
@@ -296,7 +308,7 @@ async function onDeleteHeir(heir: Heir) {
|
|||||||
toast: true,
|
toast: true,
|
||||||
position: 'top-end',
|
position: 'top-end',
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Pewaris berjaya dipadam.',
|
title: 'Penama berjaya dipadam.',
|
||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
timer: 3000,
|
timer: 3000,
|
||||||
})
|
})
|
||||||
@@ -304,7 +316,7 @@ async function onDeleteHeir(heir: Heir) {
|
|||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Ralat',
|
title: 'Ralat',
|
||||||
text: getApiErrorMessage(error, 'Gagal memadam pewaris.'),
|
text: getApiErrorMessage(error, 'Gagal memadam penama.'),
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
deletingHeirId.value = null
|
deletingHeirId.value = null
|
||||||
@@ -323,23 +335,20 @@ onMounted(async () => {
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold text-slate-900">Pewaris</h3>
|
<h3 class="text-lg font-semibold text-slate-900">Penama</h3>
|
||||||
<p class="mt-1 text-sm text-slate-500">
|
<p class="mt-1 text-sm text-slate-500">
|
||||||
Urus maklumat pewaris anda.
|
Urus maklumat penama anda. Hanya satu penama dibenarkan.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loadingHeirs" class="text-sm text-slate-500">
|
<div v-if="loadingHeirs" class="text-sm text-slate-500">
|
||||||
Memuatkan pewaris...
|
Memuatkan penama...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="heirs.length" class="space-y-3">
|
<div v-else-if="heirs.length" class="space-y-3">
|
||||||
<div
|
<div v-for="heir in heirs" :key="heir.id"
|
||||||
v-for="heir in heirs"
|
class="flex flex-col gap-4 rounded-lg border border-foreground/10 p-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
:key="heir.id"
|
|
||||||
class="flex flex-col gap-4 rounded-lg border border-foreground/10 p-4 sm:flex-row sm:items-start sm:justify-between"
|
|
||||||
>
|
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="font-medium text-slate-900">{{ heir.name }}</span>
|
<span class="font-medium text-slate-900">{{ heir.name }}</span>
|
||||||
@@ -351,64 +360,47 @@ onMounted(async () => {
|
|||||||
<p class="mt-1 text-sm text-slate-700">{{ heir.address }}</p>
|
<p class="mt-1 text-sm text-slate-700">{{ heir.address }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 gap-2">
|
<div class="flex shrink-0 gap-2">
|
||||||
<Button
|
<Button type="button" variant="ghost" class="border border-foreground/15 shadow-none"
|
||||||
type="button"
|
:disabled="deletingHeirId === heir.id" @click="startEditHeir(heir)">
|
||||||
variant="ghost"
|
|
||||||
class="border border-foreground/15 shadow-none"
|
|
||||||
:disabled="deletingHeirId === heir.id"
|
|
||||||
@click="startEditHeir(heir)"
|
|
||||||
>
|
|
||||||
<Lucide class="mr-2 size-4" icon="Pencil" />
|
<Lucide class="mr-2 size-4" icon="Pencil" />
|
||||||
Kemaskini
|
Kemaskini
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="button" variant="ghost" class="border border-foreground/15 shadow-none text-danger"
|
||||||
type="button"
|
:disabled="deletingHeirId === heir.id" @click="onDeleteHeir(heir)">
|
||||||
variant="ghost"
|
<Lucide class="mr-2 size-4" :icon="deletingHeirId === heir.id ? 'LoaderCircle' : 'Trash'"
|
||||||
class="border border-foreground/15 shadow-none text-danger"
|
:class="{ 'animate-spin': deletingHeirId === heir.id }" />
|
||||||
:disabled="deletingHeirId === heir.id"
|
|
||||||
@click="onDeleteHeir(heir)"
|
|
||||||
>
|
|
||||||
<Lucide
|
|
||||||
class="mr-2 size-4"
|
|
||||||
:icon="deletingHeirId === heir.id ? 'LoaderCircle' : 'Trash'"
|
|
||||||
:class="{ 'animate-spin': deletingHeirId === heir.id }"
|
|
||||||
/>
|
|
||||||
Padam
|
Padam
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-else class="rounded-lg border border-dashed border-foreground/15 p-6 text-center text-sm text-slate-500">
|
||||||
v-else
|
Tiada penama direkodkan.
|
||||||
class="rounded-lg border border-dashed border-foreground/15 p-6 text-center text-sm text-slate-500"
|
|
||||||
>
|
|
||||||
Tiada pewaris direkodkan.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="space-y-6 border-t border-foreground/10 pt-6" @submit.prevent="onSaveHeir">
|
<div v-if="hasReachedHeirLimit && !isEditingHeir"
|
||||||
|
class="rounded-lg border border-foreground/10 bg-foreground/5 p-4 text-sm text-slate-600">
|
||||||
|
Had penama telah dicapai. Kemaskini atau padam penama sedia ada untuk membuat perubahan.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form v-if="showHeirForm" class="space-y-6 border-t border-foreground/10 pt-6" @submit.prevent="onSaveHeir">
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-base font-semibold text-slate-900">
|
<h4 class="text-base font-semibold text-slate-900">
|
||||||
{{ isEditingHeir ? 'Kemaskini Pewaris' : 'Tambah Pewaris' }}
|
{{ isEditingHeir ? 'Kemaskini Penama' : 'Tambah Penama' }}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="mt-1 text-sm text-slate-500">
|
<p class="mt-1 text-sm text-slate-500">
|
||||||
{{
|
{{
|
||||||
isEditingHeir
|
isEditingHeir
|
||||||
? 'Kemas kini maklumat pewaris yang dipilih.'
|
? 'Kemas kini maklumat penama yang dipilih.'
|
||||||
: 'Tambah pewaris baharu ke profil anda.'
|
: 'Tambah penama baharu ke profil anda.'
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button v-if="isEditingHeir" type="button" variant="ghost" class="border border-foreground/15 shadow-none"
|
||||||
v-if="isEditingHeir"
|
:disabled="savingHeir" @click="resetHeirForm">
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
class="border border-foreground/15 shadow-none"
|
|
||||||
:disabled="savingHeir"
|
|
||||||
@click="resetHeirForm"
|
|
||||||
>
|
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" variant="primary" :disabled="savingHeir">
|
<Button type="submit" variant="primary" :disabled="savingHeir">
|
||||||
@@ -421,38 +413,21 @@ onMounted(async () => {
|
|||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="heir-name">Nama</FieldLabel>
|
<FieldLabel for="heir-name">Nama</FieldLabel>
|
||||||
<Input
|
<Input id="heir-name" v-model="heirForm.name" type="text" placeholder="Nama penuh"
|
||||||
id="heir-name"
|
:aria-invalid="!!heirErrors.name" @input="clearHeirFieldError('name')" />
|
||||||
v-model="heirForm.name"
|
|
||||||
type="text"
|
|
||||||
placeholder="Nama penuh"
|
|
||||||
:aria-invalid="!!heirErrors.name"
|
|
||||||
@input="clearHeirFieldError('name')"
|
|
||||||
/>
|
|
||||||
<FieldError v-if="heirErrors.name">{{ heirErrors.name }}</FieldError>
|
<FieldError v-if="heirErrors.name">{{ heirErrors.name }}</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="heir-ic">No. Kad Pengenalan</FieldLabel>
|
<FieldLabel for="heir-ic-number">No. Kad Pengenalan</FieldLabel>
|
||||||
<Input
|
<Input id="heir-ic-number" v-model="heirForm.ic_number" type="text" placeholder="Contoh: 900101011234"
|
||||||
id="heir-ic"
|
:aria-invalid="!!heirErrors.ic_number" @input="clearHeirFieldError('ic_number')" />
|
||||||
v-model="heirForm.ic_number"
|
|
||||||
type="text"
|
|
||||||
placeholder="No. kad pengenalan"
|
|
||||||
:aria-invalid="!!heirErrors.ic_number"
|
|
||||||
@input="clearHeirFieldError('ic_number')"
|
|
||||||
/>
|
|
||||||
<FieldError v-if="heirErrors.ic_number">{{ heirErrors.ic_number }}</FieldError>
|
<FieldError v-if="heirErrors.ic_number">{{ heirErrors.ic_number }}</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>Hubungan</FieldLabel>
|
<FieldLabel>Hubungan</FieldLabel>
|
||||||
<SelectRoot
|
<SelectRoot :key="`heir-relationship-${editingHeirId ?? 'new'}`" class="w-full"
|
||||||
:key="`heir-relationship-${editingHeirId ?? 'new'}`"
|
:collection="relationshipCollection" :default-value="relationshipInitial" :disabled="savingHeir"
|
||||||
class="w-full"
|
@value-change="setRelationshipValue">
|
||||||
:collection="relationshipCollection"
|
|
||||||
:default-value="relationshipInitial"
|
|
||||||
:disabled="savingHeir"
|
|
||||||
@value-change="setRelationshipValue"
|
|
||||||
>
|
|
||||||
<SelectControl>
|
<SelectControl>
|
||||||
<SelectTrigger :aria-invalid="!!heirErrors.relationship">
|
<SelectTrigger :aria-invalid="!!heirErrors.relationship">
|
||||||
<SelectValueText placeholder="Pilih hubungan" />
|
<SelectValueText placeholder="Pilih hubungan" />
|
||||||
@@ -461,11 +436,7 @@ onMounted(async () => {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItemGroup>
|
<SelectItemGroup>
|
||||||
<SelectItemGroupLabel>Hubungan</SelectItemGroupLabel>
|
<SelectItemGroupLabel>Hubungan</SelectItemGroupLabel>
|
||||||
<SelectItem
|
<SelectItem v-for="item in relationshipCollection.items" :key="item.label" :item="item">
|
||||||
v-for="item in relationshipCollection.items"
|
|
||||||
:key="item.label"
|
|
||||||
:item="item"
|
|
||||||
>
|
|
||||||
<SelectItemText>{{ item.label }}</SelectItemText>
|
<SelectItemText>{{ item.label }}</SelectItemText>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectItemGroup>
|
</SelectItemGroup>
|
||||||
@@ -474,37 +445,23 @@ onMounted(async () => {
|
|||||||
<FieldError v-if="heirErrors.relationship">{{ heirErrors.relationship }}</FieldError>
|
<FieldError v-if="heirErrors.relationship">{{ heirErrors.relationship }}</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="heir-phone">No. Telefon</FieldLabel>
|
<FieldLabel for="heir-phone-number">No. Telefon</FieldLabel>
|
||||||
<Input
|
<Input id="heir-phone-number" v-model="heirForm.phone_number" type="text"
|
||||||
id="heir-phone"
|
placeholder="Contoh: 0123456789" :aria-invalid="!!heirErrors.phone_number"
|
||||||
v-model="heirForm.phone_number"
|
@input="clearHeirFieldError('phone_number')" />
|
||||||
type="text"
|
|
||||||
placeholder="No. telefon"
|
|
||||||
:aria-invalid="!!heirErrors.phone_number"
|
|
||||||
@input="clearHeirFieldError('phone_number')"
|
|
||||||
/>
|
|
||||||
<FieldError v-if="heirErrors.phone_number">{{ heirErrors.phone_number }}</FieldError>
|
<FieldError v-if="heirErrors.phone_number">{{ heirErrors.phone_number }}</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field class="md:col-span-2">
|
<Field class="md:col-span-2">
|
||||||
<FieldLabel for="heir-address">Alamat</FieldLabel>
|
<FieldLabel for="heir-address">Alamat</FieldLabel>
|
||||||
<Textarea
|
<Textarea id="heir-address" v-model="heirForm.address" rows="3" placeholder="Alamat penama"
|
||||||
id="heir-address"
|
:aria-invalid="!!heirErrors.address" @input="clearHeirFieldError('address')" />
|
||||||
v-model="heirForm.address"
|
|
||||||
placeholder="Alamat penuh"
|
|
||||||
class="resize-none"
|
|
||||||
:aria-invalid="!!heirErrors.address"
|
|
||||||
@input="clearHeirFieldError('address')"
|
|
||||||
/>
|
|
||||||
<FieldError v-if="heirErrors.address">{{ heirErrors.address }}</FieldError>
|
<FieldError v-if="heirErrors.address">{{ heirErrors.address }}</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
<Field class="md:col-span-2">
|
<Field class="md:col-span-2">
|
||||||
<CheckboxRoot
|
<CheckboxRoot :checked="heirForm.is_primary" :disabled="savingHeir"
|
||||||
:checked="heirForm.is_primary"
|
@checked-change="({ checked }) => (heirForm.is_primary = checked === true)">
|
||||||
:disabled="savingHeir"
|
|
||||||
@checked-change="({ checked }) => (heirForm.is_primary = checked === true)"
|
|
||||||
>
|
|
||||||
<CheckboxControl />
|
<CheckboxControl />
|
||||||
<CheckboxLabel>Pewaris utama</CheckboxLabel>
|
<CheckboxLabel>Penama utama</CheckboxLabel>
|
||||||
</CheckboxRoot>
|
</CheckboxRoot>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +1,17 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
import fakers from '@/utils/faker'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Box } from '@/components/ui/box'
|
import { Box } from '@/components/ui/box'
|
||||||
import { TabsRoot, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
import { TabsRoot, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||||
import { MenuRoot, MenuTrigger, MenuPositioner, MenuContent, MenuItem } from '@/components/ui/menu'
|
|
||||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { SwitchRoot, SwitchControl } from '@/components/ui/switch'
|
|
||||||
import { ProgressRoot, ProgressTrack, ProgressRange } from '@/components/ui/progress-linear'
|
|
||||||
import {
|
|
||||||
CarouselRoot,
|
|
||||||
CarouselPrevTrigger,
|
|
||||||
CarouselNextTrigger,
|
|
||||||
CarouselItemGroup,
|
|
||||||
CarouselItem,
|
|
||||||
} from '@/components/ui/carousel'
|
|
||||||
import { Lucide } from '@/components/ui/lucide'
|
import { Lucide } from '@/components/ui/lucide'
|
||||||
import { FileIcon } from '@/components/ui/file-icon'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import logoUrl from '@/assets/images/logo-kopkb.svg'
|
|
||||||
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
||||||
|
import { listEmployments } from '@/modules/profile/services/employment.service'
|
||||||
import { uploadProfileImage } from '@/modules/profile/services/profile.service'
|
import { uploadProfileImage } from '@/modules/profile/services/profile.service'
|
||||||
|
import type { Employment } from '@/modules/profile/types/employment.types'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import MemberDigitalCard from '../components/MemberDigitalCard.vue'
|
||||||
import ProfileTab from './ProfileTab.vue'
|
import ProfileTab from './ProfileTab.vue'
|
||||||
import EmploymentTab from './EmploymentTab.vue'
|
import EmploymentTab from './EmploymentTab.vue'
|
||||||
import BankDetailTab from './BankDetailTab.vue'
|
import BankDetailTab from './BankDetailTab.vue'
|
||||||
@@ -34,12 +23,59 @@ const authStore = useAuthStore()
|
|||||||
const uploadingImage = ref(false)
|
const uploadingImage = ref(false)
|
||||||
const imageInputRef = ref<HTMLInputElement | null>(null)
|
const imageInputRef = ref<HTMLInputElement | null>(null)
|
||||||
const imagePreviewUrl = ref<string | null>(null)
|
const imagePreviewUrl = ref<string | null>(null)
|
||||||
|
const employments = ref<Employment[]>([])
|
||||||
|
|
||||||
|
const companyName = computed(() => {
|
||||||
|
const currentEmployment = employments.value.find((employment) => employment.is_current)
|
||||||
|
return currentEmployment?.company_name ?? employments.value[0]?.company_name ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const profileUrl = computed(() => {
|
||||||
|
const token = authStore.user?.public_profile_token
|
||||||
|
if (!token) return null
|
||||||
|
|
||||||
|
const baseUrl = (typeof window !== 'undefined'
|
||||||
|
? window.location.origin
|
||||||
|
: import.meta.env.VITE_APP_URL || ''
|
||||||
|
).replace(/\/$/, '')
|
||||||
|
|
||||||
|
return `${baseUrl}/v/${token}`
|
||||||
|
})
|
||||||
|
|
||||||
const displayValue = (value: string | number | null | undefined) => {
|
const displayValue = (value: string | number | null | undefined) => {
|
||||||
if (value === null || value === undefined || value === '') return '-'
|
if (value === null || value === undefined || value === '') return '-'
|
||||||
return String(value).trim() || '-'
|
return String(value).trim() || '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
|
active: 'Aktif',
|
||||||
|
pending: 'Menunggu',
|
||||||
|
inactive: 'Tidak Aktif',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusLabel = computed(() => {
|
||||||
|
const status = authStore.userStatus
|
||||||
|
if (!status) return '-'
|
||||||
|
return STATUS_LABELS[status] ?? status.charAt(0).toUpperCase() + status.slice(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusBadgeVariant = computed(() => {
|
||||||
|
if (authStore.isAccountActive) return 'success' as const
|
||||||
|
if (authStore.isAccountPending) return 'pending' as const
|
||||||
|
return 'secondary' as const
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatDateLabel(value: string | null | undefined): string {
|
||||||
|
if (!value) return '-'
|
||||||
|
const date = new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) return value
|
||||||
|
return new Intl.DateTimeFormat('ms-MY', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
}).format(date)
|
||||||
|
}
|
||||||
|
|
||||||
const avatarSrc = computed(
|
const avatarSrc = computed(
|
||||||
() => imagePreviewUrl.value ?? authStore.userImageUrl ?? undefined,
|
() => imagePreviewUrl.value ?? authStore.userImageUrl ?? undefined,
|
||||||
)
|
)
|
||||||
@@ -98,10 +134,21 @@ async function onImageSelected(event: Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchEmployments() {
|
||||||
|
try {
|
||||||
|
const res = await listEmployments()
|
||||||
|
employments.value = res.data
|
||||||
|
} catch {
|
||||||
|
employments.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!authStore.user) {
|
if (!authStore.user) {
|
||||||
await authStore.fetchSession()
|
await authStore.fetchSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await fetchEmployments()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -113,9 +160,10 @@ onMounted(async () => {
|
|||||||
<TabsRoot defaultValue="1">
|
<TabsRoot defaultValue="1">
|
||||||
<!-- BEGIN: Profile Info -->
|
<!-- BEGIN: Profile Info -->
|
||||||
<Box raised="single" class="mt-5 p-0">
|
<Box raised="single" class="mt-5 p-0">
|
||||||
<div class="flex flex-col border-b border-foreground/15 p-5 lg:flex-row">
|
<div class="flex flex-col border-b border-foreground/15 lg:flex-row">
|
||||||
<div class="flex flex-1 items-center justify-center px-5 lg:justify-start">
|
<!-- Identity -->
|
||||||
<div class="relative" :class="{ 'opacity-60': uploadingImage }">
|
<div class="flex flex-1 items-center justify-center p-5 lg:justify-start">
|
||||||
|
<div class="relative shrink-0" :class="{ 'opacity-60': uploadingImage }">
|
||||||
<AvatarRoot class="size-20 border-5 bg-background rounded-full sm:size-24 lg:size-32">
|
<AvatarRoot class="size-20 border-5 bg-background rounded-full sm:size-24 lg:size-32">
|
||||||
<AvatarFallback>{{ authStore.userName }}</AvatarFallback>
|
<AvatarFallback>{{ authStore.userName }}</AvatarFallback>
|
||||||
<AvatarImage v-if="avatarSrc" :src="avatarSrc" :alt="authStore.userName" />
|
<AvatarImage v-if="avatarSrc" :src="avatarSrc" :alt="authStore.userName" />
|
||||||
@@ -129,87 +177,103 @@ onMounted(async () => {
|
|||||||
<input ref="imageInputRef" type="file" accept="image/jpeg,image/png,image/jpg,image/gif" class="hidden"
|
<input ref="imageInputRef" type="file" accept="image/jpeg,image/png,image/jpg,image/gif" class="hidden"
|
||||||
@change="onImageSelected" />
|
@change="onImageSelected" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-5">
|
<div class="ml-5 min-w-0">
|
||||||
<div class="w-24 truncate text-lg font-medium sm:w-40 sm:whitespace-normal">
|
<div class="truncate text-lg font-medium sm:whitespace-normal">
|
||||||
{{ authStore.userName || '-' }}
|
{{ authStore.userName || '-' }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="authStore.userMemberType"
|
||||||
|
class="mt-1 truncate text-sm capitalize opacity-70 sm:whitespace-normal">
|
||||||
|
{{ authStore.userMemberType }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||||
|
<Badge :variant="statusBadgeVariant">{{ statusLabel }}</Badge>
|
||||||
|
<Badge v-if="authStore.userMemberNumber" look="outline" variant="secondary">
|
||||||
|
No. {{ authStore.userMemberNumber }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mt-6 flex-1 border-t border-l border-r border-foreground/15 px-5 pt-5 lg:mt-0 lg:border-t-0 lg:pt-0">
|
<!-- Contact & membership -->
|
||||||
<div class="text-center font-medium lg:mt-3 lg:text-left">Maklumat Hubungan</div>
|
<div class="flex-1 border-t border-foreground/15 p-5 lg:border-t-0 lg:border-l">
|
||||||
<div class="mt-4 flex flex-col items-center justify-center lg:items-start">
|
<div class="text-center font-medium lg:text-left">Maklumat Hubungan</div>
|
||||||
|
<div class="mt-4 flex flex-col items-center lg:items-start">
|
||||||
<div class="flex items-center truncate sm:whitespace-normal">
|
<div class="flex items-center truncate sm:whitespace-normal">
|
||||||
<Lucide class="mr-2 size-4" icon="Mail" />
|
<Lucide class="mr-2 size-4 shrink-0" icon="Mail" />
|
||||||
{{ displayValue(authStore.user?.email) }}
|
{{ displayValue(authStore.user?.email) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
|
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
|
||||||
<Lucide class="mr-2 size-4" icon="Phone" />
|
<Lucide class="mr-2 size-4 shrink-0" icon="Phone" />
|
||||||
{{ displayValue(authStore.user?.phone_number) }}
|
{{ displayValue(authStore.user?.phone_number) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
|
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
|
||||||
<Lucide class="mr-2 size-4" icon="IdCard" />
|
<Lucide class="mr-2 size-4 shrink-0" icon="IdCard" />
|
||||||
{{ displayValue(authStore.user?.ic_number) }}
|
{{ displayValue(authStore.user?.ic_number) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mt-6 flex flex-1 items-center justify-center border-t border-foreground/15 px-5 pt-5 lg:mt-0 lg:border-0 lg:pt-0">
|
|
||||||
<div
|
|
||||||
class="relative aspect-1.75/1 w-full max-w-68 overflow-hidden rounded-2xl bg-linear-to-br from-primary via-primary/95 to-primary/75 p-4 text-primary-foreground shadow-lg ring-1 ring-white/20 sm:max-w-xs sm:p-5"
|
|
||||||
role="img" aria-label="Kad digital anggota">
|
|
||||||
<div class="pointer-events-none absolute inset-0 bg-noise opacity-30" />
|
|
||||||
<div class="pointer-events-none absolute -right-10 -top-10 size-36 rounded-full bg-white/10" />
|
|
||||||
<div class="pointer-events-none absolute -bottom-12 -left-8 size-40 rounded-full bg-white/5" />
|
|
||||||
<div
|
|
||||||
class="pointer-events-none absolute right-4 top-1/2 size-10 -translate-y-1/2 rounded-md border border-white/20 bg-white/10" />
|
|
||||||
|
|
||||||
<div class="relative flex h-full flex-col justify-between">
|
<div class="mt-6 grid grid-cols-2 gap-4 sm:grid-cols-3">
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="text-center lg:text-left">
|
||||||
<img :src="logoUrl" alt="" class="h-7 w-auto brightness-0 invert sm:h-8" />
|
<div class="truncate text-base font-medium">
|
||||||
<div class="text-right text-[10px] font-semibold uppercase tracking-[0.2em] opacity-80">
|
{{ displayValue(authStore.userPosition) }}
|
||||||
Kad Digital
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-xs opacity-70">Jawatan</div>
|
||||||
<div>
|
</div>
|
||||||
<div class="text-[10px] font-medium uppercase tracking-widest opacity-70">No. Anggota</div>
|
<div class="text-center lg:text-left">
|
||||||
<div class="mt-1 font-mono text-2xl font-semibold tracking-[0.15em] sm:text-3xl">
|
<div class="truncate text-base font-medium">
|
||||||
{{ displayValue(authStore.userMemberNumber) }}
|
{{ displayValue(companyName) }}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-xs opacity-70">Syarikat</div>
|
||||||
<div class="flex items-end justify-between gap-3 border-t border-white/15 pt-3">
|
</div>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="col-span-2 text-center sm:col-span-1 lg:text-left">
|
||||||
<div class="truncate text-sm font-medium">{{ authStore.userName || '-' }}</div>
|
<div class="truncate text-base font-medium">
|
||||||
<div class="mt-0.5 text-[10px] uppercase tracking-wide opacity-60">Nama</div>
|
{{ formatDateLabel(authStore.userJoinDate) }}
|
||||||
</div>
|
|
||||||
<div class="shrink-0 text-right">
|
|
||||||
<div class="text-sm font-semibold">{{ displayValue(authStore.userMemberType) }}</div>
|
|
||||||
<div class="mt-0.5 text-[10px] uppercase tracking-wide opacity-60">Jenis Anggota</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-xs opacity-70">Tarikh Sertai</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Digital card -->
|
||||||
|
<div
|
||||||
|
class="flex shrink-0 items-center justify-center border-t border-foreground/15 p-5 lg:border-t-0 lg:border-l">
|
||||||
|
<MemberDigitalCard large :member-number="authStore.userMemberNumber" :member-name="authStore.userName"
|
||||||
|
:member-type="authStore.userMemberType" :company-name="companyName" :profile-url="profileUrl"
|
||||||
|
:image-url="avatarSrc" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Tabs title -->
|
<!-- Tabs title -->
|
||||||
<div class="px-5 py-4">
|
<div class="px-5 py-4">
|
||||||
<TabsList class="w-full mb-0 flex justify-between">
|
<TabsList class="mb-0 flex w-full">
|
||||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="1">
|
<TabsTrigger
|
||||||
<Lucide class="mr-2 size-4" icon="User" /> Profil
|
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 sm:px-3 md:text-sm"
|
||||||
|
value="1" aria-label="Profil">
|
||||||
|
<Lucide class="size-4 shrink-0 md:mr-2" icon="User" />
|
||||||
|
<span class="hidden md:inline">Profil</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="5">
|
<TabsTrigger
|
||||||
<Lucide class="mr-2 size-4" icon="Briefcase" /> Pekerjaan
|
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 sm:px-3 md:text-sm"
|
||||||
|
value="5" aria-label="Pekerjaan">
|
||||||
|
<Lucide class="size-4 shrink-0 md:mr-2" icon="Briefcase" />
|
||||||
|
<span class="hidden md:inline">Pekerjaan</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="3">
|
<TabsTrigger
|
||||||
<Lucide class="mr-2 size-4" icon="Banknote" /> Bank
|
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 sm:px-3 md:text-sm"
|
||||||
|
value="3" aria-label="Bank">
|
||||||
|
<Lucide class="size-4 shrink-0 md:mr-2" icon="Banknote" />
|
||||||
|
<span class="hidden md:inline">Bank</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="6">
|
<TabsTrigger
|
||||||
<Lucide class="mr-2 size-4" icon="Users" /> Pewaris
|
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 sm:px-3 md:text-sm"
|
||||||
|
value="6" aria-label="Penama">
|
||||||
|
<Lucide class="size-4 shrink-0 md:mr-2" icon="Users" />
|
||||||
|
<span class="hidden md:inline">Penama</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="2">
|
<TabsTrigger
|
||||||
<Lucide class="mr-2 size-4" icon="Lock" /> Kata Laluan
|
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 sm:px-3 md:text-sm"
|
||||||
|
value="2" aria-label="Kata Laluan">
|
||||||
|
<Lucide class="size-4 shrink-0 md:mr-2" icon="Lock" />
|
||||||
|
<span class="hidden md:inline">Kata Laluan</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,7 +294,7 @@ onMounted(async () => {
|
|||||||
<TabsContent value="2" class="mt-8">
|
<TabsContent value="2" class="mt-8">
|
||||||
<ChangePasswordTab embedded />
|
<ChangePasswordTab embedded />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<!-- Pewaris -->
|
<!-- Penama -->
|
||||||
<TabsContent value="6" class="mt-8">
|
<TabsContent value="6" class="mt-8">
|
||||||
<HeirTab embedded />
|
<HeirTab embedded />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -13,16 +13,12 @@ defineProps<{
|
|||||||
<div :class="embedded ? '' : 'mt-5'">
|
<div :class="embedded ? '' : 'mt-5'">
|
||||||
<Box raised="single" class="p-6">
|
<Box raised="single" class="p-6">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h3 class="text-lg font-semibold text-slate-900">Pewaris</h3>
|
<h3 class="text-lg font-semibold text-slate-900">Penama</h3>
|
||||||
<p class="mt-1 text-sm text-slate-500">Senarai pewaris pengguna.</p>
|
<p class="mt-1 text-sm text-slate-500">Senarai penama pengguna.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="user.heirs?.length" class="space-y-3">
|
<div v-if="user.heirs?.length" class="space-y-3">
|
||||||
<div
|
<div v-for="heir in user.heirs" :key="heir.id" class="rounded-lg border border-foreground/10 p-4">
|
||||||
v-for="heir in user.heirs"
|
|
||||||
:key="heir.id"
|
|
||||||
class="rounded-lg border border-foreground/10 p-4"
|
|
||||||
>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="font-medium text-slate-900">{{ heir.name }}</span>
|
<span class="font-medium text-slate-900">{{ heir.name }}</span>
|
||||||
<Badge v-if="heir.is_primary" class="bg-green-500 text-white">Utama</Badge>
|
<Badge v-if="heir.is_primary" class="bg-green-500 text-white">Utama</Badge>
|
||||||
@@ -34,11 +30,8 @@ defineProps<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-else class="rounded-lg border border-dashed border-foreground/15 p-6 text-center text-sm text-slate-500">
|
||||||
v-else
|
Tiada penama direkodkan.
|
||||||
class="rounded-lg border border-dashed border-foreground/15 p-6 text-center text-sm text-slate-500"
|
|
||||||
>
|
|
||||||
Tiada pewaris direkodkan.
|
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ onMounted(() => {
|
|||||||
<Lucide class="mr-2 size-4" icon="Banknote" /> Bank
|
<Lucide class="mr-2 size-4" icon="Banknote" /> Bank
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger class="inline-flex w-1/4 items-center justify-center" value="6">
|
<TabsTrigger class="inline-flex w-1/4 items-center justify-center" value="6">
|
||||||
<Lucide class="mr-2 size-4" icon="Users" /> Pewaris
|
<Lucide class="mr-2 size-4" icon="Users" /> Penama
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,8 +81,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ThemeSwitcher />
|
|
||||||
|
|
||||||
<button v-for="tab in sideTabs" :key="tab.id" type="button" :aria-label="tab.label" :style="{
|
<button v-for="tab in sideTabs" :key="tab.id" type="button" :aria-label="tab.label" :style="{
|
||||||
top: `calc(50% + ${tab.offsetRem}rem)`,
|
top: `calc(50% + ${tab.offsetRem}rem)`,
|
||||||
['--color' as any]: 'var(--color-primary)',
|
['--color' as any]: 'var(--color-primary)',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -351,7 +351,7 @@ const salesReportDate = ref<any[]>([
|
|||||||
</div>
|
</div>
|
||||||
<div class="relative ms-auto flex-none">
|
<div class="relative ms-auto flex-none">
|
||||||
<Donut2 class="w-[90px] h-[90px]" />
|
<Donut2 class="w-[90px] h-[90px]" />
|
||||||
<div class="absolute start-0 top-0 flex h-full w-full items-center justify-center font-medium">
|
<div class="absolute inset-s-0 top-0 flex h-full w-full items-center justify-center font-medium">
|
||||||
20%
|
20%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -378,7 +378,7 @@ const salesReportDate = ref<any[]>([
|
|||||||
</div>
|
</div>
|
||||||
<div class="relative ms-auto flex-none">
|
<div class="relative ms-auto flex-none">
|
||||||
<Donut2 class="w-[90px] h-[90px]" />
|
<Donut2 class="w-[90px] h-[90px]" />
|
||||||
<div class="absolute start-0 top-0 flex h-full w-full items-center justify-center font-medium">
|
<div class="absolute inset-s-0 top-0 flex h-full w-full items-center justify-center font-medium">
|
||||||
45%
|
45%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,16 +445,14 @@ const salesReportDate = ref<any[]>([
|
|||||||
<CarouselItem class="border-transparent bg-none bg-transparent" :index="0">
|
<CarouselItem class="border-transparent bg-none bg-transparent" :index="0">
|
||||||
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
|
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
|
||||||
<div
|
<div
|
||||||
class="w-full bg-primary/[.05] mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
|
class="w-full bg-primary/5 mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
|
||||||
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="phoneIllustration"
|
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="phoneIllustration"
|
||||||
alt="Midone - Tailwind Admin Dashboard Template" />
|
alt="MyKOPKB" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-8">
|
<div class="px-8">
|
||||||
<div class="text-center text-xl font-medium">Welcome to Midone Admin!</div>
|
<div class="text-center text-xl font-medium">Selamat Datang ke Sistem MyKOPKB</div>
|
||||||
<div class="mt-3 text-center text-base leading-relaxed opacity-70">
|
<div class="mt-3 text-center text-base leading-relaxed opacity-70">
|
||||||
Premium admin dashboard template for all kinds <br />
|
Sistem MyKOPKB adalah sistem yang membantu pengguna untuk menguruskan permohonan keahlian KOPKB.
|
||||||
of projects. With a unique and modern design, Midone offers the perfect
|
|
||||||
foundation to build professional applications with ease.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute inset-x-0 bottom-0 flex place-content-between px-5">
|
<div class="absolute inset-x-0 bottom-0 flex place-content-between px-5">
|
||||||
@@ -472,9 +470,9 @@ const salesReportDate = ref<any[]>([
|
|||||||
<CarouselItem class="border-transparent bg-none bg-transparent" :index="1">
|
<CarouselItem class="border-transparent bg-none bg-transparent" :index="1">
|
||||||
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
|
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
|
||||||
<div
|
<div
|
||||||
class="w-full bg-primary/[.05] mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
|
class="w-full bg-primary/5 mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
|
||||||
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="womanIllustration"
|
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="womanIllustration"
|
||||||
alt="Midone - Tailwind Admin Dashboard Template" />
|
alt="MyKOPKB" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="text-center text-xl font-medium">Example Request Information</div>
|
<div class="text-center text-xl font-medium">Example Request Information</div>
|
||||||
|
|||||||
@@ -1,266 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import fakers from '@/utils/faker'
|
|
||||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Box } from '@/components/ui/box'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { CheckboxRoot, CheckboxControl } from '@/components/ui/checkbox'
|
|
||||||
import { Field, FieldLabel } from '@/components/ui/field'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Lucide } from '@/components/ui/lucide'
|
|
||||||
import { MenuRoot, MenuTrigger, MenuPositioner, MenuContent, MenuItem } from '@/components/ui/menu'
|
|
||||||
|
|
||||||
import {
|
|
||||||
PopoverRoot,
|
|
||||||
PopoverTrigger,
|
|
||||||
PopoverPositioner,
|
|
||||||
PopoverContent,
|
|
||||||
} from '@/components/ui/popover'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="mt-8 grid grid-cols-10 gap-6">
|
|
||||||
<div class="col-span-10 lg:col-span-2">
|
|
||||||
<h2 class="mr-auto mt-2 text-lg font-medium">Inbox</h2>
|
|
||||||
<!-- BEGIN: Inbox Menu -->
|
|
||||||
<Box class="mt-6">
|
|
||||||
<Button class="w-full" type="button" variant="primary">
|
|
||||||
<Lucide class="size-4" icon="Edit" /> Compose
|
|
||||||
</Button>
|
|
||||||
<div class="mt-6 flex flex-col gap-2 border-t border-dashed border-foreground/[.08] pt-5">
|
|
||||||
<a class="[&.active]:bg-foreground/5 [&.active]:border-foreground/10 active flex items-center rounded-md border border-transparent px-3 py-2"
|
|
||||||
href="">
|
|
||||||
<Lucide class="mr-2 size-4" icon="Mail" /> Inbox
|
|
||||||
<Badge class="ms-auto" look="outline" variant="warning"> 214 </Badge>
|
|
||||||
</a>
|
|
||||||
<a class="[&.active]:bg-foreground/5 [&.active]:border-foreground/10 flex items-center rounded-md border border-transparent px-3 py-2"
|
|
||||||
href="">
|
|
||||||
<Lucide class="mr-2 size-4" icon="Star" /> Marked
|
|
||||||
</a>
|
|
||||||
<a class="[&.active]:bg-foreground/5 [&.active]:border-foreground/10 flex items-center rounded-md border border-transparent px-3 py-2"
|
|
||||||
href="">
|
|
||||||
<Lucide class="mr-2 size-4" icon="Inbox" /> Draft
|
|
||||||
<Badge class="ms-auto" look="outline" variant="warning"> 21 </Badge>
|
|
||||||
</a>
|
|
||||||
<a class="[&.active]:bg-foreground/5 [&.active]:border-foreground/10 flex items-center rounded-md border border-transparent px-3 py-2"
|
|
||||||
href="">
|
|
||||||
<Lucide class="mr-2 size-4" icon="Send" /> Sent
|
|
||||||
</a>
|
|
||||||
<a class="[&.active]:bg-foreground/5 [&.active]:border-foreground/10 flex items-center rounded-md border border-transparent px-3 py-2"
|
|
||||||
href="">
|
|
||||||
<Lucide class="mr-2 size-4" icon="Trash" /> Trash
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 border-t border-dashed border-foreground/[.08] pt-4">
|
|
||||||
<a class="flex items-center truncate px-3 py-2" href="">
|
|
||||||
<div
|
|
||||||
class="bg-(--color)/20 border-(--color)/60 me-3 mr-3 h-2 w-2 rounded-full border [--color:var(--color-pending)]">
|
|
||||||
</div>
|
|
||||||
Custom Work
|
|
||||||
</a>
|
|
||||||
<a class="mt-2 flex items-center truncate rounded-md px-3 py-2" href="">
|
|
||||||
<div
|
|
||||||
class="bg-(--color)/20 border-(--color)/60 me-3 mr-3 h-2 w-2 rounded-full border [--color:var(--color-success)]">
|
|
||||||
</div>
|
|
||||||
Important Meetings
|
|
||||||
</a>
|
|
||||||
<a class="mt-2 flex items-center truncate rounded-md px-3 py-2" href="">
|
|
||||||
<div
|
|
||||||
class="bg-(--color)/20 border-(--color)/60 me-3 mr-3 h-2 w-2 rounded-full border [--color:var(--color-warning)]">
|
|
||||||
</div>
|
|
||||||
Work
|
|
||||||
</a>
|
|
||||||
<a class="mt-2 flex items-center truncate rounded-md px-3 py-2" href="">
|
|
||||||
<div
|
|
||||||
class="bg-(--color)/20 border-(--color)/60 me-3 mr-3 h-2 w-2 rounded-full border [--color:var(--color-pending)]">
|
|
||||||
</div>
|
|
||||||
Design
|
|
||||||
</a>
|
|
||||||
<a class="mt-2 flex items-center truncate rounded-md px-3 py-2" href="">
|
|
||||||
<div
|
|
||||||
class="bg-(--color)/20 border-(--color)/60 me-3 mr-3 h-2 w-2 rounded-full border [--color:var(--color-danger)]">
|
|
||||||
</div>
|
|
||||||
Next Week
|
|
||||||
</a>
|
|
||||||
<a class="mt-2 flex items-center truncate rounded-md px-3 py-2" href="">
|
|
||||||
<Lucide class="mr-2 size-4" icon="Plus" /> Add New Label
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
<!-- END: Inbox Menu -->
|
|
||||||
</div>
|
|
||||||
<div class="col-span-10 lg:col-span-8">
|
|
||||||
<!-- BEGIN: Inbox Filter -->
|
|
||||||
<div class="flex flex-col-reverse items-center sm:flex-row">
|
|
||||||
<div class="relative mr-auto mt-3 w-full sm:mt-0 sm:w-auto">
|
|
||||||
<Lucide class="absolute inset-y-0 left-0 z-10 my-auto ml-3 size-4 opacity-70" icon="Search" />
|
|
||||||
<Input class="w-full px-10 sm:w-64" type="text" placeholder="Search mail" />
|
|
||||||
<PopoverRoot>
|
|
||||||
<PopoverTrigger as-child>
|
|
||||||
<a class="absolute inset-y-0 right-0 mr-3 flex items-center" href="#">
|
|
||||||
<Lucide class="size-4 cursor-pointer opacity-70" icon="ChevronDown" />
|
|
||||||
</a>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverPositioner>
|
|
||||||
<PopoverContent class="ml-2 w-[30rem]">
|
|
||||||
<div class="grid grid-cols-12 gap-4 gap-y-3 p-3">
|
|
||||||
<Field class="col-span-6">
|
|
||||||
<FieldLabel class="text-xs" for="input-filter-1">From</FieldLabel>
|
|
||||||
<Input id="input-filter-1" type="text" placeholder="example@gmail.com" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-6">
|
|
||||||
<FieldLabel class="text-xs" for="input-filter-2">To</FieldLabel>
|
|
||||||
<Input id="input-filter-2" type="text" placeholder="example@gmail.com" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-6">
|
|
||||||
<FieldLabel class="text-xs" for="input-filter-3">Subject</FieldLabel>
|
|
||||||
<Input id="input-filter-3" type="text" placeholder="Important Meeting" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-6">
|
|
||||||
<FieldLabel class="text-xs" for="input-filter-4">Has the Words</FieldLabel>
|
|
||||||
<Input id="input-filter-4" type="text" placeholder="Job, Work, Documentation" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-6">
|
|
||||||
<FieldLabel class="text-xs" for="input-filter-5">Doesn't Have</FieldLabel>
|
|
||||||
<Input id="input-filter-5" type="text" placeholder="Job, Work, Documentation" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-6">
|
|
||||||
<FieldLabel class="text-xs" for="input-filter-6">Size</FieldLabel>
|
|
||||||
<NativeSelect id="input-filter-6">
|
|
||||||
<NativeSelectOption value="10">10</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="25">25</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="35">35</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="50">50</NativeSelectOption>
|
|
||||||
</NativeSelect>
|
|
||||||
</Field>
|
|
||||||
<div class="col-span-12 mt-3 flex items-center">
|
|
||||||
<Button look="outline" variant="secondary" class="ml-auto w-32">
|
|
||||||
Create Filter
|
|
||||||
</Button>
|
|
||||||
<Button class="ml-2 w-32" variant="primary"> Search </Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</PopoverPositioner>
|
|
||||||
</PopoverRoot>
|
|
||||||
</div>
|
|
||||||
<div class="flex w-full sm:w-auto">
|
|
||||||
<Button class="mr-2" look="outline" variant="primary"> Start a Video Call </Button>
|
|
||||||
<MenuRoot class="w-auto">
|
|
||||||
<MenuTrigger as-child>
|
|
||||||
<Box class="px-2" as-child>
|
|
||||||
<Button variant="ghost" class="box flex items-center">
|
|
||||||
<span class="flex size-5 items-center justify-center">
|
|
||||||
<Lucide class="h-4 w-4" icon="Plus" />
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPositioner>
|
|
||||||
<MenuContent class="w-40">
|
|
||||||
<MenuItem value="contacts">
|
|
||||||
<Lucide class="mr-2 h-4 w-4" icon="User" /> Contacts
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="settings">
|
|
||||||
<Lucide class="mr-2 h-4 w-4" icon="Settings" /> Settings
|
|
||||||
</MenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</MenuPositioner>
|
|
||||||
</MenuRoot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- END: Inbox Filter -->
|
|
||||||
<!-- BEGIN: Inbox Content -->
|
|
||||||
<Box class="mt-5 p-0">
|
|
||||||
<div class="flex flex-col-reverse border-b border-foreground/15 px-5 py-6 opacity-70 sm:flex-row">
|
|
||||||
<div
|
|
||||||
class="-mx-5 mt-3 flex items-center border-t border-foreground/15 px-5 pt-5 sm:mx-0 sm:mt-0 sm:border-0 sm:px-0 sm:pt-0">
|
|
||||||
<CheckboxRoot>
|
|
||||||
<CheckboxControl />
|
|
||||||
</CheckboxRoot>
|
|
||||||
<MenuRoot class="ml-1" :positioning="{ placement: 'bottom-start' }">
|
|
||||||
<MenuTrigger as-child>
|
|
||||||
<a class="block size-5" href="#">
|
|
||||||
<Lucide class="size-5" icon="ChevronDown" />
|
|
||||||
</a>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPositioner>
|
|
||||||
<MenuContent class="w-32">
|
|
||||||
<MenuItem value="all">All</MenuItem>
|
|
||||||
<MenuItem value="none">None</MenuItem>
|
|
||||||
<MenuItem value="read">Read</MenuItem>
|
|
||||||
<MenuItem value="unread">Unread</MenuItem>
|
|
||||||
<MenuItem value="starred">Starred</MenuItem>
|
|
||||||
<MenuItem value="unstarred">Unstarred</MenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</MenuPositioner>
|
|
||||||
</MenuRoot>
|
|
||||||
<a class="ml-5 flex size-5 items-center justify-center" href="#">
|
|
||||||
<Lucide class="size-4" icon="RefreshCw" />
|
|
||||||
</a>
|
|
||||||
<a class="ml-5 flex size-5 items-center justify-center" href="#">
|
|
||||||
<Lucide class="size-4" icon="MoreHorizontal" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center sm:ml-auto">
|
|
||||||
<div>1 - 50 of 5,238</div>
|
|
||||||
<a class="ml-5 flex size-5 items-center justify-center" href="#">
|
|
||||||
<Lucide class="size-4" icon="ChevronLeft" />
|
|
||||||
</a>
|
|
||||||
<a class="ml-5 flex size-5 items-center justify-center" href="#">
|
|
||||||
<Lucide class="size-4" icon="ChevronRight" />
|
|
||||||
</a>
|
|
||||||
<a class="ml-5 flex size-5 items-center justify-center" href="#">
|
|
||||||
<Lucide class="size-4" icon="Settings" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto sm:overflow-x-visible">
|
|
||||||
<div v-for="(faker, index) in fakers" :key="index" :class="[
|
|
||||||
'transition duration-200 ease-in-out transform cursor-pointer inline-block sm:block border-b border-foreground/[.08] mx-px',
|
|
||||||
'hover:scale-[1.02] hover:relative hover:z-20 hover:shadow-md hover:border-0 hover:rounded',
|
|
||||||
!faker['trueFalse'][0] ? 'bg-foreground/5 opacity-90' : 'bg-background',
|
|
||||||
]">
|
|
||||||
<div class="flex px-5 py-3.5">
|
|
||||||
<div class="mr-5 flex w-72 flex-none items-center">
|
|
||||||
<CheckboxRoot>
|
|
||||||
<CheckboxControl />
|
|
||||||
</CheckboxRoot>
|
|
||||||
<a class="ml-4 flex size-5 flex-none items-center justify-center opacity-60" href="#">
|
|
||||||
<Lucide class="size-4" icon="Star" />
|
|
||||||
</a>
|
|
||||||
<a class="ml-2 flex size-5 flex-none items-center justify-center opacity-60" href="#">
|
|
||||||
<Lucide class="size-4" icon="Bookmark" />
|
|
||||||
</a>
|
|
||||||
<AvatarRoot class="ml-5 size-7 rounded-full">
|
|
||||||
<AvatarFallback>{{ faker['users'][0]!['name'].substring(0, 2) }}</AvatarFallback>
|
|
||||||
<AvatarImage :src="faker['photos'][0]" :alt="faker['users'][0]!['name']" />
|
|
||||||
</AvatarRoot>
|
|
||||||
<div :class="['ml-3 truncate', { 'font-medium': faker['trueFalse'][0] }]">
|
|
||||||
{{ faker['users'][0]!['name'] }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-64 truncate sm:w-auto">
|
|
||||||
<span :class="['ml-3 truncate', { 'font-medium': faker['trueFalse'][0] }]">
|
|
||||||
{{ faker['news'][0]!['superShortContent'] }}
|
|
||||||
</span>
|
|
||||||
{{ faker['news'][0]!['shortContent'] }}
|
|
||||||
</div>
|
|
||||||
<div :class="[
|
|
||||||
'pl-10 ml-auto whitespace-nowrap',
|
|
||||||
{ 'font-medium': faker['trueFalse'][0] },
|
|
||||||
]">
|
|
||||||
{{ faker['times'][0] }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-center px-5 py-6 text-center opacity-70 sm:flex-row sm:text-left">
|
|
||||||
<div>4.41 GB (25%) of 17 GB used Manage</div>
|
|
||||||
<div class="mt-2 sm:ml-auto sm:mt-0">Last account activity: 36 minutes ago</div>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
<!-- END: Inbox Content -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import * as combobox from '@zag-js/combobox'
|
|
||||||
import fakers from '@/utils/faker'
|
|
||||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
||||||
import { Box } from '@/components/ui/box'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
|
||||||
ComboboxRoot,
|
|
||||||
ComboboxControl,
|
|
||||||
ComboboxTrigger,
|
|
||||||
ComboboxContent,
|
|
||||||
ComboboxInput,
|
|
||||||
ComboboxItemGroup,
|
|
||||||
ComboboxItemGroupLabel,
|
|
||||||
ComboboxItem,
|
|
||||||
ComboboxItemText,
|
|
||||||
} from '@/components/ui/combobox'
|
|
||||||
import {
|
|
||||||
DatePickerRoot,
|
|
||||||
DatePickerControl,
|
|
||||||
DatePickerInput,
|
|
||||||
DatePickerTrigger,
|
|
||||||
DatePickerClearTrigger,
|
|
||||||
DatePickerPositioner,
|
|
||||||
DatePickerContent,
|
|
||||||
DatePickerYearSelect,
|
|
||||||
DatePickerMonthSelect,
|
|
||||||
DatePickerView,
|
|
||||||
DatePickerViewControl,
|
|
||||||
DatePickerPrevTrigger,
|
|
||||||
DatePickerViewTrigger,
|
|
||||||
DatePickerNextTrigger,
|
|
||||||
DatePickerRangeText,
|
|
||||||
DatePickerTable,
|
|
||||||
DatePickerTableHead,
|
|
||||||
DatePickerTableRow,
|
|
||||||
DatePickerTableHeader,
|
|
||||||
DatePickerTableBody,
|
|
||||||
DatePickerTableCell,
|
|
||||||
DatePickerTableCellTrigger,
|
|
||||||
DatePickerContext,
|
|
||||||
} from '@/components/ui/datepicker'
|
|
||||||
import { Field, FieldLabel } from '@/components/ui/field'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Lucide } from '@/components/ui/lucide'
|
|
||||||
import { MenuRoot, MenuTrigger, MenuPositioner, MenuContent, MenuItem } from '@/components/ui/menu'
|
|
||||||
import { SwitchRoot, SwitchControl } from '@/components/ui/switch'
|
|
||||||
import { TabsRoot, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import {
|
|
||||||
TooltipRoot,
|
|
||||||
TooltipTrigger,
|
|
||||||
TooltipPositioner,
|
|
||||||
TooltipContent,
|
|
||||||
} from '@/components/ui/tooltip'
|
|
||||||
|
|
||||||
// Categories combobox data
|
|
||||||
const categoryData = [
|
|
||||||
{ label: 'Horror', code: 'horror' },
|
|
||||||
{ label: 'Sci-fi', code: 'sci-fi' },
|
|
||||||
{ label: 'Action', code: 'action' },
|
|
||||||
{ label: 'Drama', code: 'drama' },
|
|
||||||
{ label: 'Comedy', code: 'comedy' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const categoryValue = ref(['Horror', 'Action'])
|
|
||||||
const categoryOptions = ref(categoryData)
|
|
||||||
|
|
||||||
const categoryCollection = computed(() =>
|
|
||||||
combobox.collection({
|
|
||||||
items: categoryOptions.value,
|
|
||||||
itemToValue: (item) => item.label,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tags combobox data
|
|
||||||
const tagData = [
|
|
||||||
{ label: 'Leonardo DiCaprio', code: 'leonardo' },
|
|
||||||
{ label: 'Johnny Deep', code: 'johnny' },
|
|
||||||
{ label: 'Robert Downey, Jr', code: 'robert' },
|
|
||||||
{ label: 'Samuel L. Jackson', code: 'samuel' },
|
|
||||||
{ label: 'Morgan Freeman', code: 'morgan' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const tagValue = ref(['Leonardo DiCaprio', 'Robert Downey, Jr'])
|
|
||||||
const tagOptions = ref(tagData)
|
|
||||||
|
|
||||||
const tagCollection = computed(() =>
|
|
||||||
combobox.collection({
|
|
||||||
items: tagOptions.value,
|
|
||||||
itemToValue: (item) => item.label,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-col items-center sm:flex-row">
|
|
||||||
<h2 class="mr-auto text-lg font-medium">Add New Post</h2>
|
|
||||||
<div class="mt-4 flex w-full sm:mt-0 sm:w-auto">
|
|
||||||
<MenuRoot class="mr-2">
|
|
||||||
<MenuTrigger as-child>
|
|
||||||
<Box as-child>
|
|
||||||
<Button variant="ghost" class="box flex items-center">
|
|
||||||
English
|
|
||||||
<Lucide class="ml-2 size-4" icon="ChevronDown" />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPositioner>
|
|
||||||
<MenuContent class="w-40">
|
|
||||||
<MenuItem value="english">
|
|
||||||
<Lucide class="mr-2 size-4" icon="Activity" />
|
|
||||||
<span class="truncate">English</span>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="indonesian">
|
|
||||||
<Lucide class="mr-2 size-4" icon="Activity" />
|
|
||||||
<span class="truncate">Indonesian</span>
|
|
||||||
</MenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</MenuPositioner>
|
|
||||||
</MenuRoot>
|
|
||||||
<Box as-child>
|
|
||||||
<Button class="box ml-auto mr-2 flex items-center sm:ml-0" type="button" variant="ghost">
|
|
||||||
<Lucide class="size-4" icon="Eye" /> Preview
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<MenuRoot class="w-auto">
|
|
||||||
<MenuTrigger as-child>
|
|
||||||
<Button class="box flex items-center" variant="primary" look="outline">
|
|
||||||
<Lucide class="size-4" icon="Save" />
|
|
||||||
Save
|
|
||||||
<Lucide class="size-4" icon="ChevronDown" />
|
|
||||||
</Button>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPositioner>
|
|
||||||
<MenuContent class="w-40">
|
|
||||||
<MenuItem value="new-post">
|
|
||||||
<Lucide class="mr-2 size-4" icon="FileText" /> As New Post
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="draft">
|
|
||||||
<Lucide class="mr-2 size-4" icon="FileText" /> As Draft
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="export-pdf">
|
|
||||||
<Lucide class="mr-2 size-4" icon="FileText" /> Export to PDF
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="export-word">
|
|
||||||
<Lucide class="mr-2 size-4" icon="FileText" /> Export to Word
|
|
||||||
</MenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</MenuPositioner>
|
|
||||||
</MenuRoot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 grid grid-cols-12 gap-8">
|
|
||||||
<!-- BEGIN: Post Content -->
|
|
||||||
<div class="col-span-12 lg:col-span-8">
|
|
||||||
<Box raised="single">
|
|
||||||
<Input type="text" placeholder="Title" />
|
|
||||||
</Box>
|
|
||||||
<Box class="mt-8">
|
|
||||||
<TabsRoot defaultValue="content">
|
|
||||||
<TabsList class="w-full">
|
|
||||||
<TabsTrigger
|
|
||||||
class="w-full sm:w-1/3 inline-flex items-center justify-center"
|
|
||||||
value="content"
|
|
||||||
>
|
|
||||||
<Lucide class="mr-2 size-4" icon="FileText" /> Content
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
class="w-full sm:w-1/3 inline-flex items-center justify-center"
|
|
||||||
value="meta-title"
|
|
||||||
>
|
|
||||||
<Lucide class="mr-2 size-4" icon="Code" /> Meta Title
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
class="w-full sm:w-1/3 inline-flex items-center justify-center"
|
|
||||||
value="keywords"
|
|
||||||
>
|
|
||||||
<Lucide class="mr-2 size-4" icon="AlignLeft" /> Keywords
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="content" class="mt-3.5">
|
|
||||||
<div class="rounded-lg border border-foreground/15 p-5">
|
|
||||||
<div class="flex items-center border-b border-foreground/15 pb-5 font-medium">
|
|
||||||
<Lucide class="mr-2 size-4" icon="ChevronDown" /> Text Content
|
|
||||||
</div>
|
|
||||||
<div class="mt-5">
|
|
||||||
<Textarea placeholder="Write your content here...">Content of the editor.</Textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 rounded-lg border border-foreground/15 p-5">
|
|
||||||
<div class="flex items-center border-b border-foreground/15 pb-5 font-medium">
|
|
||||||
<Lucide class="mr-2 size-4" icon="ChevronDown" /> Caption & Images
|
|
||||||
</div>
|
|
||||||
<div class="mt-5">
|
|
||||||
<Field>
|
|
||||||
<FieldLabel for="post-form-7">Caption</FieldLabel>
|
|
||||||
<Input id="post-form-7" type="text" placeholder="Write caption" />
|
|
||||||
</Field>
|
|
||||||
<Field class="mt-3">
|
|
||||||
<FieldLabel>Upload Image</FieldLabel>
|
|
||||||
<div class="rounded-xl border-2 border-dashed pt-4 border-foreground/15">
|
|
||||||
<div class="flex flex-wrap px-4">
|
|
||||||
<div
|
|
||||||
v-for="(faker, index) in fakers.slice(0, 4)"
|
|
||||||
:key="index"
|
|
||||||
class="image-fit relative mb-5 mr-5 h-24 w-24 cursor-pointer"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="rounded-xl"
|
|
||||||
:src="faker.images[0]"
|
|
||||||
alt="Midone - Tailwind Admin Dashboard Template"
|
|
||||||
/>
|
|
||||||
<TooltipRoot>
|
|
||||||
<TooltipTrigger as-child>
|
|
||||||
<div
|
|
||||||
class="bg-(--color)/80 border-(--color) text-medium absolute right-0 top-0 -mr-2 -mt-2 flex size-5 cursor-pointer items-center justify-center rounded-full text-white [--color:var(--color-danger)]"
|
|
||||||
>
|
|
||||||
<Lucide class="size-4" icon="X" />
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPositioner>
|
|
||||||
<TooltipContent>Remove this image?</TooltipContent>
|
|
||||||
</TooltipPositioner>
|
|
||||||
</TooltipRoot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="relative flex cursor-pointer items-center px-4 pb-4">
|
|
||||||
<Lucide class="mr-2 size-4" icon="Image" />
|
|
||||||
<span class="text-primary mr-1">Upload a file</span>
|
|
||||||
or drag and drop
|
|
||||||
<Input class="absolute left-0 top-0 h-full w-full opacity-0" type="file" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</TabsRoot>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
<!-- END: Post Content -->
|
|
||||||
<!-- BEGIN: Post Info -->
|
|
||||||
<div class="col-span-12 lg:col-span-4">
|
|
||||||
<Box class="box p-5">
|
|
||||||
<Field>
|
|
||||||
<FieldLabel>Written By</FieldLabel>
|
|
||||||
<MenuRoot>
|
|
||||||
<MenuTrigger as-child>
|
|
||||||
<Button variant="ghost" class="border border-foreground/15 w-full">
|
|
||||||
<AvatarRoot class="mr-1 size-6 rounded-full">
|
|
||||||
<AvatarFallback>{{
|
|
||||||
fakers[0]!['users'][0]!['name'].substring(0, 2)
|
|
||||||
}}</AvatarFallback>
|
|
||||||
<AvatarImage
|
|
||||||
:src="fakers[0]!['photos'][0]"
|
|
||||||
:alt="fakers[0]!['users'][0]!['name']"
|
|
||||||
/>
|
|
||||||
</AvatarRoot>
|
|
||||||
<div class="truncate">{{ fakers[0]!['users'][0]!['name'] }}</div>
|
|
||||||
<Lucide class="ml-auto size-4" icon="ChevronDown" />
|
|
||||||
</Button>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPositioner>
|
|
||||||
<MenuContent>
|
|
||||||
<MenuItem
|
|
||||||
v-for="(faker, index) in fakers.slice(0, 5)"
|
|
||||||
:key="index"
|
|
||||||
:value="'author-' + index"
|
|
||||||
class="py-2"
|
|
||||||
>
|
|
||||||
<div class="size-5 image-fit me-1">
|
|
||||||
<img
|
|
||||||
class="rounded-full size-full"
|
|
||||||
:src="faker['photos'][0]"
|
|
||||||
alt="Midone - Tailwind Admin Dashboard Template"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>{{ faker['users'][0]!['name'] }}</div>
|
|
||||||
</MenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</MenuPositioner>
|
|
||||||
</MenuRoot>
|
|
||||||
</Field>
|
|
||||||
<Field class="mt-3">
|
|
||||||
<FieldLabel>Post Date</FieldLabel>
|
|
||||||
<DatePickerRoot>
|
|
||||||
<DatePickerControl>
|
|
||||||
<DatePickerInput />
|
|
||||||
<DatePickerTrigger />
|
|
||||||
<DatePickerClearTrigger>Clear</DatePickerClearTrigger>
|
|
||||||
</DatePickerControl>
|
|
||||||
<Teleport to="body">
|
|
||||||
<DatePickerPositioner>
|
|
||||||
<DatePickerContent>
|
|
||||||
<DatePickerYearSelect />
|
|
||||||
<DatePickerMonthSelect />
|
|
||||||
<DatePickerView view="day">
|
|
||||||
<DatePickerContext v-slot="{ datePicker }">
|
|
||||||
<DatePickerViewControl>
|
|
||||||
<DatePickerPrevTrigger />
|
|
||||||
<DatePickerViewTrigger>
|
|
||||||
<DatePickerRangeText />
|
|
||||||
</DatePickerViewTrigger>
|
|
||||||
<DatePickerNextTrigger />
|
|
||||||
</DatePickerViewControl>
|
|
||||||
<DatePickerTable>
|
|
||||||
<DatePickerTableHead>
|
|
||||||
<DatePickerTableRow>
|
|
||||||
<DatePickerTableHeader
|
|
||||||
v-for="(weekDay, id) in datePicker?.weekDays"
|
|
||||||
:key="id"
|
|
||||||
>
|
|
||||||
{{ weekDay.short }}
|
|
||||||
</DatePickerTableHeader>
|
|
||||||
</DatePickerTableRow>
|
|
||||||
</DatePickerTableHead>
|
|
||||||
<DatePickerTableBody>
|
|
||||||
<DatePickerTableRow v-for="(week, id) in datePicker?.weeks" :key="id">
|
|
||||||
<DatePickerTableCell v-for="(day, id) in week" :key="id" :value="day">
|
|
||||||
<DatePickerTableCellTrigger>
|
|
||||||
{{ day.day }}
|
|
||||||
</DatePickerTableCellTrigger>
|
|
||||||
</DatePickerTableCell>
|
|
||||||
</DatePickerTableRow>
|
|
||||||
</DatePickerTableBody>
|
|
||||||
</DatePickerTable>
|
|
||||||
</DatePickerContext>
|
|
||||||
</DatePickerView>
|
|
||||||
<DatePickerView view="month">
|
|
||||||
<DatePickerContext v-slot="{ datePicker }">
|
|
||||||
<DatePickerViewControl>
|
|
||||||
<DatePickerPrevTrigger />
|
|
||||||
<DatePickerViewTrigger>
|
|
||||||
<DatePickerRangeText />
|
|
||||||
</DatePickerViewTrigger>
|
|
||||||
<DatePickerNextTrigger />
|
|
||||||
</DatePickerViewControl>
|
|
||||||
<DatePickerTable>
|
|
||||||
<DatePickerTableBody>
|
|
||||||
<DatePickerTableRow
|
|
||||||
v-for="(months, id) in datePicker?.getMonthsGrid({
|
|
||||||
columns: 4,
|
|
||||||
format: 'short',
|
|
||||||
})"
|
|
||||||
:key="id"
|
|
||||||
>
|
|
||||||
<DatePickerTableCell
|
|
||||||
v-for="(month, id) in months"
|
|
||||||
:key="id"
|
|
||||||
:value="month.value"
|
|
||||||
>
|
|
||||||
<DatePickerTableCellTrigger>{{
|
|
||||||
month.label
|
|
||||||
}}</DatePickerTableCellTrigger>
|
|
||||||
</DatePickerTableCell>
|
|
||||||
</DatePickerTableRow>
|
|
||||||
</DatePickerTableBody>
|
|
||||||
</DatePickerTable>
|
|
||||||
</DatePickerContext>
|
|
||||||
</DatePickerView>
|
|
||||||
<DatePickerView view="year">
|
|
||||||
<DatePickerContext v-slot="{ datePicker }">
|
|
||||||
<DatePickerViewControl>
|
|
||||||
<DatePickerPrevTrigger />
|
|
||||||
<DatePickerViewTrigger>
|
|
||||||
<DatePickerRangeText />
|
|
||||||
</DatePickerViewTrigger>
|
|
||||||
<DatePickerNextTrigger />
|
|
||||||
</DatePickerViewControl>
|
|
||||||
<DatePickerTable>
|
|
||||||
<DatePickerTableBody>
|
|
||||||
<DatePickerTableRow
|
|
||||||
v-for="(years, id) in datePicker?.getYearsGrid({
|
|
||||||
columns: 4,
|
|
||||||
})"
|
|
||||||
:key="id"
|
|
||||||
>
|
|
||||||
<DatePickerTableCell
|
|
||||||
v-for="(year, id) in years"
|
|
||||||
:key="id"
|
|
||||||
:value="year.value"
|
|
||||||
>
|
|
||||||
<DatePickerTableCellTrigger>{{
|
|
||||||
year.label
|
|
||||||
}}</DatePickerTableCellTrigger>
|
|
||||||
</DatePickerTableCell>
|
|
||||||
</DatePickerTableRow>
|
|
||||||
</DatePickerTableBody>
|
|
||||||
</DatePickerTable>
|
|
||||||
</DatePickerContext>
|
|
||||||
</DatePickerView>
|
|
||||||
</DatePickerContent>
|
|
||||||
</DatePickerPositioner>
|
|
||||||
</Teleport>
|
|
||||||
</DatePickerRoot>
|
|
||||||
</Field>
|
|
||||||
<Field class="mt-3">
|
|
||||||
<FieldLabel>Categories</FieldLabel>
|
|
||||||
<ComboboxRoot
|
|
||||||
:collection="categoryCollection"
|
|
||||||
:value="categoryValue"
|
|
||||||
@value-change="(details) => (categoryValue = details.value)"
|
|
||||||
@input-value-change="
|
|
||||||
({ inputValue }) => {
|
|
||||||
const filtered = categoryData.filter((item) =>
|
|
||||||
item.label.toLowerCase().includes(inputValue.toLowerCase()),
|
|
||||||
)
|
|
||||||
categoryOptions = filtered.length > 0 ? filtered : categoryData
|
|
||||||
}
|
|
||||||
"
|
|
||||||
class="w-full"
|
|
||||||
multiple
|
|
||||||
>
|
|
||||||
<ComboboxControl>
|
|
||||||
<ComboboxTrigger />
|
|
||||||
</ComboboxControl>
|
|
||||||
<ComboboxContent>
|
|
||||||
<ComboboxInput placeholder="Search categories..." />
|
|
||||||
<ComboboxItemGroup>
|
|
||||||
<ComboboxItemGroupLabel>Categories</ComboboxItemGroupLabel>
|
|
||||||
<ComboboxItem
|
|
||||||
v-for="item in categoryCollection.items"
|
|
||||||
:key="item.code"
|
|
||||||
:item="item"
|
|
||||||
>
|
|
||||||
<ComboboxItemText>{{ item.label }}</ComboboxItemText>
|
|
||||||
</ComboboxItem>
|
|
||||||
</ComboboxItemGroup>
|
|
||||||
</ComboboxContent>
|
|
||||||
</ComboboxRoot>
|
|
||||||
</Field>
|
|
||||||
<Field class="mt-3">
|
|
||||||
<FieldLabel>Tags</FieldLabel>
|
|
||||||
<ComboboxRoot
|
|
||||||
:collection="tagCollection"
|
|
||||||
:value="tagValue"
|
|
||||||
@value-change="(details) => (tagValue = details.value)"
|
|
||||||
@input-value-change="
|
|
||||||
({ inputValue }) => {
|
|
||||||
const filtered = tagData.filter((item) =>
|
|
||||||
item.label.toLowerCase().includes(inputValue.toLowerCase()),
|
|
||||||
)
|
|
||||||
tagOptions = filtered.length > 0 ? filtered : tagData
|
|
||||||
}
|
|
||||||
"
|
|
||||||
class="w-full"
|
|
||||||
multiple
|
|
||||||
>
|
|
||||||
<ComboboxControl>
|
|
||||||
<ComboboxTrigger />
|
|
||||||
</ComboboxControl>
|
|
||||||
<ComboboxContent>
|
|
||||||
<ComboboxInput placeholder="Search tags..." />
|
|
||||||
<ComboboxItemGroup>
|
|
||||||
<ComboboxItemGroupLabel>Tags</ComboboxItemGroupLabel>
|
|
||||||
<ComboboxItem v-for="item in tagCollection.items" :key="item.code" :item="item">
|
|
||||||
<ComboboxItemText>{{ item.label }}</ComboboxItemText>
|
|
||||||
</ComboboxItem>
|
|
||||||
</ComboboxItemGroup>
|
|
||||||
</ComboboxContent>
|
|
||||||
</ComboboxRoot>
|
|
||||||
</Field>
|
|
||||||
<Field class="mt-3">
|
|
||||||
<FieldLabel>Published</FieldLabel>
|
|
||||||
<SwitchRoot>
|
|
||||||
<SwitchControl />
|
|
||||||
</SwitchRoot>
|
|
||||||
</Field>
|
|
||||||
<Field class="mt-3">
|
|
||||||
<FieldLabel>Show Author Name</FieldLabel>
|
|
||||||
<SwitchRoot>
|
|
||||||
<SwitchControl />
|
|
||||||
</SwitchRoot>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
<!-- END: Post Info -->
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import fakers from '@/utils/faker'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Box } from '@/components/ui/box'
|
|
||||||
import { MenuRoot, MenuTrigger, MenuPositioner, MenuContent, MenuItem } from '@/components/ui/menu'
|
|
||||||
|
|
||||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
||||||
import {
|
|
||||||
PaginationContext,
|
|
||||||
PaginationRoot,
|
|
||||||
PaginationItem,
|
|
||||||
PaginationPrevTrigger,
|
|
||||||
PaginationNextTrigger,
|
|
||||||
PaginationEllipsis,
|
|
||||||
} from '@/components/ui/pagination'
|
|
||||||
import {
|
|
||||||
TooltipRoot,
|
|
||||||
TooltipTrigger,
|
|
||||||
TooltipPositioner,
|
|
||||||
TooltipContent,
|
|
||||||
} from '@/components/ui/tooltip'
|
|
||||||
import { Lucide } from '@/components/ui/lucide'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-lg font-medium">Users Layout</h2>
|
|
||||||
<div class="mt-5 grid grid-cols-12 gap-x-6 gap-y-8">
|
|
||||||
<div class="col-span-12 mt-2 flex flex-wrap items-center sm:flex-nowrap">
|
|
||||||
<Button class="mr-2" look="outline" variant="primary"> Add New User </Button>
|
|
||||||
<MenuRoot class="w-auto">
|
|
||||||
<MenuTrigger as-child>
|
|
||||||
<Box class="px-2" as-child>
|
|
||||||
<Button variant="ghost" class="box flex items-center">
|
|
||||||
<span class="flex h-5 w-5 items-center justify-center">
|
|
||||||
<Lucide class="h-4 w-4" icon="Plus" />
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPositioner>
|
|
||||||
<MenuContent class="w-40">
|
|
||||||
<MenuItem value="group">
|
|
||||||
<Lucide class="mr-2 h-4 w-4" icon="Users" /> Add Group
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="message">
|
|
||||||
<Lucide class="mr-2 h-4 w-4" icon="MessageCircle" /> Send Message
|
|
||||||
</MenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</MenuPositioner>
|
|
||||||
</MenuRoot>
|
|
||||||
<div class="mx-auto hidden opacity-70 md:block">Showing 1 to 10 of 150 entries</div>
|
|
||||||
<div class="mt-3 w-full sm:ml-auto sm:mt-0 sm:w-auto md:ml-0">
|
|
||||||
<div class="relative w-56">
|
|
||||||
<Input class="w-56 pr-10" type="text" placeholder="Search..." />
|
|
||||||
<Lucide class="absolute inset-y-0 right-0 my-auto mr-3 h-4 w-4" icon="Search" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- BEGIN: Users Layout -->
|
|
||||||
<div v-for="(faker, index) in fakers.slice(0, 10)" :key="index" class="col-span-12 md:col-span-6">
|
|
||||||
<Box raised="single" class="p-0">
|
|
||||||
<div class="flex flex-col items-center border-b border-foreground/15 p-5 lg:flex-row">
|
|
||||||
<AvatarRoot class="size-24 bg-background rounded-full lg:mr-1 lg:size-12">
|
|
||||||
<AvatarFallback>IM</AvatarFallback>
|
|
||||||
<AvatarImage :src="faker['images'][2]" :alt="'Uploaded at ' + faker['dates'][2]" />
|
|
||||||
</AvatarRoot>
|
|
||||||
<div class="mt-3 text-center lg:ml-2 lg:mr-auto lg:mt-0 lg:text-left">
|
|
||||||
<a class="font-medium" href="">
|
|
||||||
{{ faker['users'][0]!['name'] }}
|
|
||||||
</a>
|
|
||||||
<div class="mt-0.5 text-xs opacity-70">
|
|
||||||
{{ faker['jobs'][0] }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="-ml-2 mt-3 flex lg:ml-0 lg:mt-0 lg:justify-end">
|
|
||||||
<TooltipRoot>
|
|
||||||
<TooltipTrigger as-child>
|
|
||||||
<a class="ml-2 flex h-8 w-8 items-center justify-center rounded-full border border-foreground/15"
|
|
||||||
href="">
|
|
||||||
<Lucide class="h-3 w-3 fill-current opacity-50" icon="Facebook" />
|
|
||||||
</a>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPositioner>
|
|
||||||
<TooltipContent>Facebook</TooltipContent>
|
|
||||||
</TooltipPositioner>
|
|
||||||
</TooltipRoot>
|
|
||||||
<TooltipRoot>
|
|
||||||
<TooltipTrigger as-child>
|
|
||||||
<a class="ml-2 flex h-8 w-8 items-center justify-center rounded-full border border-foreground/15"
|
|
||||||
href="">
|
|
||||||
<Lucide class="h-3 w-3 fill-current opacity-50" icon="Twitter" />
|
|
||||||
</a>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPositioner>
|
|
||||||
<TooltipContent>Twitter</TooltipContent>
|
|
||||||
</TooltipPositioner>
|
|
||||||
</TooltipRoot>
|
|
||||||
<TooltipRoot>
|
|
||||||
<TooltipTrigger as-child>
|
|
||||||
<a class="ml-2 flex h-8 w-8 items-center justify-center rounded-full border border-foreground/15"
|
|
||||||
href="">
|
|
||||||
<Lucide class="h-3 w-3 fill-current opacity-50" icon="Linkedin" />
|
|
||||||
</a>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPositioner>
|
|
||||||
<TooltipContent>Linked In</TooltipContent>
|
|
||||||
</TooltipPositioner>
|
|
||||||
</TooltipRoot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center justify-center p-5 lg:flex-nowrap">
|
|
||||||
<div class="mb-4 mr-auto w-full lg:mb-0 lg:w-1/2">
|
|
||||||
<div class="flex text-xs opacity-70">
|
|
||||||
<div class="mr-auto">Progress</div>
|
|
||||||
<div>20%</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 h-1 rounded-full bg-foreground/10">
|
|
||||||
<div class="h-full w-[20%] rounded-full bg-primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button class="mr-2" size="sm" look="outline"> Message </Button>
|
|
||||||
<Button variant="primary" size="sm"> View Profile </Button>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
<!-- END: Users Layout -->
|
|
||||||
<!-- BEGIN: Pagination -->
|
|
||||||
<div class="col-span-12 flex flex-wrap items-center sm:flex-row sm:flex-nowrap">
|
|
||||||
<PaginationRoot :count="150" :pageSize="10" :siblingCount="1" class="w-full sm:mr-auto sm:w-auto">
|
|
||||||
<PaginationPrevTrigger>Previous</PaginationPrevTrigger>
|
|
||||||
<PaginationContext v-slot="{ pagination }">
|
|
||||||
<template v-for="(page, index) in pagination?.pages" :key="index">
|
|
||||||
<PaginationItem v-if="page.type === 'page'" v-bind="{ ...page }">
|
|
||||||
{{ page.value }}
|
|
||||||
</PaginationItem>
|
|
||||||
<PaginationEllipsis v-else :index="index" />
|
|
||||||
</template>
|
|
||||||
</PaginationContext>
|
|
||||||
<PaginationNextTrigger>Next</PaginationNextTrigger>
|
|
||||||
</PaginationRoot>
|
|
||||||
<NativeSelect class="mt-3 w-20 sm:mt-0">
|
|
||||||
<NativeSelectOption value="10">10</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="25">25</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="35">35</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="50">50</NativeSelectOption>
|
|
||||||
</NativeSelect>
|
|
||||||
</div>
|
|
||||||
<!-- END: Pagination -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import fakers from '@/utils/faker'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Box } from '@/components/ui/box'
|
|
||||||
import { MenuRoot, MenuTrigger, MenuPositioner, MenuContent, MenuItem } from '@/components/ui/menu'
|
|
||||||
|
|
||||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
||||||
import {
|
|
||||||
PaginationContext,
|
|
||||||
PaginationRoot,
|
|
||||||
PaginationItem,
|
|
||||||
PaginationPrevTrigger,
|
|
||||||
PaginationNextTrigger,
|
|
||||||
PaginationEllipsis,
|
|
||||||
} from '@/components/ui/pagination'
|
|
||||||
import { Lucide } from '@/components/ui/lucide'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-lg font-medium">Users Layout</h2>
|
|
||||||
<div class="mt-5 grid grid-cols-12 gap-x-6 gap-y-8">
|
|
||||||
<div class="col-span-12 mt-2 flex flex-wrap items-center sm:flex-nowrap">
|
|
||||||
<Button class="mr-2" look="outline" variant="primary"> Add New User </Button>
|
|
||||||
<MenuRoot class="w-auto">
|
|
||||||
<MenuTrigger as-child>
|
|
||||||
<Box class="px-2" as-child>
|
|
||||||
<Button variant="ghost" class="box flex items-center">
|
|
||||||
<span class="flex h-5 w-5 items-center justify-center">
|
|
||||||
<Lucide class="h-4 w-4" icon="Plus" />
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPositioner>
|
|
||||||
<MenuContent class="w-40">
|
|
||||||
<MenuItem value="group">
|
|
||||||
<Lucide class="mr-2 h-4 w-4" icon="Users" /> Add Group
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="message">
|
|
||||||
<Lucide class="mr-2 h-4 w-4" icon="MessageCircle" /> Send Message
|
|
||||||
</MenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</MenuPositioner>
|
|
||||||
</MenuRoot>
|
|
||||||
<div class="mx-auto hidden opacity-70 md:block">Showing 1 to 10 of 150 entries</div>
|
|
||||||
<div class="mt-3 w-full sm:ml-auto sm:mt-0 sm:w-auto md:ml-0">
|
|
||||||
<div class="relative w-56">
|
|
||||||
<Input class="w-56 pr-10" type="text" placeholder="Search..." />
|
|
||||||
<Lucide class="absolute inset-y-0 right-0 my-auto mr-3 h-4 w-4" icon="Search" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- BEGIN: Users Layout -->
|
|
||||||
<div v-for="(faker, index) in fakers" :key="index" class="col-span-12 md:col-span-6">
|
|
||||||
<Box raised="single">
|
|
||||||
<div class="flex flex-col items-center lg:flex-row">
|
|
||||||
<AvatarRoot class="size-24 bg-background rounded-full lg:mr-1 lg:size-12">
|
|
||||||
<AvatarFallback>IM</AvatarFallback>
|
|
||||||
<AvatarImage :src="faker['images'][2]" :alt="'Uploaded at ' + faker['dates'][2]" />
|
|
||||||
</AvatarRoot>
|
|
||||||
<div class="mt-3 text-center lg:ml-2 lg:mr-auto lg:mt-0 lg:text-left">
|
|
||||||
<a class="font-medium" href="">
|
|
||||||
{{ faker['users'][0]!['name'] }}
|
|
||||||
</a>
|
|
||||||
<div class="mt-0.5 text-xs opacity-70">
|
|
||||||
{{ faker['jobs'][0] }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 flex lg:mt-0">
|
|
||||||
<Button class="mr-2" size="sm" look="outline"> View Profile </Button>
|
|
||||||
<Button variant="primary" size="sm"> Message </Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
<!-- END: Users Layout -->
|
|
||||||
<!-- BEGIN: Pagination -->
|
|
||||||
<div class="col-span-12 flex flex-wrap items-center sm:flex-row sm:flex-nowrap">
|
|
||||||
<PaginationRoot :count="150" :pageSize="10" :siblingCount="1" class="w-full sm:mr-auto sm:w-auto">
|
|
||||||
<PaginationPrevTrigger>Previous</PaginationPrevTrigger>
|
|
||||||
<PaginationContext v-slot="{ pagination }">
|
|
||||||
<template v-for="(page, index) in pagination?.pages" :key="index">
|
|
||||||
<PaginationItem v-if="page.type === 'page'" v-bind="{ ...page }">
|
|
||||||
{{ page.value }}
|
|
||||||
</PaginationItem>
|
|
||||||
<PaginationEllipsis v-else :index="index" />
|
|
||||||
</template>
|
|
||||||
</PaginationContext>
|
|
||||||
<PaginationNextTrigger>Next</PaginationNextTrigger>
|
|
||||||
</PaginationRoot>
|
|
||||||
<NativeSelect class="mt-3 w-20 sm:mt-0">
|
|
||||||
<NativeSelectOption value="10">10</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="25">25</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="35">35</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="50">50</NativeSelectOption>
|
|
||||||
</NativeSelect>
|
|
||||||
</div>
|
|
||||||
<!-- END: Pagination -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import fakers from '@/utils/faker'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Box } from '@/components/ui/box'
|
|
||||||
import { MenuRoot, MenuTrigger, MenuPositioner, MenuContent, MenuItem } from '@/components/ui/menu'
|
|
||||||
|
|
||||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
||||||
import {
|
|
||||||
PaginationContext,
|
|
||||||
PaginationRoot,
|
|
||||||
PaginationItem,
|
|
||||||
PaginationPrevTrigger,
|
|
||||||
PaginationNextTrigger,
|
|
||||||
PaginationEllipsis,
|
|
||||||
} from '@/components/ui/pagination'
|
|
||||||
import { Lucide } from '@/components/ui/lucide'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-lg font-medium">Users Layout</h2>
|
|
||||||
<div class="mt-5 grid grid-cols-12 gap-x-6 gap-y-8">
|
|
||||||
<div class="col-span-12 mt-2 flex flex-wrap items-center sm:flex-nowrap">
|
|
||||||
<Button class="mr-2" look="outline" variant="primary"> Add New User </Button>
|
|
||||||
<MenuRoot class="w-auto">
|
|
||||||
<MenuTrigger as-child>
|
|
||||||
<Box class="px-2" as-child>
|
|
||||||
<Button variant="ghost" class="box flex items-center">
|
|
||||||
<span class="flex h-5 w-5 items-center justify-center">
|
|
||||||
<Lucide class="h-4 w-4" icon="Plus" />
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPositioner>
|
|
||||||
<MenuContent class="w-40">
|
|
||||||
<MenuItem value="group">
|
|
||||||
<Lucide class="mr-2 h-4 w-4" icon="Users" /> Add Group
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="message">
|
|
||||||
<Lucide class="mr-2 h-4 w-4" icon="MessageCircle" /> Send Message
|
|
||||||
</MenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</MenuPositioner>
|
|
||||||
</MenuRoot>
|
|
||||||
<div class="mx-auto hidden opacity-70 md:block">Showing 1 to 10 of 150 entries</div>
|
|
||||||
<div class="mt-3 w-full sm:ml-auto sm:mt-0 sm:w-auto md:ml-0">
|
|
||||||
<div class="relative w-56">
|
|
||||||
<Input class="w-56 pr-10" type="text" placeholder="Search..." />
|
|
||||||
<Lucide class="absolute inset-y-0 right-0 my-auto mr-3 h-4 w-4" icon="Search" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- BEGIN: Users Layout -->
|
|
||||||
<div v-for="(faker, index) in fakers.slice(0, 9)" :key="index" class="col-span-12 md:col-span-6 lg:col-span-4">
|
|
||||||
<Box raised="single" class="p-0">
|
|
||||||
<div class="flex items-start px-5 pt-5">
|
|
||||||
<div class="flex w-full flex-col items-center lg:flex-row">
|
|
||||||
<AvatarRoot class="size-14 bg-background rounded-full">
|
|
||||||
<AvatarFallback>IM</AvatarFallback>
|
|
||||||
<AvatarImage :src="faker['photos'][0]" :alt="faker['users'][0]!['name']" />
|
|
||||||
</AvatarRoot>
|
|
||||||
<div class="mt-3 text-center lg:ml-4 lg:mt-0 lg:text-left">
|
|
||||||
<a class="font-medium" href="">
|
|
||||||
{{ faker['users'][0]!['name'] }}
|
|
||||||
</a>
|
|
||||||
<div class="mt-0.5 text-xs opacity-70">
|
|
||||||
{{ faker['jobs'][0] }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MenuRoot class="w-auto absolute right-0 top-0 mr-5 mt-3">
|
|
||||||
<MenuTrigger as-child>
|
|
||||||
<a class="block size-5" href="#">
|
|
||||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
|
||||||
</a>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPositioner>
|
|
||||||
<MenuContent class="w-40">
|
|
||||||
<MenuItem value="edit">
|
|
||||||
<Lucide class="mr-2 h-4 w-4" icon="Edit" /> Edit
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="delete">
|
|
||||||
<Lucide class="mr-2 h-4 w-4" icon="Trash" /> Delete
|
|
||||||
</MenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</MenuPositioner>
|
|
||||||
</MenuRoot>
|
|
||||||
</div>
|
|
||||||
<div class="p-5 text-center lg:text-left">
|
|
||||||
<div>{{ faker['news'][0]!['shortContent'] }}</div>
|
|
||||||
<div class="mt-5 flex items-center justify-center opacity-70 lg:justify-start">
|
|
||||||
<Lucide class="mr-2 size-3.5" icon="Mail" />
|
|
||||||
{{ faker['users'][0]!['email'] }}
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 flex items-center justify-center opacity-70 lg:justify-start">
|
|
||||||
<Lucide class="mr-2 size-3.5" icon="Instagram" />
|
|
||||||
{{ faker['users'][0]!['name'] }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="border-t border-foreground/15 p-5 text-center lg:text-right">
|
|
||||||
<Button class="mr-2" size="sm" look="outline"> Message </Button>
|
|
||||||
<Button variant="primary" size="sm"> View Profile </Button>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
<!-- END: Users Layout -->
|
|
||||||
<!-- BEGIN: Pagination -->
|
|
||||||
<div class="col-span-12 flex flex-wrap items-center sm:flex-row sm:flex-nowrap">
|
|
||||||
<PaginationRoot :count="150" :pageSize="10" :siblingCount="1" class="w-full sm:mr-auto sm:w-auto">
|
|
||||||
<PaginationPrevTrigger>Previous</PaginationPrevTrigger>
|
|
||||||
<PaginationContext v-slot="{ pagination }">
|
|
||||||
<template v-for="(page, index) in pagination?.pages" :key="index">
|
|
||||||
<PaginationItem v-if="page.type === 'page'" v-bind="{ ...page }">
|
|
||||||
{{ page.value }}
|
|
||||||
</PaginationItem>
|
|
||||||
<PaginationEllipsis v-else :index="index" />
|
|
||||||
</template>
|
|
||||||
</PaginationContext>
|
|
||||||
<PaginationNextTrigger>Next</PaginationNextTrigger>
|
|
||||||
</PaginationRoot>
|
|
||||||
<NativeSelect class="mt-3 w-20 sm:mt-0">
|
|
||||||
<NativeSelectOption value="10">10</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="25">25</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="35">35</NativeSelectOption>
|
|
||||||
<NativeSelectOption value="50">50</NativeSelectOption>
|
|
||||||
</NativeSelect>
|
|
||||||
</div>
|
|
||||||
<!-- END: Pagination -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Box } from '@/components/ui/box'
|
|
||||||
import { Field, FieldLabel } from '@/components/ui/field'
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<h2 class="mr-auto text-lg font-medium">Wizard Layout</h2>
|
|
||||||
</div>
|
|
||||||
<!-- BEGIN: Wizard Layout -->
|
|
||||||
<Box class="mt-5 py-10 sm:py-20">
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<Button class="mx-2 size-14 rounded-full shadow-none"> 1 </Button>
|
|
||||||
<Button variant="ghost" class="bg-background border border-foreground/15 shadow-none mx-2 size-14 rounded-full">
|
|
||||||
2
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" class="bg-background border border-foreground/15 shadow-none mx-2 size-14 rounded-full">
|
|
||||||
3
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="mt-10 px-5">
|
|
||||||
<div class="text-center text-lg font-medium">Setup Your Account</div>
|
|
||||||
<div class="mt-2 text-center opacity-70">
|
|
||||||
To start off, please enter your username, email address and password.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-10 border-t border-foreground/10 px-5 pt-10 sm:px-20">
|
|
||||||
<div class="text-base font-medium">Profile Settings</div>
|
|
||||||
<div class="mt-5 grid grid-cols-12 gap-4 gap-y-5">
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-1">From</FieldLabel>
|
|
||||||
<Input id="input-wizard-1" type="text" placeholder="example@gmail.com" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-2">To</FieldLabel>
|
|
||||||
<Input id="input-wizard-2" type="text" placeholder="example@gmail.com" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-3">Subject</FieldLabel>
|
|
||||||
<Input id="input-wizard-3" type="text" placeholder="Important Meeting" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-4">Has the Words</FieldLabel>
|
|
||||||
<Input id="input-wizard-4" type="text" placeholder="Job, Work, Documentation" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-5">Doesn't Have</FieldLabel>
|
|
||||||
<Input id="input-wizard-5" type="text" placeholder="Job, Work, Documentation" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-6">Size</FieldLabel>
|
|
||||||
<NativeSelect id="input-wizard-6">
|
|
||||||
<NativeSelectOption>10</NativeSelectOption>
|
|
||||||
<NativeSelectOption>25</NativeSelectOption>
|
|
||||||
<NativeSelectOption>35</NativeSelectOption>
|
|
||||||
<NativeSelectOption>50</NativeSelectOption>
|
|
||||||
</NativeSelect>
|
|
||||||
</Field>
|
|
||||||
<div class="col-span-12 mt-5 flex items-center justify-center sm:justify-end">
|
|
||||||
<Button look="outline" class="w-32">Previous</Button>
|
|
||||||
<Button class="ml-2 w-32">Next</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
<!-- END: Wizard Layout -->
|
|
||||||
</template>
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Box } from '@/components/ui/box'
|
|
||||||
import { Field, FieldLabel } from '@/components/ui/field'
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<h2 class="mr-auto text-lg font-medium">Wizard Layout</h2>
|
|
||||||
</div>
|
|
||||||
<!-- BEGIN: Wizard Layout -->
|
|
||||||
<Box class="mt-5 py-10 sm:py-20">
|
|
||||||
<div class="grid gap-5 px-5 sm:px-20">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Button class="mx-2 size-12 rounded-full shadow-none"> 1 </Button>
|
|
||||||
<div class="text-primary ml-3 text-base font-medium opacity-100">Create New Account</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Button variant="ghost" class="bg-background border border-foreground/15 shadow-none mx-2 size-12 rounded-full">
|
|
||||||
2
|
|
||||||
</Button>
|
|
||||||
<div class="ml-3 text-base opacity-70">Setup Your Profile</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Button variant="ghost" class="bg-background border border-foreground/15 shadow-none mx-2 size-12 rounded-full">
|
|
||||||
3
|
|
||||||
</Button>
|
|
||||||
<div class="ml-3 text-base opacity-70">Setup Your Business Details</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Button variant="ghost" class="bg-background border border-foreground/15 shadow-none mx-2 size-12 rounded-full">
|
|
||||||
4
|
|
||||||
</Button>
|
|
||||||
<div class="ml-3 text-base opacity-70">Setup Billing Account</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Button variant="ghost" class="bg-background border border-foreground/15 shadow-none mx-2 size-12 rounded-full">
|
|
||||||
5
|
|
||||||
</Button>
|
|
||||||
<div class="ml-3 text-base opacity-70">Finalize your purchase</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-10 border-t border-foreground/10 px-5 pt-10 sm:px-20">
|
|
||||||
<div class="text-base font-medium">Profile Settings</div>
|
|
||||||
<div class="mt-5 grid grid-cols-12 gap-4 gap-y-5">
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-1">From</FieldLabel>
|
|
||||||
<Input id="input-wizard-1" type="text" placeholder="example@gmail.com" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-2">To</FieldLabel>
|
|
||||||
<Input id="input-wizard-2" type="text" placeholder="example@gmail.com" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-3">Subject</FieldLabel>
|
|
||||||
<Input id="input-wizard-3" type="text" placeholder="Important Meeting" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-4">Has the Words</FieldLabel>
|
|
||||||
<Input id="input-wizard-4" type="text" placeholder="Job, Work, Documentation" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-5">Doesn't Have</FieldLabel>
|
|
||||||
<Input id="input-wizard-5" type="text" placeholder="Job, Work, Documentation" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-6">Size</FieldLabel>
|
|
||||||
<NativeSelect id="input-wizard-6">
|
|
||||||
<NativeSelectOption>10</NativeSelectOption>
|
|
||||||
<NativeSelectOption>25</NativeSelectOption>
|
|
||||||
<NativeSelectOption>35</NativeSelectOption>
|
|
||||||
<NativeSelectOption>50</NativeSelectOption>
|
|
||||||
</NativeSelect>
|
|
||||||
</Field>
|
|
||||||
<div class="col-span-12 mt-5 flex items-center justify-center sm:justify-end">
|
|
||||||
<Button look="outline" class="w-32">Previous</Button>
|
|
||||||
<Button class="ml-2 w-32">Next</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
<!-- END: Wizard Layout -->
|
|
||||||
</template>
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Box } from '@/components/ui/box'
|
|
||||||
import { Field, FieldLabel } from '@/components/ui/field'
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<h2 class="mr-auto text-lg font-medium">Wizard Layout</h2>
|
|
||||||
</div>
|
|
||||||
<!-- BEGIN: Wizard Layout -->
|
|
||||||
<Box class="mt-5 py-10 sm:py-20">
|
|
||||||
<div
|
|
||||||
class="before:bg-foreground/10 relative flex flex-col justify-center px-5 before:absolute before:bottom-0 before:top-0 before:mt-6 before:hidden before:h-0.5 before:w-[69%] sm:px-20 lg:flex-row before:lg:block">
|
|
||||||
<div class="z-10 flex flex-1 items-center lg:block lg:text-center">
|
|
||||||
<Button class="mx-2 size-12 rounded-full shadow-none"> 1 </Button>
|
|
||||||
<div class="text-primary ml-3 font-medium opacity-100 lg:mx-auto lg:mt-3 lg:w-32">
|
|
||||||
Create New Account
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="z-10 flex flex-1 items-center lg:block lg:text-center">
|
|
||||||
<Button variant="ghost" class="bg-background border border-foreground/15 shadow-none mx-2 size-12 rounded-full">
|
|
||||||
2
|
|
||||||
</Button>
|
|
||||||
<div class="ml-3 opacity-70 lg:mx-auto lg:mt-3 lg:w-32">Setup Your Profile</div>
|
|
||||||
</div>
|
|
||||||
<div class="z-10 flex flex-1 items-center lg:block lg:text-center">
|
|
||||||
<Button variant="ghost" class="bg-background border border-foreground/15 shadow-none mx-2 size-12 rounded-full">
|
|
||||||
3
|
|
||||||
</Button>
|
|
||||||
<div class="ml-3 opacity-70 lg:mx-auto lg:mt-3 lg:w-32">Setup Your Business Details</div>
|
|
||||||
</div>
|
|
||||||
<div class="z-10 flex flex-1 items-center lg:block lg:text-center">
|
|
||||||
<Button variant="ghost" class="bg-background border border-foreground/15 shadow-none mx-2 size-12 rounded-full">
|
|
||||||
4
|
|
||||||
</Button>
|
|
||||||
<div class="ml-3 opacity-70 lg:mx-auto lg:mt-3 lg:w-32">Setup Billing Account</div>
|
|
||||||
</div>
|
|
||||||
<div class="z-10 flex flex-1 items-center lg:block lg:text-center">
|
|
||||||
<Button variant="ghost" class="bg-background border border-foreground/15 shadow-none mx-2 size-12 rounded-full">
|
|
||||||
5
|
|
||||||
</Button>
|
|
||||||
<div class="ml-3 opacity-70 lg:mx-auto lg:mt-3 lg:w-32">Finalize your purchase</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-10 border-t border-foreground/10 px-5 pt-10 sm:px-20">
|
|
||||||
<div class="text-base font-medium">Profile Settings</div>
|
|
||||||
<div class="mt-5 grid grid-cols-12 gap-4 gap-y-5">
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-1">From</FieldLabel>
|
|
||||||
<Input id="input-wizard-1" type="text" placeholder="example@gmail.com" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-2">To</FieldLabel>
|
|
||||||
<Input id="input-wizard-2" type="text" placeholder="example@gmail.com" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-3">Subject</FieldLabel>
|
|
||||||
<Input id="input-wizard-3" type="text" placeholder="Important Meeting" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-4">Has the Words</FieldLabel>
|
|
||||||
<Input id="input-wizard-4" type="text" placeholder="Job, Work, Documentation" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-5">Doesn't Have</FieldLabel>
|
|
||||||
<Input id="input-wizard-5" type="text" placeholder="Job, Work, Documentation" />
|
|
||||||
</Field>
|
|
||||||
<Field class="col-span-12 sm:col-span-6">
|
|
||||||
<FieldLabel for="input-wizard-6">Size</FieldLabel>
|
|
||||||
<NativeSelect id="input-wizard-6">
|
|
||||||
<NativeSelectOption>10</NativeSelectOption>
|
|
||||||
<NativeSelectOption>25</NativeSelectOption>
|
|
||||||
<NativeSelectOption>35</NativeSelectOption>
|
|
||||||
<NativeSelectOption>50</NativeSelectOption>
|
|
||||||
</NativeSelect>
|
|
||||||
</Field>
|
|
||||||
<div class="col-span-12 mt-5 flex items-center justify-center sm:justify-end">
|
|
||||||
<Button look="outline" class="w-32">Previous</Button>
|
|
||||||
<Button class="ml-2 w-32">Next</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
<!-- END: Wizard Layout -->
|
|
||||||
</template>
|
|
||||||
Reference in New Issue
Block a user