feat: convert all backoffice CRUD from modals to dedicated pages

Replaced modal-based create/edit/delete with dedicated TanStack Router
pages for 7 modules:

- Roles: /roles, /roles/create, /roles/$id
- Permissions: /permissions, /permissions/create, /permissions/$id
- CMS Events: /cms-events, /cms-events/create, /cms-events/$id
- CMS Testimonials: /cms-testimonials, /cms-testimonials/create, /cms-testimonials/$id
- Accounts: /accounts, /accounts/$id
- Gacha Dashboard: /dashboard, /dashboard/create, /dashboard/$id
- Gacha Rolls: /gacha-roll, /gacha-roll/create, /gacha-roll/$id

Each module now has:
- List page with DataTable + inline delete confirmation
- Dedicated create page with form + back navigation
- Dedicated edit page with pre-filled form + back navigation
- No more modals for CRUD operations

Deleted 21 modal component files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-11 22:02:57 +07:00
co-authored by Claude Opus 4.6
parent 00b0997a9b
commit 8749e83dad
39 changed files with 1935 additions and 3021 deletions
@@ -1,162 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useEffect, useState } from 'react';
interface IModalEditAccount {
isOpen: boolean;
onClose: () => void;
handleEditAccount?: () => Promise<void>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { fullname?: string; email?: string };
onDataCapture?: (data: any) => void;
}
const ModalEditAccount = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleEditAccount,
initialValues,
onDataCapture,
}: IModalEditAccount) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && (
<StepOne
nextStep={nextStep}
onClose={onClose}
initialValues={initialValues}
onDataCapture={onDataCapture}
/>
)}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleEditAccount={handleEditAccount}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
initialValues?: { fullname?: string; email?: string };
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
const [fullName, setFullName] = useState(initialValues?.fullname ?? '');
const [email, setEmail] = useState(initialValues?.email ?? '');
useEffect(() => {
setFullName(initialValues?.fullname ?? '');
setEmail(initialValues?.email ?? '');
}, [initialValues]);
return (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Edit Data Akun
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<InputField
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
size="lg"
className="w-full"
/>
<InputField
label="Email"
type="text"
placeholder="Masukkan Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
size="lg"
className="w-full"
/>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={() => {
onDataCapture?.({ fullname: fullName, email });
nextStep();
}}
>
Perbarui Data
</Button>
</Modal.Content>
</>
);
};
interface IStepTwoProps {
onClose: () => void;
handleEditAccount?: () => Promise<void>;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleEditAccount, resetStep }: IStepTwoProps) => (
<>
<Modal.Header className="mb-0 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Data
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin dengan
<br /> perubahan yang dilakukan?
</p>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => {
onClose();
resetStep();
}}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={async () => {
if (handleEditAccount) await handleEditAccount();
onClose();
resetStep();
}}
>
Update
</Button>
</Modal.Content>
</>
);
export default ModalEditAccount;
@@ -1,202 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
import { useItem, useConfirmItem } from '../../_hooks/cms-events/use-item';
interface IModalAddEvent {
isOpen: boolean;
onClose: () => void;
handleAdd?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddEvent = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleAdd,
onDataCapture,
}: IModalAddEvent) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleAdd={handleAdd}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Event
</h2>
</Modal.Header>
<Modal.Content>
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Nama Event"
name="name"
type="text"
placeholder="Masukkan Nama Event"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Deskripsi"
name="description"
type="text"
placeholder="Masukkan Deskripsi Event"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Link Detail"
name="detail_link"
type="text"
placeholder="Masukkan Link Detail"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Lokasi"
name="location"
type="text"
placeholder="Masukkan Lokasi"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Harga"
name="price"
type="number"
placeholder="Masukkan Harga"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Tanggal Mulai"
name="start_date"
type="date"
placeholder="Pilih Tanggal Mulai"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Tanggal Selesai"
name="end_date"
type="date"
placeholder="Pilih Tanggal Selesai"
size="lg"
className="w-full"
/>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="is_online"
className="rounded"
{...form.register('is_online')}
/>
<label htmlFor="is_online" className="text-p3 font-medium text-neutral-800">
Event Online
</label>
</div>
</div>
<Button variant="primary" size="lg" className="w-full" type="submit">
Tambah Event
</Button>
</form>
</Modal.Content>
</>
);
};
interface IStepTwoProps {
onClose: () => void;
handleAdd?: () => Promise<boolean>;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleAdd, resetStep }: IStepTwoProps) => {
const { onConfirm, onCancel } = useConfirmItem(
onClose,
resetStep,
handleAdd,
{
success: 'Data event berhasil ditambahkan',
error: 'Data event gagal ditambahkan',
}
);
return (
<>
<Modal.Header className="mb-10 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Event
</h2>
<p className="text-p3 text-center text-neutral-400">
Apakah kamu yakin ingin
<br /> menambahkan event ini?
</p>
</Modal.Header>
<Modal.Content className="flex mb-0 gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={onCancel}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={onConfirm}
>
Tambahkan
</Button>
</Modal.Content>
</>
);
};
export default ModalAddEvent;
@@ -1,72 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { useConfirmItem } from '../../_hooks/cms-events/use-item';
interface IModalDeleteEvent {
isOpen: boolean;
onClose: () => void;
handleDelete?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
}
const ModalDeleteEvent = ({
isOpen,
onClose,
resetStep,
handleDelete,
}: IModalDeleteEvent) => {
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
success: 'Data event berhasil dihapus',
error: 'Data event gagal dihapus',
});
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={onClose}
closeButtonClassName="hidden"
>
<Modal.Header className="gap-8">
<img
src="/chibi-delete.webp"
alt="Delete?"
width={148}
className="self-center"
/>
<div className="text-center">
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
Delete Event
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin untuk menghapus event ini? Menghapus data ini
mungkin akan mempengaruhi fungsional sistem
</p>
</div>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="secondary"
size="lg"
className="w-full"
onClick={onClose}
>
Batal Hapus
</Button>
<Button
variant="primary"
size="lg"
className="w-full bg-danger-500 hover:bg-danger-600"
onClick={onConfirm}
>
Hapus Event
</Button>
</Modal.Content>
</Modal>
);
};
export default ModalDeleteEvent;
@@ -1,166 +0,0 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
interface IModalUpdateEvent {
isOpen: boolean;
onClose: () => void;
handleUpdate?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: {
name?: string;
description?: string;
detail_link?: string;
location?: string;
price?: number;
start_date?: string;
end_date?: string;
is_online?: boolean;
};
onDataCapture?: (data: any) => void;
}
const ModalUpdateEvent = ({
isOpen,
onClose,
resetStep,
handleUpdate,
initialValues,
onDataCapture,
}: IModalUpdateEvent) => {
const form = useForm<any>({
mode: 'all',
defaultValues: initialValues,
});
useEffect(() => {
if (isOpen) {
form.reset(initialValues);
}
}, [isOpen, initialValues]);
const onSubmit = form.handleSubmit(async (data) => {
try {
onDataCapture?.(data);
if (handleUpdate) await handleUpdate();
toast.success('Perubahan event berhasil dilakukan');
onClose();
resetStep();
} catch (error) {
console.log(error);
toast.error('Perubahan event gagal dilakukan');
}
});
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
isOpen={isOpen}
onClose={onClose}
disableEscapeKeyDown={true}
>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Event
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Nama Event"
name="name"
type="text"
placeholder="Masukkan Nama Event"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Deskripsi"
name="description"
type="text"
placeholder="Masukkan Deskripsi Event"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Link Detail"
name="detail_link"
type="text"
placeholder="Masukkan Link Detail"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Lokasi"
name="location"
type="text"
placeholder="Masukkan Lokasi"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Harga"
name="price"
type="number"
placeholder="Masukkan Harga"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Tanggal Mulai"
name="start_date"
type="date"
placeholder="Pilih Tanggal Mulai"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Tanggal Selesai"
name="end_date"
type="date"
placeholder="Pilih Tanggal Selesai"
size="lg"
className="w-full"
/>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="is_online_update"
className="rounded"
{...form.register('is_online')}
/>
<label htmlFor="is_online_update" className="text-p3 font-medium text-neutral-800">
Event Online
</label>
</div>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Update Event
</Button>
</form>
</Modal.Content>
</Modal>
);
};
export default ModalUpdateEvent;
@@ -1,146 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
import { useItem, useConfirmItem } from '../../_hooks/cms-testimonials/use-item';
interface IModalAddTestimonial {
isOpen: boolean;
onClose: () => void;
handleAdd?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddTestimonial = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleAdd,
onDataCapture,
}: IModalAddTestimonial) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleAdd={handleAdd}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Testimonial
</h2>
</Modal.Header>
<Modal.Content>
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Role"
name="role"
type="text"
placeholder="Masukkan Role (e.g. Software Engineer)"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Konten Testimonial"
name="content"
type="text"
placeholder="Masukkan Konten Testimonial"
size="lg"
className="w-full"
/>
</div>
<Button variant="primary" size="lg" className="w-full" type="submit">
Tambah Testimonial
</Button>
</form>
</Modal.Content>
</>
);
};
interface IStepTwoProps {
onClose: () => void;
handleAdd?: () => Promise<boolean>;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleAdd, resetStep }: IStepTwoProps) => {
const { onConfirm, onCancel } = useConfirmItem(
onClose,
resetStep,
handleAdd,
{
success: 'Data testimonial berhasil ditambahkan',
error: 'Data testimonial gagal ditambahkan',
}
);
return (
<>
<Modal.Header className="mb-10 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Testimonial
</h2>
<p className="text-p3 text-center text-neutral-400">
Apakah kamu yakin ingin
<br /> menambahkan testimonial ini?
</p>
</Modal.Header>
<Modal.Content className="flex mb-0 gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={onCancel}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={onConfirm}
>
Tambahkan
</Button>
</Modal.Content>
</>
);
};
export default ModalAddTestimonial;
@@ -1,72 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { useConfirmItem } from '../../_hooks/cms-testimonials/use-item';
interface IModalDeleteTestimonial {
isOpen: boolean;
onClose: () => void;
handleDelete?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
}
const ModalDeleteTestimonial = ({
isOpen,
onClose,
resetStep,
handleDelete,
}: IModalDeleteTestimonial) => {
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
success: 'Data testimonial berhasil dihapus',
error: 'Data testimonial gagal dihapus',
});
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={onClose}
closeButtonClassName="hidden"
>
<Modal.Header className="gap-8">
<img
src="/chibi-delete.webp"
alt="Delete?"
width={148}
className="self-center"
/>
<div className="text-center">
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
Delete Testimonial
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin untuk menghapus testimonial ini? Menghapus data ini
mungkin akan mempengaruhi fungsional sistem
</p>
</div>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="secondary"
size="lg"
className="w-full"
onClick={onClose}
>
Batal Hapus
</Button>
<Button
variant="primary"
size="lg"
className="w-full bg-danger-500 hover:bg-danger-600"
onClick={onConfirm}
>
Hapus Testimonial
</Button>
</Modal.Content>
</Modal>
);
};
export default ModalDeleteTestimonial;
@@ -1,104 +0,0 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
interface IModalUpdateTestimonial {
isOpen: boolean;
onClose: () => void;
handleUpdate?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: {
role?: string;
content?: string;
};
onDataCapture?: (data: any) => void;
}
const ModalUpdateTestimonial = ({
isOpen,
onClose,
resetStep,
handleUpdate,
initialValues,
onDataCapture,
}: IModalUpdateTestimonial) => {
const form = useForm<any>({
mode: 'all',
defaultValues: initialValues,
});
useEffect(() => {
if (isOpen) {
form.reset(initialValues);
}
}, [isOpen, initialValues]);
const onSubmit = form.handleSubmit(async (data) => {
try {
onDataCapture?.(data);
if (handleUpdate) await handleUpdate();
toast.success('Perubahan testimonial berhasil dilakukan');
onClose();
resetStep();
} catch (error) {
console.log(error);
toast.error('Perubahan testimonial gagal dilakukan');
}
});
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
isOpen={isOpen}
onClose={onClose}
disableEscapeKeyDown={true}
>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Testimonial
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Role"
name="role"
type="text"
placeholder="Masukkan Role"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Konten Testimonial"
name="content"
type="text"
placeholder="Masukkan Konten Testimonial"
size="lg"
className="w-full"
/>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Update Testimonial
</Button>
</form>
</Modal.Content>
</Modal>
);
};
export default ModalUpdateTestimonial;
@@ -1,164 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { useConfirmItem, useItem } from '../../_hooks/dashboard/use-item';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
interface IModalAddItem {
isOpen: boolean;
onClose: () => void;
handleAddItem?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddItem = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleAddItem,
onDataCapture,
}: IModalAddItem) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleAddItem={handleAddItem}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Item Gacha
</h2>
<p className="text-p3 text-neutral-400">
Lengkapi detail di bawah ini untuk menambahkan item gacha
</p>
</Modal.Header>
<Modal.Content>
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Nama Hadiah"
name="itemName"
type="text"
placeholder="Masukkan Nama Hadiah"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Quantity"
name="quantity"
type="number"
min={1}
placeholder="Masukkan Kuantitas Item"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Foto Barang"
type="file"
name="foto"
placeholder=".jpg, .jpeg, atau .png"
size="lg"
className="w-full"
/>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Tambahkan Item
</Button>
</form>
</Modal.Content>
</>
);
};
interface IStepTwoProps {
onClose: () => void;
handleAddItem?: () => Promise<boolean>;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
const { onConfirm, onCancel } = useConfirmItem(
onClose,
resetStep,
handleAddItem,
{
success: 'Item ditambahkan ke gacha item',
error: 'Item gagal ditambahkan ke gacha item',
}
);
return (
<>
<Modal.Header className="mb-0 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Item
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin ingin
<br /> menambahkan item ini?
</p>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={onCancel}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={onConfirm}
>
Tambahkan
</Button>
</Modal.Content>
</>
);
};
export default ModalAddItem;
@@ -1,63 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalDeleteItem {
isOpen: boolean;
onClose: () => void;
handleDeleteItem?: () => Promise<boolean>;
}
const ModalDeleteItem = ({
isOpen,
onClose,
handleDeleteItem,
}: IModalDeleteItem) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={onClose}
closeButtonClassName="hidden"
>
<Modal.Header className="gap-8">
<img
src="/chibi-delete.webp"
alt="Delete item?"
width={148}
className="self-center"
/>
<div className="text-center">
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
Delete Item
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin untuk menghapus item ini?
</p>
</div>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="secondary"
size="lg"
className="w-full"
onClick={onClose}
>
Batal Hapus
</Button>
<Button
variant="danger"
size="lg"
className="w-full"
onClick={async () => {
if (handleDeleteItem) await handleDeleteItem();
onClose();
}}
>
Hapus Item
</Button>
</Modal.Content>
</Modal>
);
};
export default ModalDeleteItem;
@@ -1,165 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useConfirmItem, useItem } from '../../_hooks/dashboard/use-item';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
interface IModalEditItem {
isOpen: boolean;
onClose: () => void;
handleEditItem?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { itemName?: string; quantity?: number };
onDataCapture?: (data: any) => void;
}
const ModalEditItem = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleEditItem,
initialValues,
onDataCapture,
}: IModalEditItem) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleEditItem={handleEditItem}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
initialValues?: { itemName?: string; quantity?: number };
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
return (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Edit Item Gacha
</h2>
<p className="text-p3 text-neutral-400">
Silakan mengubah detail dari item yang diperlukan
</p>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Nama Hadiah"
type="text"
name="itemName"
placeholder="Masukkan Nama Hadiah"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Quantity"
type="number"
name="quantity"
placeholder="Masukkan Kuantitas Item"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Foto Barang"
type="file"
name="foto"
placeholder=".jpg, .jpeg, atau .png"
size="lg"
className="w-full"
/>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Perbarui Item
</Button>
</form>
</Modal.Content>
</>
);
};
interface IStepTwoProps {
onClose: () => void;
handleEditItem?: () => Promise<boolean>;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => {
const { onConfirm, onCancel } = useConfirmItem(
onClose,
resetStep,
handleEditItem,
{
success: 'Perubahan item berhasil dilakukan',
error: 'Perubahan item gagal dilakukan',
}
);
return (
<>
<Modal.Header className="mb-0 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Item
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin dengan
<br /> perubahan yang dilakukan?
</p>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={onCancel}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={onConfirm}
>
Update
</Button>
</Modal.Content>
</>
);
};
export default ModalEditItem;
@@ -1,163 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
import { useItem, useConfirmItem } from '../../_hooks/gacha-roll/use-item';
interface IModalAddItem {
isOpen: boolean;
onClose: () => void;
handleAddItem?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddItem = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleAddItem,
onDataCapture,
}: IModalAddItem) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleAddItem={handleAddItem}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Item Roll Gacha
</h2>
<p className="text-p3 text-neutral-400">
Lengkapi detal di bawah ini, untuk menambahkan item gacha
</p>
</Modal.Header>
<Modal.Content>
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Pilih Item"
name="itemName"
type="text"
placeholder="Pilih item yang dimasukkan ke roll"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Quantity"
name="quantity"
type="number"
min={1}
placeholder="Masukkan Kuantitas Item"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Chance Rate"
name="chanceRate"
type="number"
value={0.1}
min={0.1}
step={0.1}
max={1}
placeholder="Masukkan Chance Rate (0,1 - 1)"
size="lg"
className="w-full"
/>
</div>
<Button variant="primary" size="lg" className="w-full" type="submit">
Tambahkan Item
</Button>
</form>
</Modal.Content>
</>
);
};
interface IStepTwoProps {
onClose: () => void;
handleAddItem?: () => Promise<boolean>;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
const { onConfirm, onCancel } = useConfirmItem(
onClose,
resetStep,
handleAddItem,
{
success: 'Item ditambahkan ke roll gacha',
error: 'Item gagal ditambahkan ke roll gacha',
}
);
return (
<>
<Modal.Header className="mb-0 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah ke Roll Gacha
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin ingin
<br /> menambahkan item ini ke roll gacha?
</p>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={onCancel}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={onConfirm}
>
Tambahkan
</Button>
</Modal.Content>
</>
);
};
export default ModalAddItem;
@@ -1,63 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalDeleteItem {
isOpen: boolean;
onClose: () => void;
handleDeleteItem?: () => Promise<void>;
}
const ModalDeleteItem = ({
isOpen,
onClose,
handleDeleteItem,
}: IModalDeleteItem) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={onClose}
closeButtonClassName="hidden"
>
<Modal.Header className="gap-8">
<img
src="/chibi-delete.webp"
alt="Delete item?"
width={148}
className="self-center"
/>
<div className="text-center">
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
Delete Item
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin untuk menghapus item ini?
</p>
</div>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="secondary"
size="lg"
className="w-full"
onClick={onClose}
>
Batal Hapus
</Button>
<Button
variant="danger"
size="lg"
className="w-full"
onClick={async () => {
if (handleDeleteItem) await handleDeleteItem();
onClose();
}}
>
Hapus Item
</Button>
</Modal.Content>
</Modal>
);
};
export default ModalDeleteItem;
@@ -1,168 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { useConfirmItem, useItem } from '../../_hooks/gacha-roll/use-item';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
interface IModalUpdateItem {
isOpen: boolean;
onClose: () => void;
handleUpdateItem?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
onDataCapture?: (data: any) => void;
}
const ModalUpdateItem = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleUpdateItem,
initialValues,
onDataCapture,
}: IModalUpdateItem) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleUpdateItem={handleUpdateItem}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
return (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Item Roll Gacha
</h2>
</Modal.Header>
<Modal.Content>
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Pilih Item"
name="itemName"
type="text"
placeholder="Pilih item yang dimasukkan ke roll"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Quantity"
name="quantity"
type="number"
min={1}
placeholder="Masukkan Kuantitas Item"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Chance Rate"
name="chanceRate"
type="number"
value={0.1}
min={0.1}
step={0.1}
max={1}
placeholder="Masukkan Chance Rate (0,1 - 1)"
size="lg"
className="w-full"
/>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Perbarui Item
</Button>
</form>
</Modal.Content>
</>
);
};
interface IStepTwoProps {
onClose: () => void;
handleUpdateItem?: () => Promise<boolean>;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleUpdateItem, resetStep }: IStepTwoProps) => {
const { onConfirm, onCancel } = useConfirmItem(
onClose,
resetStep,
handleUpdateItem,
{
success: 'Perubahan item roll berhasil dilakukan',
error: 'Perubahan item roll gagal dilakukan',
}
);
return (
<>
<Modal.Header className="mb-0 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Item
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin dengan
<br /> perubahan yang dilakukan?
</p>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={onCancel}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={onConfirm}
>
Update
</Button>
</Modal.Content>
</>
);
};
export default ModalUpdateItem;
@@ -1,135 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
import { useItem, useConfirmItem } from '../../_hooks/permissions/use-item';
interface IModalAddPermission {
isOpen: boolean;
onClose: () => void;
handleAddItem?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddPermission = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleAddItem,
onDataCapture,
}: IModalAddPermission) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-3 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleAddItem={handleAddItem}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500">
Tambah Permissions
</h2>
</Modal.Header>
<Modal.Content>
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<ControlledInputField
control={form.control}
label="Name"
name="name"
type="text"
placeholder="Nama Permission"
size="lg"
className="w-full"
/>
<Button variant="primary" size="lg" className="w-full" type="submit">
Tambah Permission
</Button>
</form>
</Modal.Content>
</>
);
};
interface IStepTwoProps {
onClose: () => void;
handleAddItem?: () => Promise<boolean>;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
const { onConfirm, onCancel } = useConfirmItem(
onClose,
resetStep,
handleAddItem,
{
success: 'Data permissions berhasil ditambahkan',
error: 'Data permissions gagal ditambahkan',
}
);
return (
<>
<Modal.Header className="mb-7 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Permissions
</h2>
<p className="text-p3 text-center text-neutral-400">
Apakah kamu yakin ingin
<br /> menambahkan permission ini?
</p>
</Modal.Header>
<Modal.Content className="flex mb-0 gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={onCancel}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={onConfirm}
>
Tambahkan
</Button>
</Modal.Content>
</>
);
};
export default ModalAddPermission;
@@ -1,72 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { useConfirmItem } from '../../_hooks/permissions/use-item';
interface IModalDeletePermission {
isOpen: boolean;
onClose: () => void;
handleDelete?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
}
const ModalDeletePermission = ({
isOpen,
onClose,
resetStep,
handleDelete,
}: IModalDeletePermission) => {
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
success: 'Data permissions berhasil dihapus',
error: 'Data permissions gagal dihapus',
});
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={onClose}
closeButtonClassName="hidden"
>
<Modal.Header className="gap-8">
<img
src="/chibi-delete.webp"
alt="Delete?"
width={148}
className="self-center"
/>
<div className="text-center">
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
Delete Permissions
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin untuk menghapus permission ini? Menghapus data ini
mungkin akan mempengaruhi fungsional sistem
</p>
</div>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="secondary"
size="lg"
className="w-full"
onClick={onClose}
>
Batal Hapus
</Button>
<Button
variant="primary"
size="lg"
className="w-full bg-danger-500 hover:bg-danger-600"
onClick={onConfirm}
>
Hapus Item
</Button>
</Modal.Content>
</Modal>
);
};
export default ModalDeletePermission;
@@ -1,90 +0,0 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
interface IModalUpdatePermission {
isOpen: boolean;
onClose: () => void;
handleUpdate?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { name?: string };
onDataCapture?: (data: any) => void;
}
const ModalUpdatePermission = ({
isOpen,
onClose,
resetStep,
handleUpdate,
initialValues,
onDataCapture,
}: IModalUpdatePermission) => {
const form = useForm<{ name: string }>({
mode: 'all',
defaultValues: initialValues,
});
useEffect(() => {
if (isOpen) {
form.reset(initialValues);
}
}, [isOpen, initialValues]);
const onSubmit = form.handleSubmit(async (data) => {
try {
onDataCapture?.(data);
if (handleUpdate) await handleUpdate();
toast.success('Perubahan permissions berhasil dilakukan');
onClose();
resetStep();
} catch (error) {
console.log(error);
toast.error('Perubahan permissions gagal dilakukan');
}
});
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-3 text-center"
isOpen={isOpen}
onClose={onClose}
disableEscapeKeyDown={true}
>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500">
Update Permissions
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<ControlledInputField
control={form.control}
label="Name"
name="name"
type="text"
placeholder="Nama Permission"
size="lg"
className="w-full"
/>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Update Permission
</Button>
</form>
</Modal.Content>
</Modal>
);
};
export default ModalUpdatePermission;
@@ -1,188 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
import { useItem, useConfirmItem } from '../../_hooks/roles/use-item';
interface IModalAddRole {
isOpen: boolean;
onClose: () => void;
handleAdd?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddRole = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleAdd,
onDataCapture,
}: IModalAddRole) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleAdd={handleAdd}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Roles
</h2>
</Modal.Header>
<Modal.Content>
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Nama Role"
name="name"
type="text"
placeholder="Masukkan Nama Role"
size="lg"
className="w-full"
/>
</div>
<div className="flex flex-col gap-4 items-start overflow-auto">
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
Permissions
</span>
<div className="flex gap-x-6 overflow-x-scroll">
{[
'Gacha Items',
'Gacha Roll',
'Roll',
'Users',
'Gacha Claim',
].map((title) => (
<div
key={title}
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
>
<span className="text-nowrap text-label1">{title}</span>
<div className="flex gap-[8px] items-center">
<input
type="checkbox"
id={`${title}-all`}
className="rounded"
/>
<label htmlFor={`${title}-all`} className="text-nowrap">
Check All
</label>
</div>
<hr className="border-blue-200" />
<div className="flex flex-col items-start gap-4 mb-4">
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-read`} />
<label htmlFor={`${title}-read`}>Read</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-create`} />
<label htmlFor={`${title}-create`}>Create</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-update`} />
<label htmlFor={`${title}-update`}>Update</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-delete`} />
<label htmlFor={`${title}-delete`}>Delete</label>
</div>
</div>
</div>
))}
</div>
</div>
<Button variant="primary" size="lg" className="w-full" type="submit">
Tambah Role
</Button>
</form>
</Modal.Content>
</>
);
};
interface IStepTwoProps {
onClose: () => void;
handleAdd?: () => Promise<boolean>;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleAdd, resetStep }: IStepTwoProps) => {
const { onConfirm, onCancel } = useConfirmItem(
onClose,
resetStep,
handleAdd,
{
success: 'Data role berhasil ditambahkan',
error: 'Data role gagal ditambahkan',
}
);
return (
<>
<Modal.Header className="mb-10 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Roles
</h2>
<p className="text-p3 text-center text-neutral-400">
Apakah kamu yakin ingin
<br /> menambahkan role ini?
</p>
</Modal.Header>
<Modal.Content className="flex mb-0 gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={onCancel}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={onConfirm}
>
Tambahkan
</Button>
</Modal.Content>
</>
);
};
export default ModalAddRole;
@@ -1,72 +0,0 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { useConfirmItem } from '../../_hooks/roles/use-item';
interface IModalDeletePermission {
isOpen: boolean;
onClose: () => void;
handleDelete?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
}
const ModalDeletePermission = ({
isOpen,
onClose,
resetStep,
handleDelete,
}: IModalDeletePermission) => {
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
success: 'Data roles berhasil dihapus',
error: 'Data roles gagal dihapus',
});
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={onClose}
closeButtonClassName="hidden"
>
<Modal.Header className="gap-8">
<img
src="/chibi-delete.webp"
alt="Delete?"
width={148}
className="self-center"
/>
<div className="text-center">
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
Delete Roles
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin untuk menghapus role ini? Menghapus data ini
mungkin akan mempengaruhi fungsional sistem
</p>
</div>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="secondary"
size="lg"
className="w-full"
onClick={onClose}
>
Batal Hapus
</Button>
<Button
variant="primary"
size="lg"
className="w-full bg-danger-500 hover:bg-danger-600"
onClick={onConfirm}
>
Hapus Role
</Button>
</Modal.Content>
</Modal>
);
};
export default ModalDeletePermission;
@@ -1,139 +0,0 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
interface IModalUpdateRole {
isOpen: boolean;
onClose: () => void;
handleUpdate?: () => Promise<boolean>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { name?: string };
onDataCapture?: (data: any) => void;
}
const ModalUpdateRole = ({
isOpen,
onClose,
resetStep,
handleUpdate,
initialValues,
onDataCapture,
}: IModalUpdateRole) => {
const form = useForm<{ name: string }>({
mode: 'all',
defaultValues: initialValues,
});
useEffect(() => {
if (isOpen) {
form.reset(initialValues);
}
}, [isOpen, initialValues]);
const onSubmit = form.handleSubmit(async (data) => {
try {
onDataCapture?.(data);
if (handleUpdate) await handleUpdate();
toast.success('Perubahan roles berhasil dilakukan');
onClose();
resetStep();
} catch (error) {
console.log(error);
toast.error('Perubahan roles gagal dilakukan');
}
});
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
isOpen={isOpen}
onClose={onClose}
disableEscapeKeyDown={true}
>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Roles
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Nama Role"
name="name"
type="text"
placeholder="Masukkan Nama Role"
size="lg"
className="w-full"
/>
</div>
<div className="flex flex-col gap-4 items-start overflow-auto">
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
Permissions
</span>
<div className="flex gap-x-6 overflow-x-scroll">
{['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map(
(title) => (
<div
key={title}
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
>
<span className="text-nowrap text-label1">{title}</span>
<div className="flex gap-[8px] items-center">
<input
type="checkbox"
id={`${title}-all`}
className="rounded"
/>
<label htmlFor={`${title}-all`} className="text-nowrap">
Check All
</label>
</div>
<hr className="border-blue-200" />
<div className="flex flex-col items-start gap-4 mb-4">
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-read`} />
<label htmlFor={`${title}-read`}>Read</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-create`} />
<label htmlFor={`${title}-create`}>Create</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-update`} />
<label htmlFor={`${title}-update`}>Update</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-delete`} />
<label htmlFor={`${title}-delete`}>Delete</label>
</div>
</div>
</div>
)
)}
</div>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Update Role
</Button>
</form>
</Modal.Content>
</Modal>
);
};
export default ModalUpdateRole;
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import * as React from 'react'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { Fragment, useState } from 'react'
import {
FilterOutlined,
SearchOutlined,
@@ -16,11 +16,8 @@ import {
useReactTable,
RowSelectionState,
} from '@tanstack/react-table'
import ModalEditAccount from './_components/accounts/modal-edit-account'
import { useQueryState } from '@imphnen-frontend-service/utils'
import {
useUserList,
useUpdateUserById,
TUsersListItem,
} from '@imphnen-frontend-service/service'
@@ -29,21 +26,8 @@ export const Route = createFileRoute('/_authenticated/accounts')({
})
function AccountsPage() {
const [showModalEditAccount, setShowModalEditAccount] = useState(false)
const [selectedUser, setSelectedUser] = useState<TUsersListItem | null>(null)
const navigate = useNavigate()
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('step', {
defaultValue: 1,
maxValue: 2,
minValue: 1,
})
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
@@ -58,17 +42,10 @@ function AccountsPage() {
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
})
const updateUser = useUpdateUserById()
const users: TUsersListItem[] = usersData?.data ?? []
const totalItems = usersData?.meta?.total ?? users.length
const handleEditAccount = async () => {
if (selectedUser && pendingFormData.current) {
await updateUser.mutateAsync({ id: selectedUser.id, data: pendingFormData.current })
}
}
const columns: ColumnDef<TUsersListItem>[] = [
{
id: 'select',
@@ -122,8 +99,7 @@ function AccountsPage() {
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedUser(row.original)
setShowModalEditAccount(true)
navigate({ to: '/accounts/$id', params: { id: row.original.id } })
}}
className="flex items-center gap-2"
>
@@ -193,18 +169,6 @@ function AccountsPage() {
)}
</section>
</main>
<ModalEditAccount
currentStep={currentStep}
isOpen={showModalEditAccount}
onClose={() => setShowModalEditAccount(false)}
handleEditAccount={handleEditAccount}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
initialValues={selectedUser ? { fullname: selectedUser.fullname, email: selectedUser.email } : undefined}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
</Fragment>
)
}
@@ -0,0 +1,110 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { InputField } from '@imphnen-frontend-service/ui/molecules'
import {
useUserList,
useUpdateUserById,
} from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/accounts/$id')({
component: AccountsEditPage,
})
function AccountsEditPage() {
const { id } = Route.useParams()
const navigate = useNavigate()
const updateUser = useUpdateUserById()
const { data: usersData, isLoading } = useUserList({ search: '', per_page: 100 })
const user = usersData?.data?.find((u) => u.id === id)
const [fullName, setFullName] = useState('')
const [email, setEmail] = useState('')
useEffect(() => {
if (user) {
setFullName(user.fullname)
setEmail(user.email)
}
}, [user])
const handleSubmit = async () => {
try {
await updateUser.mutateAsync({ id, data: { fullname: fullName, email } })
toast.success('Data akun berhasil diperbarui')
navigate({ to: '/accounts' })
} catch (error) {
console.log(error)
toast.error('Data akun gagal diperbarui')
}
}
if (isLoading) {
return (
<main className="w-full px-[48px] py-[40px]">
<div className="text-center py-8 text-neutral-400">Loading...</div>
</main>
)
}
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/accounts' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Edit Data Akun</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex flex-col gap-6">
<InputField
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
size="lg"
className="w-full"
/>
<InputField
label="Email"
type="text"
placeholder="Masukkan Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
size="lg"
className="w-full"
/>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
onClick={handleSubmit}
>
Perbarui Data
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/accounts' })}
>
Batal
</Button>
</div>
</div>
</div>
</div>
</main>
)
}
@@ -1,5 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { Fragment, useState } from 'react'
import {
SearchOutlined,
EditOutlined,
@@ -16,41 +16,22 @@ import {
RowSelectionState,
useReactTable,
} from '@tanstack/react-table'
import ModalAddEvent from './_components/cms-events/modal-add-event'
import ModalUpdateEvent from './_components/cms-events/modal-update-event'
import ModalDeleteEvent from './_components/cms-events/modal-delete-event'
import { useQueryState } from '@imphnen-frontend-service/utils'
import {
useEventList,
useCreateEvent,
useUpdateEvent,
useDeleteEvent,
TEventsListItem,
} from '@imphnen-frontend-service/service'
import React from 'react'
import { toast } from 'sonner'
export const Route = createFileRoute('/_authenticated/cms-events')({
component: CmsEventsPage,
})
function CmsEventsPage() {
const [showModalAddItem, setShowModalAddItem] = useState(false)
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedEvent, setSelectedEvent] = useState<TEventsListItem | null>(null)
const navigate = useNavigate()
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('step', {
defaultValue: 1,
maxValue: 2,
minValue: 1,
})
const [deleteId, setDeleteId] = useState<string | null>(null)
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
@@ -64,32 +45,20 @@ function CmsEventsPage() {
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
})
const createEvent = useCreateEvent()
const updateEvent = useUpdateEvent()
const deleteEvent = useDeleteEvent()
const events: TEventsListItem[] = eventsData?.data ?? []
const totalItems = eventsData?.meta?.total ?? events.length
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
await createEvent.mutateAsync(pendingFormData.current)
const handleDelete = async (id: string) => {
try {
await deleteEvent.mutateAsync(id)
toast.success('Data event berhasil dihapus')
setDeleteId(null)
} catch (error) {
console.log(error)
toast.error('Data event gagal dihapus')
}
return true
}
const handleUpdate = async (): Promise<boolean> => {
if (selectedEvent && pendingFormData.current) {
await updateEvent.mutateAsync({ id: selectedEvent.id, data: pendingFormData.current })
}
return true
}
const handleDelete = async (): Promise<boolean> => {
if (selectedEvent) {
await deleteEvent.mutateAsync(selectedEvent.id)
}
return true
}
const columns: ColumnDef<TEventsListItem>[] = [
@@ -163,25 +132,49 @@ function CmsEventsPage() {
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedEvent(row.original)
setShowModalUpdateItem(true)
navigate({ to: '/cms-events/$id', params: { id: row.original.id } })
}}
className="flex items-center gap-2"
>
<EditOutlined /> Update
</Button>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedEvent(row.original)
setShowModalDeleteItem(true)
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
{deleteId === row.original.id ? (
<div className="flex items-center gap-2">
<span className="text-label2 text-neutral-500">Yakin?</span>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleDelete(row.original.id)
}}
>
Ya
</Button>
<Button
variant="secondary"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteId(null)
}}
>
Batal
</Button>
</div>
) : (
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteId(row.original.id)
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
)}
</div>
),
},
@@ -228,7 +221,7 @@ function CmsEventsPage() {
variant="primary"
size="md"
className="flex gap-3 text-nowrap"
onClick={() => setShowModalAddItem(true)}
onClick={() => navigate({ to: '/cms-events/create' })}
>
<PlusOutlined />
Tambah Event
@@ -248,44 +241,6 @@ function CmsEventsPage() {
)}
</section>
</main>
<ModalAddEvent
currentStep={currentStep}
isOpen={showModalAddItem}
onClose={() => setShowModalAddItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAdd={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalUpdateEvent
isOpen={showModalUpdateItem}
onClose={() => setShowModalUpdateItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleUpdate={handleUpdate}
initialValues={selectedEvent ? {
name: selectedEvent.name,
description: selectedEvent.description,
detail_link: selectedEvent.detail_link,
location: selectedEvent.location,
price: selectedEvent.price,
start_date: selectedEvent.start_date,
end_date: selectedEvent.end_date,
is_online: selectedEvent.is_online,
} : undefined}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalDeleteEvent
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleDelete={handleDelete}
/>
</Fragment>
)
}
@@ -0,0 +1,195 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import {
useEventList,
useUpdateEvent,
} from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/cms-events/$id')({
component: CmsEventsEditPage,
})
function CmsEventsEditPage() {
const { id } = Route.useParams()
const navigate = useNavigate()
const updateEvent = useUpdateEvent()
const { data: eventsData, isLoading } = useEventList({ search: '', per_page: 100 })
const event = eventsData?.data?.find((e) => e.id === id)
const form = useForm<{
name: string
description: string
detail_link: string
location: string
price: number
start_date: string
end_date: string
is_online: boolean
}>({
mode: 'all',
defaultValues: {
name: '',
description: '',
detail_link: '',
location: '',
price: 0,
start_date: '',
end_date: '',
is_online: false,
},
})
useEffect(() => {
if (event) {
form.reset({
name: event.name,
description: event.description,
detail_link: event.detail_link,
location: event.location,
price: event.price,
start_date: event.start_date,
end_date: event.end_date,
is_online: event.is_online,
})
}
}, [event])
const onSubmit = form.handleSubmit(async (data) => {
try {
await updateEvent.mutateAsync({ id, data })
toast.success('Perubahan event berhasil dilakukan')
navigate({ to: '/cms-events' })
} catch (error) {
console.log(error)
toast.error('Perubahan event gagal dilakukan')
}
})
if (isLoading) {
return (
<main className="w-full px-[48px] py-[40px]">
<div className="text-center py-8 text-neutral-400">Loading...</div>
</main>
)
}
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/cms-events' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Edit Event</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Nama Event"
name="name"
type="text"
placeholder="Masukkan Nama Event"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Deskripsi"
name="description"
type="text"
placeholder="Masukkan Deskripsi Event"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Link Detail"
name="detail_link"
type="text"
placeholder="Masukkan Link Detail"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Lokasi"
name="location"
type="text"
placeholder="Masukkan Lokasi"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Harga"
name="price"
type="number"
placeholder="Masukkan Harga"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Tanggal Mulai"
name="start_date"
type="date"
placeholder="Pilih Tanggal Mulai"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Tanggal Selesai"
name="end_date"
type="date"
placeholder="Pilih Tanggal Selesai"
size="lg"
className="w-full"
/>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="is_online_update"
className="rounded"
{...form.register('is_online')}
/>
<label htmlFor="is_online_update" className="text-p3 font-medium text-neutral-800">
Event Online
</label>
</div>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Update Event
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/cms-events' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -0,0 +1,164 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import { useCreateEvent } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/cms-events/create')({
component: CmsEventsCreatePage,
})
function CmsEventsCreatePage() {
const navigate = useNavigate()
const createEvent = useCreateEvent()
const form = useForm<{
name: string
description: string
detail_link: string
location: string
price: number
start_date: string
end_date: string
is_online: boolean
}>({
mode: 'all',
defaultValues: {
name: '',
description: '',
detail_link: '',
location: '',
price: 0,
start_date: '',
end_date: '',
is_online: false,
},
})
const onSubmit = form.handleSubmit(async (data) => {
try {
await createEvent.mutateAsync(data)
toast.success('Data event berhasil ditambahkan')
navigate({ to: '/cms-events' })
} catch (error) {
console.log(error)
toast.error('Data event gagal ditambahkan')
}
})
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/cms-events' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Tambah Event</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Nama Event"
name="name"
type="text"
placeholder="Masukkan Nama Event"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Deskripsi"
name="description"
type="text"
placeholder="Masukkan Deskripsi Event"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Link Detail"
name="detail_link"
type="text"
placeholder="Masukkan Link Detail"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Lokasi"
name="location"
type="text"
placeholder="Masukkan Lokasi"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Harga"
name="price"
type="number"
placeholder="Masukkan Harga"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Tanggal Mulai"
name="start_date"
type="date"
placeholder="Pilih Tanggal Mulai"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Tanggal Selesai"
name="end_date"
type="date"
placeholder="Pilih Tanggal Selesai"
size="lg"
className="w-full"
/>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="is_online"
className="rounded"
{...form.register('is_online')}
/>
<label htmlFor="is_online" className="text-p3 font-medium text-neutral-800">
Event Online
</label>
</div>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Tambah Event
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/cms-events' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -1,5 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { Fragment, useState } from 'react'
import {
SearchOutlined,
EditOutlined,
@@ -16,41 +16,22 @@ import {
RowSelectionState,
useReactTable,
} from '@tanstack/react-table'
import ModalAddTestimonial from './_components/cms-testimonials/modal-add-testimonial'
import ModalUpdateTestimonial from './_components/cms-testimonials/modal-update-testimonial'
import ModalDeleteTestimonial from './_components/cms-testimonials/modal-delete-testimonial'
import { useQueryState } from '@imphnen-frontend-service/utils'
import {
useTestimonialList,
useCreateTestimonial,
useUpdateTestimonial,
useDeleteTestimonial,
TTestimonialsListItem,
} from '@imphnen-frontend-service/service'
import React from 'react'
import { toast } from 'sonner'
export const Route = createFileRoute('/_authenticated/cms-testimonials')({
component: CmsTestimonialsPage,
})
function CmsTestimonialsPage() {
const [showModalAddItem, setShowModalAddItem] = useState(false)
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedTestimonial, setSelectedTestimonial] = useState<TTestimonialsListItem | null>(null)
const navigate = useNavigate()
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('step', {
defaultValue: 1,
maxValue: 2,
minValue: 1,
})
const [deleteId, setDeleteId] = useState<string | null>(null)
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
@@ -64,32 +45,20 @@ function CmsTestimonialsPage() {
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
})
const createTestimonial = useCreateTestimonial()
const updateTestimonial = useUpdateTestimonial()
const deleteTestimonial = useDeleteTestimonial()
const testimonials: TTestimonialsListItem[] = testimonialsData?.data ?? []
const totalItems = testimonialsData?.meta?.total ?? testimonials.length
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
await createTestimonial.mutateAsync(pendingFormData.current)
const handleDelete = async (id: string) => {
try {
await deleteTestimonial.mutateAsync(id)
toast.success('Data testimonial berhasil dihapus')
setDeleteId(null)
} catch (error) {
console.log(error)
toast.error('Data testimonial gagal dihapus')
}
return true
}
const handleUpdate = async (): Promise<boolean> => {
if (selectedTestimonial && pendingFormData.current) {
await updateTestimonial.mutateAsync({ id: selectedTestimonial.id, data: pendingFormData.current })
}
return true
}
const handleDelete = async (): Promise<boolean> => {
if (selectedTestimonial) {
await deleteTestimonial.mutateAsync(selectedTestimonial.id)
}
return true
}
const columns: ColumnDef<TTestimonialsListItem>[] = [
@@ -147,25 +116,49 @@ function CmsTestimonialsPage() {
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedTestimonial(row.original)
setShowModalUpdateItem(true)
navigate({ to: '/cms-testimonials/$id', params: { id: row.original.id } })
}}
className="flex items-center gap-2"
>
<EditOutlined /> Update
</Button>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedTestimonial(row.original)
setShowModalDeleteItem(true)
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
{deleteId === row.original.id ? (
<div className="flex items-center gap-2">
<span className="text-label2 text-neutral-500">Yakin?</span>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleDelete(row.original.id)
}}
>
Ya
</Button>
<Button
variant="secondary"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteId(null)
}}
>
Batal
</Button>
</div>
) : (
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteId(row.original.id)
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
)}
</div>
),
},
@@ -212,7 +205,7 @@ function CmsTestimonialsPage() {
variant="primary"
size="md"
className="flex gap-3 text-nowrap"
onClick={() => setShowModalAddItem(true)}
onClick={() => navigate({ to: '/cms-testimonials/create' })}
>
<PlusOutlined />
Tambah Testimonial
@@ -232,38 +225,6 @@ function CmsTestimonialsPage() {
)}
</section>
</main>
<ModalAddTestimonial
currentStep={currentStep}
isOpen={showModalAddItem}
onClose={() => setShowModalAddItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAdd={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalUpdateTestimonial
isOpen={showModalUpdateItem}
onClose={() => setShowModalUpdateItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleUpdate={handleUpdate}
initialValues={selectedTestimonial ? {
role: selectedTestimonial.role,
content: selectedTestimonial.content,
} : undefined}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalDeleteTestimonial
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleDelete={handleDelete}
/>
</Fragment>
)
}
@@ -0,0 +1,115 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import {
useTestimonialList,
useUpdateTestimonial,
} from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/cms-testimonials/$id')({
component: CmsTestimonialsEditPage,
})
function CmsTestimonialsEditPage() {
const { id } = Route.useParams()
const navigate = useNavigate()
const updateTestimonial = useUpdateTestimonial()
const { data: testimonialsData, isLoading } = useTestimonialList({ search: '', per_page: 100 })
const testimonial = testimonialsData?.data?.find((t) => t.id === id)
const form = useForm<{ role: string; content: string }>({
mode: 'all',
defaultValues: { role: '', content: '' },
})
useEffect(() => {
if (testimonial) {
form.reset({
role: testimonial.role,
content: testimonial.content,
})
}
}, [testimonial])
const onSubmit = form.handleSubmit(async (data) => {
try {
await updateTestimonial.mutateAsync({ id, data })
toast.success('Perubahan testimonial berhasil dilakukan')
navigate({ to: '/cms-testimonials' })
} catch (error) {
console.log(error)
toast.error('Perubahan testimonial gagal dilakukan')
}
})
if (isLoading) {
return (
<main className="w-full px-[48px] py-[40px]">
<div className="text-center py-8 text-neutral-400">Loading...</div>
</main>
)
}
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/cms-testimonials' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Edit Testimonial</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Role"
name="role"
type="text"
placeholder="Masukkan Role"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Konten Testimonial"
name="content"
type="text"
placeholder="Masukkan Konten Testimonial"
size="lg"
className="w-full"
/>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Update Testimonial
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/cms-testimonials' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -0,0 +1,90 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import { useCreateTestimonial } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/cms-testimonials/create')({
component: CmsTestimonialsCreatePage,
})
function CmsTestimonialsCreatePage() {
const navigate = useNavigate()
const createTestimonial = useCreateTestimonial()
const form = useForm<{ role: string; content: string }>({
mode: 'all',
defaultValues: { role: '', content: '' },
})
const onSubmit = form.handleSubmit(async (data) => {
try {
await createTestimonial.mutateAsync(data)
toast.success('Data testimonial berhasil ditambahkan')
navigate({ to: '/cms-testimonials' })
} catch (error) {
console.log(error)
toast.error('Data testimonial gagal ditambahkan')
}
})
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/cms-testimonials' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Tambah Testimonial</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Role"
name="role"
type="text"
placeholder="Masukkan Role (e.g. Software Engineer)"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Konten Testimonial"
name="content"
type="text"
placeholder="Masukkan Konten Testimonial"
size="lg"
className="w-full"
/>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Tambah Testimonial
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/cms-testimonials' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -1,92 +1,46 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import {
PlusOutlined,
ReloadOutlined,
UsergroupAddOutlined,
UsergroupDeleteOutlined,
UserSwitchOutlined,
DeleteOutlined,
} from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import ModalAddItem from './_components/dashboard/modal-add-item'
import ModalEditItem from './_components/dashboard/modal-edit-item'
import ModalDeleteItem from './_components/dashboard/modal-delete-item'
import { useQueryState } from '@imphnen-frontend-service/utils'
import { Fragment, useState } from 'react'
import {
useUserList,
useGachaItemList,
useCreateGachaItem,
useUpdateGachaItem,
useDeleteGachaItem,
TGachaItemDto,
} from '@imphnen-frontend-service/service'
import { toast } from 'sonner'
export const Route = createFileRoute('/_authenticated/dashboard')({
component: DashboardPage,
})
function DashboardPage() {
const [showModalAddItem, setShowModalAddItem] = useState(false)
const [showModalEditItem, setShowModalEditItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null)
const pendingFormData = useRef<any>(null)
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('step', {
defaultValue: 1,
maxValue: 2,
minValue: 1,
})
const navigate = useNavigate()
const [deleteId, setDeleteId] = useState<string | null>(null)
const { data: usersData } = useUserList({ per_page: 1 })
const { data: gachaItemsData } = useGachaItemList({ per_page: 9 })
const createItem = useCreateGachaItem()
const updateItem = useUpdateGachaItem()
const deleteItem = useDeleteGachaItem()
const totalUsers = usersData?.meta?.total ?? 0
const gachaItems: TGachaItemDto[] = gachaItemsData?.data ?? []
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
const { itemName, quantity } = pendingFormData.current
await createItem.mutateAsync({
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
name: itemName,
description: '',
rarity: 'common',
type_: 'physical',
category: 'merchandise',
value: 0,
weight: 1,
stock: quantity ?? 1,
is_limited: false,
})
const handleDelete = async (id: string) => {
try {
await deleteItem.mutateAsync(id)
toast.success('Item berhasil dihapus')
setDeleteId(null)
} catch (error) {
console.log(error)
toast.error('Item gagal dihapus')
}
return true
}
const handleEdit = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) {
const { itemName, quantity } = pendingFormData.current
await updateItem.mutateAsync({
id: selectedItem.id,
data: { name: itemName, stock: quantity },
})
}
return true
}
const handleDelete = async (): Promise<boolean> => {
if (selectedItem) {
await deleteItem.mutateAsync(selectedItem.id)
}
return true
}
return (
@@ -158,7 +112,7 @@ function DashboardPage() {
variant="primary"
size="sm"
className="items-end gap-3"
onClick={() => setShowModalAddItem(true)}
onClick={() => navigate({ to: '/dashboard/create' })}
>
<span>Tambah Item</span>
<PlusOutlined className="text-[16px]" />
@@ -185,24 +139,40 @@ function DashboardPage() {
variant="text"
size="sm"
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
onClick={() => {
setSelectedItem(item)
setShowModalEditItem(true)
}}
onClick={() => navigate({ to: '/dashboard/$id', params: { id: item.id } })}
>
Edit
</Button>
<Button
variant="text"
size="sm"
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
onClick={() => {
setSelectedItem(item)
setShowModalDeleteItem(true)
}}
>
Delete
</Button>
{deleteId === item.id ? (
<div className="flex items-center gap-1">
<span className="text-[10px] text-neutral-400">Yakin?</span>
<Button
variant="text"
size="sm"
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
onClick={() => handleDelete(item.id)}
>
Ya
</Button>
<Button
variant="text"
size="sm"
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent"
onClick={() => setDeleteId(null)}
>
Batal
</Button>
</div>
) : (
<Button
variant="text"
size="sm"
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
onClick={() => setDeleteId(item.id)}
>
Delete
</Button>
)}
</div>
</div>
@@ -220,35 +190,6 @@ function DashboardPage() {
/>
</div>
</main>
<ModalAddItem
currentStep={currentStep}
isOpen={showModalAddItem}
onClose={() => setShowModalAddItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalEditItem
currentStep={currentStep}
isOpen={showModalEditItem}
onClose={() => setShowModalEditItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleEditItem={handleEdit}
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalDeleteItem
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
handleDeleteItem={async () => { await handleDelete(); return true }}
/>
</Fragment>
)
}
@@ -0,0 +1,124 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import {
useGachaItemList,
useUpdateGachaItem,
} from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/dashboard/$id')({
component: DashboardEditPage,
})
function DashboardEditPage() {
const { id } = Route.useParams()
const navigate = useNavigate()
const updateItem = useUpdateGachaItem()
const { data: gachaItemsData, isLoading } = useGachaItemList({ per_page: 100 })
const item = gachaItemsData?.data?.find((i) => i.id === id)
const form = useForm<{ itemName: string; quantity: number; foto?: FileList }>({
mode: 'all',
defaultValues: { itemName: '', quantity: 1 },
})
useEffect(() => {
if (item) {
form.reset({ itemName: item.name, quantity: item.stock })
}
}, [item])
const onSubmit = form.handleSubmit(async (data) => {
try {
await updateItem.mutateAsync({
id,
data: { name: data.itemName, stock: data.quantity },
})
toast.success('Perubahan item berhasil dilakukan')
navigate({ to: '/dashboard' })
} catch (error) {
console.log(error)
toast.error('Perubahan item gagal dilakukan')
}
})
if (isLoading) {
return (
<main className="w-full px-[48px] py-[40px]">
<div className="text-center py-8 text-neutral-400">Loading...</div>
</main>
)
}
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/dashboard' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Edit Item Gacha</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Nama Hadiah"
type="text"
name="itemName"
placeholder="Masukkan Nama Hadiah"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Quantity"
type="number"
name="quantity"
placeholder="Masukkan Kuantitas Item"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Foto Barang"
type="file"
name="foto"
placeholder=".jpg, .jpeg, atau .png"
size="lg"
className="w-full"
/>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Perbarui Item
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/dashboard' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -0,0 +1,111 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import { useCreateGachaItem } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/dashboard/create')({
component: DashboardCreatePage,
})
function DashboardCreatePage() {
const navigate = useNavigate()
const createItem = useCreateGachaItem()
const form = useForm<{ itemName: string; quantity: number; foto?: FileList }>({
mode: 'all',
defaultValues: { itemName: '', quantity: 1 },
})
const onSubmit = form.handleSubmit(async (data) => {
try {
await createItem.mutateAsync({
item_code: (data.itemName as string).toLowerCase().replace(/\s+/g, '-'),
name: data.itemName,
description: '',
rarity: 'common',
type_: 'physical',
category: 'merchandise',
value: 0,
weight: 1,
stock: data.quantity ?? 1,
is_limited: false,
})
toast.success('Item ditambahkan ke gacha item')
navigate({ to: '/dashboard' })
} catch (error) {
console.log(error)
toast.error('Item gagal ditambahkan ke gacha item')
}
})
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/dashboard' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Tambah Item Gacha</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Nama Hadiah"
name="itemName"
type="text"
placeholder="Masukkan Nama Hadiah"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Quantity"
name="quantity"
type="number"
min={1}
placeholder="Masukkan Kuantitas Item"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Foto Barang"
type="file"
name="foto"
placeholder=".jpg, .jpeg, atau .png"
size="lg"
className="w-full"
/>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Tambahkan Item
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/dashboard' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import * as React from 'react'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { Fragment, useState } from 'react'
import {
SearchOutlined,
EditOutlined,
@@ -17,40 +17,21 @@ import {
useReactTable,
RowSelectionState,
} from '@tanstack/react-table'
import ModalAddItem from './_components/gacha-roll/modal-add-item'
import ModalUpdateItem from './_components/gacha-roll/modal-update-item'
import ModalDeleteItem from './_components/gacha-roll/modal-delete-item'
import { useQueryState } from '@imphnen-frontend-service/utils'
import {
useGachaItemList,
useCreateGachaItem,
useUpdateGachaItem,
useDeleteGachaItem,
TGachaItemDto,
} from '@imphnen-frontend-service/service'
import { toast } from 'sonner'
export const Route = createFileRoute('/_authenticated/gacha-roll')({
component: GachaRollPage,
})
function GachaRollPage() {
const [showModalAddItem, setShowModalAddItem] = useState(false)
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null)
const navigate = useNavigate()
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('step', {
defaultValue: 1,
maxValue: 2,
minValue: 1,
})
const [deleteId, setDeleteId] = useState<string | null>(null)
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
@@ -64,52 +45,20 @@ function GachaRollPage() {
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
})
const createItem = useCreateGachaItem()
const updateItem = useUpdateGachaItem()
const deleteItem = useDeleteGachaItem()
const items: TGachaItemDto[] = itemsData?.data ?? []
const totalItems = itemsData?.meta?.total ?? items.length
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
const { itemName, quantity, chanceRate } = pendingFormData.current
await createItem.mutateAsync({
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
name: itemName,
description: '',
rarity: 'common',
type_: 'physical',
category: 'merchandise',
value: 0,
weight: chanceRate ?? 1,
stock: quantity ?? 1,
is_limited: false,
})
const handleDelete = async (id: string) => {
try {
await deleteItem.mutateAsync(id)
toast.success('Item berhasil dihapus')
setDeleteId(null)
} catch (error) {
console.log(error)
toast.error('Item gagal dihapus')
}
return true
}
const handleUpdate = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) {
const { itemName, quantity, chanceRate } = pendingFormData.current
await updateItem.mutateAsync({
id: selectedItem.id,
data: {
name: itemName,
weight: chanceRate,
stock: quantity,
},
})
}
return true
}
const handleDelete = async () => {
if (selectedItem) {
await deleteItem.mutateAsync(selectedItem.id)
}
setShowModalDeleteItem(false)
}
const columns: ColumnDef<TGachaItemDto>[] = [
@@ -149,25 +98,49 @@ function GachaRollPage() {
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedItem(row.original)
setShowModalUpdateItem(true)
navigate({ to: '/gacha-roll/$id', params: { id: row.original.id } })
}}
className="flex items-center gap-2"
>
<EditOutlined /> Update
</Button>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedItem(row.original)
setShowModalDeleteItem(true)
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
{deleteId === row.original.id ? (
<div className="flex items-center gap-2">
<span className="text-label2 text-neutral-500">Yakin?</span>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleDelete(row.original.id)
}}
>
Ya
</Button>
<Button
variant="secondary"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteId(null)
}}
>
Batal
</Button>
</div>
) : (
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteId(row.original.id)
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
)}
</div>
),
},
@@ -214,7 +187,7 @@ function GachaRollPage() {
variant="primary"
size="md"
className="flex gap-3 text-nowrap"
onClick={() => setShowModalAddItem(true)}
onClick={() => navigate({ to: '/gacha-roll/create' })}
>
<PlusOutlined />
Tambah Item
@@ -229,33 +202,6 @@ function GachaRollPage() {
)}
</section>
</main>
<ModalAddItem
currentStep={currentStep}
isOpen={showModalAddItem}
onClose={() => setShowModalAddItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalUpdateItem
currentStep={currentStep}
isOpen={showModalUpdateItem}
onClose={() => setShowModalUpdateItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleUpdateItem={handleUpdate}
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalDeleteItem
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
handleDeleteItem={handleDelete}
/>
</Fragment>
)
}
@@ -0,0 +1,136 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import {
useGachaItemList,
useUpdateGachaItem,
} from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/gacha-roll/$id')({
component: GachaRollEditPage,
})
function GachaRollEditPage() {
const { id } = Route.useParams()
const navigate = useNavigate()
const updateItem = useUpdateGachaItem()
const { data: itemsData, isLoading } = useGachaItemList({ per_page: 100 })
const item = itemsData?.data?.find((i) => i.id === id)
const form = useForm<{ itemName: string; quantity: number; chanceRate: number }>({
mode: 'all',
defaultValues: { itemName: '', quantity: 1, chanceRate: 0.1 },
})
useEffect(() => {
if (item) {
form.reset({
itemName: item.name,
quantity: item.stock,
chanceRate: item.weight,
})
}
}, [item])
const onSubmit = form.handleSubmit(async (data) => {
try {
await updateItem.mutateAsync({
id,
data: {
name: data.itemName,
weight: data.chanceRate,
stock: data.quantity,
},
})
toast.success('Perubahan item roll berhasil dilakukan')
navigate({ to: '/gacha-roll' })
} catch (error) {
console.log(error)
toast.error('Perubahan item roll gagal dilakukan')
}
})
if (isLoading) {
return (
<main className="w-full px-[48px] py-[40px]">
<div className="text-center py-8 text-neutral-400">Loading...</div>
</main>
)
}
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/gacha-roll' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Edit Item Roll Gacha</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Pilih Item"
name="itemName"
type="text"
placeholder="Pilih item yang dimasukkan ke roll"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Quantity"
name="quantity"
type="number"
min={1}
placeholder="Masukkan Kuantitas Item"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Chance Rate"
name="chanceRate"
type="number"
min={0.1}
step={0.1}
max={1}
placeholder="Masukkan Chance Rate (0,1 - 1)"
size="lg"
className="w-full"
/>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Perbarui Item
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/gacha-roll' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -0,0 +1,114 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import { useCreateGachaItem } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/gacha-roll/create')({
component: GachaRollCreatePage,
})
function GachaRollCreatePage() {
const navigate = useNavigate()
const createItem = useCreateGachaItem()
const form = useForm<{ itemName: string; quantity: number; chanceRate: number }>({
mode: 'all',
defaultValues: { itemName: '', quantity: 1, chanceRate: 0.1 },
})
const onSubmit = form.handleSubmit(async (data) => {
try {
await createItem.mutateAsync({
item_code: (data.itemName as string).toLowerCase().replace(/\s+/g, '-'),
name: data.itemName,
description: '',
rarity: 'common',
type_: 'physical',
category: 'merchandise',
value: 0,
weight: data.chanceRate ?? 1,
stock: data.quantity ?? 1,
is_limited: false,
})
toast.success('Item ditambahkan ke roll gacha')
navigate({ to: '/gacha-roll' })
} catch (error) {
console.log(error)
toast.error('Item gagal ditambahkan ke roll gacha')
}
})
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/gacha-roll' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Tambah Item Roll Gacha</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Pilih Item"
name="itemName"
type="text"
placeholder="Pilih item yang dimasukkan ke roll"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Quantity"
name="quantity"
type="number"
min={1}
placeholder="Masukkan Kuantitas Item"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Chance Rate"
name="chanceRate"
type="number"
min={0.1}
step={0.1}
max={1}
placeholder="Masukkan Chance Rate (0,1 - 1)"
size="lg"
className="w-full"
/>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Tambahkan Item
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/gacha-roll' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -1,5 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { Fragment, useState } from 'react'
import {
SearchOutlined,
EditOutlined,
@@ -16,41 +16,22 @@ import {
RowSelectionState,
useReactTable,
} from '@tanstack/react-table'
import ModalAddPermission from './_components/permissions/modal-add-permission'
import ModalUpdatePermission from './_components/permissions/modal-update-permission'
import ModalDeletePermission from './_components/permissions/modal-delete-permission'
import { useQueryState } from '@imphnen-frontend-service/utils'
import {
usePermissionList,
useCreatePermission,
useUpdatePermission,
useDeletePermission,
TPermissionItem,
} from '@imphnen-frontend-service/service'
import React from 'react'
import { toast } from 'sonner'
export const Route = createFileRoute('/_authenticated/permissions')({
component: PermissionsPage,
})
function PermissionsPage() {
const [showModalAddItem, setShowModalAddItem] = useState(false)
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedItem, setSelectedItem] = useState<TPermissionItem | null>(null)
const navigate = useNavigate()
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('step', {
defaultValue: 1,
maxValue: 2,
minValue: 1,
})
const [deleteId, setDeleteId] = useState<string | null>(null)
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
@@ -64,32 +45,20 @@ function PermissionsPage() {
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
})
const createPermission = useCreatePermission()
const updatePermission = useUpdatePermission()
const deletePermission = useDeletePermission()
const permissions: TPermissionItem[] = permissionsData?.data ?? []
const totalItems = permissionsData?.meta?.total ?? permissions.length
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
await createPermission.mutateAsync(pendingFormData.current)
const handleDelete = async (id: string) => {
try {
await deletePermission.mutateAsync(id)
toast.success('Data permissions berhasil dihapus')
setDeleteId(null)
} catch (error) {
console.log(error)
toast.error('Data permissions gagal dihapus')
}
return true
}
const handleUpdate = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) {
await updatePermission.mutateAsync({ id: selectedItem.id, data: pendingFormData.current })
}
return true
}
const handleDelete = async (): Promise<boolean> => {
if (selectedItem) {
await deletePermission.mutateAsync(selectedItem.id)
}
return true
}
const columns: ColumnDef<TPermissionItem>[] = [
@@ -129,25 +98,49 @@ function PermissionsPage() {
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedItem(row.original)
setShowModalUpdateItem(true)
navigate({ to: '/permissions/$id', params: { id: row.original.id } })
}}
className="flex items-center gap-2"
>
<EditOutlined /> Update
</Button>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedItem(row.original)
setShowModalDeleteItem(true)
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
{deleteId === row.original.id ? (
<div className="flex items-center gap-2">
<span className="text-label2 text-neutral-500">Yakin?</span>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleDelete(row.original.id)
}}
>
Ya
</Button>
<Button
variant="secondary"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteId(null)
}}
>
Batal
</Button>
</div>
) : (
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteId(row.original.id)
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
)}
</div>
),
},
@@ -194,7 +187,7 @@ function PermissionsPage() {
variant="primary"
size="md"
className="flex gap-3 text-nowrap"
onClick={() => setShowModalAddItem(true)}
onClick={() => navigate({ to: '/permissions/create' })}
>
<PlusOutlined />
Tambah Permissions
@@ -214,35 +207,6 @@ function PermissionsPage() {
)}
</section>
</main>
<ModalAddPermission
currentStep={currentStep}
isOpen={showModalAddItem}
onClose={() => setShowModalAddItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalUpdatePermission
isOpen={showModalUpdateItem}
onClose={() => setShowModalUpdateItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleUpdate={handleUpdate}
initialValues={selectedItem ? { name: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalDeletePermission
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleDelete={handleDelete}
/>
</Fragment>
)
}
@@ -0,0 +1,103 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import {
usePermissionList,
useUpdatePermission,
} from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/permissions/$id')({
component: PermissionsEditPage,
})
function PermissionsEditPage() {
const { id } = Route.useParams()
const navigate = useNavigate()
const updatePermission = useUpdatePermission()
const { data: permissionsData, isLoading } = usePermissionList({ search: '', per_page: 100 })
const permission = permissionsData?.data?.find((p) => p.id === id)
const form = useForm<{ name: string }>({
mode: 'all',
defaultValues: { name: '' },
})
useEffect(() => {
if (permission) {
form.reset({ name: permission.name })
}
}, [permission])
const onSubmit = form.handleSubmit(async (data) => {
try {
await updatePermission.mutateAsync({ id, data })
toast.success('Perubahan permissions berhasil dilakukan')
navigate({ to: '/permissions' })
} catch (error) {
console.log(error)
toast.error('Perubahan permissions gagal dilakukan')
}
})
if (isLoading) {
return (
<main className="w-full px-[48px] py-[40px]">
<div className="text-center py-8 text-neutral-400">Loading...</div>
</main>
)
}
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/permissions' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Edit Permission</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Name"
name="name"
type="text"
placeholder="Nama Permission"
size="lg"
className="w-full"
/>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Update Permission
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/permissions' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -0,0 +1,81 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import { useCreatePermission } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/permissions/create')({
component: PermissionsCreatePage,
})
function PermissionsCreatePage() {
const navigate = useNavigate()
const createPermission = useCreatePermission()
const form = useForm<{ name: string }>({
mode: 'all',
defaultValues: { name: '' },
})
const onSubmit = form.handleSubmit(async (data) => {
try {
await createPermission.mutateAsync(data)
toast.success('Data permissions berhasil ditambahkan')
navigate({ to: '/permissions' })
} catch (error) {
console.log(error)
toast.error('Data permissions gagal ditambahkan')
}
})
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/permissions' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Tambah Permission</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Name"
name="name"
type="text"
placeholder="Nama Permission"
size="lg"
className="w-full"
/>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Tambah Permission
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/permissions' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -1,5 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
import { FC, Fragment, ReactElement, useRef, useState } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { Fragment, useState } from 'react'
import {
SearchOutlined,
EditOutlined,
@@ -16,41 +16,22 @@ import {
RowSelectionState,
useReactTable,
} from '@tanstack/react-table'
import ModalAddRole from './_components/roles/modal-add-role'
import ModalUpdateRole from './_components/roles/modal-update-role'
import ModalDeleteRole from './_components/roles/modal-delete-role'
import { useQueryState } from '@imphnen-frontend-service/utils'
import {
useRoleList,
useCreateRole,
useUpdateRole,
useDeleteRole,
TRolesListItem,
} from '@imphnen-frontend-service/service'
import React from 'react'
import { toast } from 'sonner'
export const Route = createFileRoute('/_authenticated/roles')({
component: RolesPage,
})
function RolesPage() {
const [showModalAddItem, setShowModalAddItem] = useState(false)
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false)
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false)
const [selectedRole, setSelectedRole] = useState<TRolesListItem | null>(null)
const navigate = useNavigate()
const [search, setSearch] = useState('')
const pendingFormData = useRef<any>(null)
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('step', {
defaultValue: 1,
maxValue: 2,
minValue: 1,
})
const [deleteId, setDeleteId] = useState<string | null>(null)
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
@@ -64,32 +45,20 @@ function RolesPage() {
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
})
const createRole = useCreateRole()
const updateRole = useUpdateRole()
const deleteRole = useDeleteRole()
const roles: TRolesListItem[] = rolesData?.data ?? []
const totalItems = rolesData?.meta?.total ?? roles.length
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
await createRole.mutateAsync(pendingFormData.current)
const handleDelete = async (id: string) => {
try {
await deleteRole.mutateAsync(id)
toast.success('Data role berhasil dihapus')
setDeleteId(null)
} catch (error) {
console.log(error)
toast.error('Data role gagal dihapus')
}
return true
}
const handleUpdate = async (): Promise<boolean> => {
if (selectedRole && pendingFormData.current) {
await updateRole.mutateAsync({ id: selectedRole.id, data: pendingFormData.current })
}
return true
}
const handleDelete = async (): Promise<boolean> => {
if (selectedRole) {
await deleteRole.mutateAsync(selectedRole.id)
}
return true
}
const columns: ColumnDef<TRolesListItem>[] = [
@@ -129,25 +98,49 @@ function RolesPage() {
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedRole(row.original)
setShowModalUpdateItem(true)
navigate({ to: '/roles/$id', params: { id: row.original.id } })
}}
className="flex items-center gap-2"
>
<EditOutlined /> Update
</Button>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedRole(row.original)
setShowModalDeleteItem(true)
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
{deleteId === row.original.id ? (
<div className="flex items-center gap-2">
<span className="text-label2 text-neutral-500">Yakin?</span>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleDelete(row.original.id)
}}
>
Ya
</Button>
<Button
variant="secondary"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteId(null)
}}
>
Batal
</Button>
</div>
) : (
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteId(row.original.id)
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
)}
</div>
),
},
@@ -194,7 +187,7 @@ function RolesPage() {
variant="primary"
size="md"
className="flex gap-3 text-nowrap"
onClick={() => setShowModalAddItem(true)}
onClick={() => navigate({ to: '/roles/create' })}
>
<PlusOutlined />
Tambah Role
@@ -214,35 +207,6 @@ function RolesPage() {
)}
</section>
</main>
<ModalAddRole
currentStep={currentStep}
isOpen={showModalAddItem}
onClose={() => setShowModalAddItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAdd={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalUpdateRole
isOpen={showModalUpdateItem}
onClose={() => setShowModalUpdateItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleUpdate={handleUpdate}
initialValues={selectedRole ? { name: selectedRole.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data }}
/>
<ModalDeleteRole
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleDelete={handleDelete}
/>
</Fragment>
)
}
@@ -0,0 +1,150 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import {
useRoleList,
useUpdateRole,
} from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/roles/$id')({
component: RolesEditPage,
})
function RolesEditPage() {
const { id } = Route.useParams()
const navigate = useNavigate()
const updateRole = useUpdateRole()
const { data: rolesData, isLoading } = useRoleList({ search: '', per_page: 100 })
const role = rolesData?.data?.find((r) => r.id === id)
const form = useForm<{ name: string }>({
mode: 'all',
defaultValues: { name: '' },
})
useEffect(() => {
if (role) {
form.reset({ name: role.name })
}
}, [role])
const onSubmit = form.handleSubmit(async (data) => {
try {
await updateRole.mutateAsync({ id, data })
toast.success('Perubahan role berhasil dilakukan')
navigate({ to: '/roles' })
} catch (error) {
console.log(error)
toast.error('Perubahan role gagal dilakukan')
}
})
if (isLoading) {
return (
<main className="w-full px-[48px] py-[40px]">
<div className="text-center py-8 text-neutral-400">Loading...</div>
</main>
)
}
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/roles' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Edit Role</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Nama Role"
name="name"
type="text"
placeholder="Masukkan Nama Role"
size="lg"
className="w-full"
/>
<div className="flex flex-col gap-4 items-start overflow-auto">
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
Permissions
</span>
<div className="flex gap-x-6 overflow-x-auto">
{['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map(
(title) => (
<div
key={title}
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900"
>
<span className="text-nowrap text-label1">{title}</span>
<div className="flex gap-[8px] items-center">
<input
type="checkbox"
id={`${title}-all`}
className="rounded"
/>
<label htmlFor={`${title}-all`} className="text-nowrap">
Check All
</label>
</div>
<hr className="border-blue-200" />
<div className="flex flex-col items-start gap-4 mb-4">
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-read`} />
<label htmlFor={`${title}-read`}>Read</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-create`} />
<label htmlFor={`${title}-create`}>Create</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-update`} />
<label htmlFor={`${title}-update`}>Update</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-delete`} />
<label htmlFor={`${title}-delete`}>Delete</label>
</div>
</div>
</div>
)
)}
</div>
</div>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Update Role
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/roles' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -0,0 +1,132 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import { useCreateRole } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/roles/create')({
component: RolesCreatePage,
})
function RolesCreatePage() {
const navigate = useNavigate()
const createRole = useCreateRole()
const form = useForm<{ name: string }>({
mode: 'all',
defaultValues: { name: '' },
})
const onSubmit = form.handleSubmit(async (data) => {
try {
await createRole.mutateAsync(data)
toast.success('Data role berhasil ditambahkan')
navigate({ to: '/roles' })
} catch (error) {
console.log(error)
toast.error('Data role gagal ditambahkan')
}
})
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/roles' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Tambah Role</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Nama Role"
name="name"
type="text"
placeholder="Masukkan Nama Role"
size="lg"
className="w-full"
/>
<div className="flex flex-col gap-4 items-start overflow-auto">
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
Permissions
</span>
<div className="flex gap-x-6 overflow-x-auto">
{[
'Gacha Items',
'Gacha Roll',
'Roll',
'Users',
'Gacha Claim',
].map((title) => (
<div
key={title}
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900"
>
<span className="text-nowrap text-label1">{title}</span>
<div className="flex gap-[8px] items-center">
<input
type="checkbox"
id={`${title}-all`}
className="rounded"
/>
<label htmlFor={`${title}-all`} className="text-nowrap">
Check All
</label>
</div>
<hr className="border-blue-200" />
<div className="flex flex-col items-start gap-4 mb-4">
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-read`} />
<label htmlFor={`${title}-read`}>Read</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-create`} />
<label htmlFor={`${title}-create`}>Create</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-update`} />
<label htmlFor={`${title}-update`}>Update</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-delete`} />
<label htmlFor={`${title}-delete`}>Delete</label>
</div>
</div>
</div>
))}
</div>
</div>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Tambah Role
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/roles' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}