BPMN Part 2
BPMN: https://alurkerja-starter.alurkerja.com/camunda/app/cockpit/default/#/processes
Endpoint:
- Konfirmasi cuti
- GET
https://alurkerja-starter.alurkerja.com/bpmn/Cuti/CutiKonfirmasiCuti/list - POST
https://alurkerja-starter.alurkerja.com/bpmn/Cuti/CutiKonfirmasiCuti/{id}/submit - GET
https://alurkerja-starter.alurkerja.com/bpmn/Cuti/CutiKonfirmasiCuti{id}
- GET
- Revisi Cuti
- GET
https://alurkerja-starter.alurkerja.com/bpmn/Cuti/CutiRevisiCuti/list - POST
https://alurkerja-starter.alurkerja.com/bpmn/Cuti/CutiRevisiCuti/{id}/submit - GET
https://alurkerja-starter.alurkerja.com/bpmn/Cuti/CutiRevisiCuti{id}
- GET
Konsep yang sama dari tahap sebelumnya tinggal diterapkan ulang.
- buat custom hook terpisah untuk berhubungan dengan BE
- gunakan
react-hook-formuntuk mengendalikan formulir, dan juga resolver untuk validasi jika dibutuhkan. - jika ada penambahan menu pada sidebar, tambahkan juga pada
src/menuConfig.ts - jika ada penambahan rute, tambahkan juga pada
src/routes.tsx
Bisa diperhatikan juga, kode kita mulai ada kesempatan untuk di-refactor jika kita mau. Proyek ini cukup kecil sehingga sebenarnya tidak terlalu berpengaruh.
Konfirmasi Cuti
src/api/konfirmasi-cuti.ts:
import { useEffect, useState } from "react";
import { CutiDTO, CutiList } from "../api/cuti";
import { axiosInstance } from "../lib/axios";
import axios from "axios";
export function useKonfirmasiCutiList() {
const [data, setData] = useState<CutiList>({
content: [],
});
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState<unknown>();
useEffect(() => {
async function getKonfirmasiCutiList() {
setIsPending(true);
try {
const response = await axiosInstance.get(
"/bpmn/Cuti/CutiKonfirmasiCuti/list"
);
if (axios.isAxiosError(response)) {
setError(response);
} else {
setData(response.data?.data);
}
} catch (error) {
console.error(error);
setError(error);
}
setIsPending(false);
}
getKonfirmasiCutiList();
}, []);
return { data, isPending, error };
}
export function useKonfirmasiCutiDetail(
id?: string,
options: { enabled?: boolean } = {
enabled: true,
}
) {
const [data, setData] = useState<CutiDTO>();
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState<unknown>();
useEffect(() => {
async function getKonfirmasiCutiDetail() {
setIsPending(true);
try {
const response = await axiosInstance.get(`/bpmn/Cuti/${id}`);
if (axios.isAxiosError(response)) {
setError(response);
} else {
console.log(response.data);
setData(response.data?.data?.object);
}
} catch (error) {
console.error(error);
setError(error);
}
setIsPending(false);
}
if (options.enabled) {
getKonfirmasiCutiDetail();
}
}, [id, options.enabled]);
return { data, isPending, error };
}
export function useKonfirmasiCutiApprove() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<unknown>();
async function mutate(id: string, decision: "revisi" | "terima") {
setIsPending(true);
try {
await axiosInstance.post(
"/bpmn/Cuti/CutiKonfirmasiCuti/" + id + "/submit?decision=" + decision,
{}
);
} catch (error) {
console.error(error);
setError(error);
}
setIsPending(false);
}
return { mutate, isPending, error };
}
src/feature/KonfirmasiCutiTable.tsx:
import { useState } from "react";
import { useKonfirmasiCutiList } from "../api/konfirmasi-cuti";
import { TableLowcode } from "alurkerja-ui";
export function KonfirmasiCutiTable() {
const [pageConfig, setPageConfig] = useState({ limit: 10, page: 0 });
const [renderState, setRenderState] = useState(0);
const [search, setSearch] = useState<string | undefined>("tes");
const { data, isPending } = useKonfirmasiCutiList();
if (isPending) {
return <div>Loading...</div>;
}
return (
<TableLowcode
baseUrl="https://alurkerja-starter.alurkerja.com"
spec={{
show_as_menu: false,
base_url: "",
name: "",
can_bulk: true,
can_create: true,
can_delete: true,
can_edit: true,
can_detail: true,
path: "/bpmn/Cuti/CutiKonfirmasiCuti/list",
is_bpmn: true,
is_usertask: false,
label: "",
description: "",
header_action: [
],
field_action: [
{
label: "Detail",
action_label: "Detail",
method: "get",
form_type: "new_page",
path: "/konfirmasi-cuti/{id}",
icon: "eye",
type: "primary",
},
{
label: "Edit",
action_label: "Edit",
method: "put",
form_type: "new_page",
path: `/konfirmasi-cuti/{id}/edit`,
icon: "edit",
type: "primary",
},
{
label: "Hapus",
action_label: "Hapus",
method: "post",
form_type: "confirm_modal",
confirm: {
title: "Hapus",
message: "Apakah anda yakin ingin menghapus data ini?",
confirm_text: "Ya",
cancel_text: "Tidak",
},
path: `/bpmn/Cuti/CutiKonfirmasiCuti/{id}/drop?reason=none`,
icon: "trash",
type: "danger",
},
],
fields: {},
}}
column={[
{
label: "Name",
key: "name",
},
{
label: "Reason",
key: "reason",
},
]}
data={data.content}
renderState={renderState}
setRenderState={setRenderState}
pageConfig={pageConfig}
setPageConfig={setPageConfig}
search={search}
setSearch={setSearch}
canFilter={false}
/>
);
}
src/feature/KonfirmasiCutiDetail.tsx:
import { useForm } from "react-hook-form";
import { Link, useParams } from "react-router-dom";
import { Input } from "alurkerja-ui";
import { useKonfirmasiCutiDetail } from "../api/konfirmasi-cuti";
export function KonfirmasiCutiDetail() {
const { id } = useParams();
const { data, isPending, error } = useKonfirmasiCutiDetail(id, {
enabled: Boolean(id),
});
const { register } = useForm({
values: data,
});
if (isPending) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error</div>;
}
return (
<div className="flex flex-col gap-y-4">
<div>
<label htmlFor="name">Nama</label>
<Input id="name" {...register("name")} disabled />
</div>
<div>
<label htmlFor="reason">Alasan</label>
<Input id="reason" {...register("reason")} disabled />
</div>
<Link to=".." className="w-fit">
Kembali
</Link>
</div>
);
}
src/feature/KonfirmasiCutiEdit.tsx:
import { useForm } from "react-hook-form";
import { Link, useParams } from "react-router-dom";
import { Button, Input } from "alurkerja-ui";
import {
useKonfirmasiCutiApprove,
useKonfirmasiCutiDetail,
} from "../api/konfirmasi-cuti";
export function KonfirmasiCutiEdit() {
const { id } = useParams();
const {
data,
isPending: isFetchingDetail,
error: detailError,
} = useKonfirmasiCutiDetail(id, {
enabled: Boolean(id),
});
const {
mutate,
isPending: isSubmitting,
error: approvalError,
} = useKonfirmasiCutiApprove();
const { register } = useForm({
values: data,
});
function handleReject() {
mutate(id!, "revisi");
}
function handleApprove() {
mutate(id!, "terima");
}
if (isFetchingDetail) {
return <div>Loading...</div>;
}
if (detailError || approvalError) {
return <div>Error</div>;
}
return (
<form className="flex flex-col gap-y-4">
<div>
<label htmlFor="name">Nama</label>
<Input id="name" {...register("name")} disabled />
</div>
<div>
<label htmlFor="reason">Alasan</label>
<Input id="reason" {...register("reason")} disabled />
</div>
<div>
<Link to=".." className="w-fit">
Kembali
</Link>
<Button loading={isSubmitting} type="button" onClick={handleReject}>
Tolak
</Button>
<Button loading={isSubmitting} type="button" onClick={handleApprove}>
Terima
</Button>
</div>
</form>
);
}
Revisi Cuti
src/api/revisi-cuti.ts:
import { useEffect, useState } from "react";
import { CutiDTO, CutiList, CutiPayload } from "./cuti";
import { axiosInstance } from "../lib/axios";
import axios from "axios";
export function useRevisiCutiList() {
const [data, setData] = useState<CutiList>({
content: [],
});
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState<unknown>();
useEffect(() => {
async function getRevisiCutiList() {
setIsPending(true);
try {
const response = await axiosInstance.get(
"/bpmn/Cuti/CutiRevisiCuti/list"
);
if (axios.isAxiosError(response)) {
setError(response);
} else {
setData(response.data?.data);
}
} catch (error) {
console.error(error);
setError(error);
}
setIsPending(false);
}
getRevisiCutiList();
}, []);
return { data, isPending, error };
}
export function useRevisiCutiDetail(
id?: string,
options: { enabled?: boolean } = {
enabled: true,
}
) {
const [data, setData] = useState<CutiDTO>();
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState<unknown>();
useEffect(() => {
async function getRevisiCutiDetail() {
setIsPending(true);
try {
const response = await axiosInstance.get(`/bpmn/Cuti/${id}`);
if (axios.isAxiosError(response)) {
setError(response);
} else {
console.log(response.data);
setData(response.data?.data?.object);
}
} catch (error) {
console.error(error);
setError(error);
}
setIsPending(false);
}
if (options.enabled) {
getRevisiCutiDetail();
}
}, [id, options.enabled]);
return { data, isPending, error };
}
export function useRevisiCutiEdit() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<unknown>();
async function mutate(id: string, values: CutiPayload) {
setIsPending(true);
try {
await axiosInstance.post(
"/bpmn/Cuti/CutiRevisiCuti/" + id + "/submit",
values
);
} catch (error) {
console.error(error);
setError(error);
}
setIsPending(false);
}
return { mutate, isPending, error };
}
src/feature/RevisiCutiTable.tsx:
import { useState } from "react";
import { useRevisiCutiList } from "../api/revisi-cuti";
import { TableLowcode } from "alurkerja-ui";
export function RevisiCutiTable() {
const [pageConfig, setPageConfig] = useState({ limit: 10, page: 0 });
const [renderState, setRenderState] = useState(0);
const [search, setSearch] = useState<string | undefined>("tes");
const { data, isPending } = useRevisiCutiList();
if (isPending) {
return <div>Loading...</div>;
}
return (
<TableLowcode
baseUrl="https://alurkerja-starter.alurkerja.com"
spec={{
show_as_menu: false,
base_url: "",
name: "",
can_bulk: true,
can_create: true,
can_delete: true,
can_edit: true,
can_detail: true,
path: "/bpmn/Cuti/CutiRevisiCuti/list",
is_bpmn: true,
is_usertask: false,
label: "",
description: "",
header_action: [
],
field_action: [
{
label: "Detail",
action_label: "Detail",
method: "get",
form_type: "new_page",
path: "/revisi-cuti/{id}",
icon: "eye",
type: "primary",
},
{
label: "Edit",
action_label: "Edit",
method: "put",
form_type: "new_page",
path: `/revisi-cuti/{id}/edit`,
icon: "edit",
type: "primary",
},
{
label: "Hapus",
action_label: "Hapus",
method: "post",
form_type: "confirm_modal",
confirm: {
title: "Hapus",
message: "Apakah anda yakin ingin menghapus data ini?",
confirm_text: "Ya",
cancel_text: "Tidak",
},
path: `/bpmn/Cuti/CutiRevisiCuti/{id}/drop?reason=none`,
icon: "trash",
type: "danger",
},
],
fields: {},
}}
column={[
{
label: "Name",
key: "name",
},
{
label: "Reason",
key: "reason",
},
]}
data={data.content}
renderState={renderState}
setRenderState={setRenderState}
pageConfig={pageConfig}
setPageConfig={setPageConfig}
search={search}
setSearch={setSearch}
canFilter={false}
/>
);
}
src/feature/RevisiCutiDetail.tsx:
import { useForm } from "react-hook-form";
import { Link, useParams } from "react-router-dom";
import { Input } from "alurkerja-ui";
import { useRevisiCutiDetail } from "../api/revisi-cuti";
export function RevisiCutiDetail() {
const { id } = useParams();
const { data, isPending, error } = useRevisiCutiDetail(id, {
enabled: Boolean(id),
});
const { register } = useForm({
values: data,
});
if (isPending) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error</div>;
}
return (
<div className="flex flex-col gap-y-4">
<div>
<label htmlFor="name">Nama</label>
<Input id="name" {...register("name")} disabled />
</div>
<div>
<label htmlFor="reason">Alasan</label>
<Input id="reason" {...register("reason")} disabled />
</div>
<Link to=".." className="w-fit">
Kembali
</Link>
</div>
);
}
src/feature/RevisiCutiEdit.tsx:
import { useForm } from "react-hook-form";
import { Link, useParams } from "react-router-dom";
import { Button, Input } from "alurkerja-ui";
import { useRevisiCutiDetail, useRevisiCutiEdit } from "../api/revisi-cuti";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const cutiReviseFormSchema = z.object({
name: z.string().min(1, { message: "Nama wajib diisi" }),
reason: z.string().min(1, { message: "Alasan wajib diisi" }),
});
type CutiReviseFormSchema = z.infer<typeof cutiReviseFormSchema>;
export function RevisiCutiEdit() {
const { id } = useParams();
const {
data,
isPending: isFetchingDetail,
error: detailError,
} = useRevisiCutiDetail(id, {
enabled: Boolean(id),
});
const {
mutate,
isPending: isSubmitting,
error: approvalError,
} = useRevisiCutiEdit();
const { register, handleSubmit } = useForm({
resolver: zodResolver(cutiReviseFormSchema),
values: data,
});
function handleRevise(values: CutiReviseFormSchema) {
mutate(id!, values);
}
if (isFetchingDetail) {
return <div>Loading...</div>;
}
if (detailError || approvalError) {
return <div>Error</div>;
}
return (
<form
className="flex flex-col gap-y-4"
onSubmit={handleSubmit(handleRevise)}
>
<div>
<label htmlFor="name">Nama</label>
<Input id="name" {...register("name")} />
</div>
<div>
<label htmlFor="reason">Alasan</label>
<Input id="reason" {...register("reason")} />
</div>
<div>
<Link to=".." className="w-fit">
Kembali
</Link>
<Button loading={isSubmitting}>
Revisi
</Button>
</div>
</form>
);
}
Rute
src/menuConfig.ts:
import { MenuConfig } from "alurkerja-ui";
export const pegawaiMenuConfig: MenuConfig[] = [
{
label: "Beranda",
href: "/",
},
{
label: "Pengajuan Cuti",
href: "/cuti",
},
{
label: "Revisi Cuti",
href: "/revisi-cuti",
},
{
label: "Artikel",
href: "/artikel",
},
];
export const hrdMenuConfig: MenuConfig[] = [
{
label: "Beranda",
href: "/",
},
{
label: "Konfirmasi Cuti",
href: "/konfirmasi-cuti",
},
{
label: "Artikel",
href: "/artikel",
},
];
src/routes.tsx:
import { RouteObject } from "react-router-dom";
import { CutiTable } from "./feature/CutiTable";
import { CutiCreate } from "./feature/CutiCreate";
import { RoleGate } from "./feature/RoleGate";
import { Layout } from "./feature/Layout";
import { CutiDetail } from "./feature/CutiDetail";
import { KonfirmasiCutiTable } from "./feature/KonfirmasiCutiTable";
import { KonfirmasiCutiDetail } from "./feature/KonfirmasiCutiDetail";
import { KonfirmasiCutiEdit } from "./feature/KonfirmasiCutiEdit";
import { ArtikelTable } from "./feature/ArtikelTable";
import { ArtikelCreate } from "./feature/ArtikelCreate";
import { ArtikelDetail } from "./feature/ArtikelDetail";
import { ArtikelEdit } from "./feature/ArtikelEdit";
import { RevisiCutiTable } from "./feature/RevisiCutiTable";
import { RevisiCutiDetail } from "./feature/RevisiCutiDetail";
import { RevisiCutiEdit } from "./feature/RevisiCutiEdit";
export const routes: RouteObject[] = [
{
path: "/",
element: <Layout />,
errorElement: <div>404</div>,
children: [
{
index: true,
element: <div>Selamat Datang</div>,
},
{
path: "cuti",
children: [
{
index: true,
element: (
<RoleGate allowed="Pegawai">
<CutiTable />
</RoleGate>
),
},
{
path: "create",
element: (
<RoleGate allowed="Pegawai">
<CutiCreate />
</RoleGate>
),
},
{
path: ":id",
element: (
<RoleGate allowed="Pegawai">
<CutiDetail />
</RoleGate>
),
},
],
},
{
path: "konfirmasi-cuti",
children: [
{
index: true,
element: (
<RoleGate allowed="HRD">
<KonfirmasiCutiTable />
</RoleGate>
),
},
{
path: ":id",
element: (
<RoleGate allowed="HRD">
<KonfirmasiCutiDetail />
</RoleGate>
),
},
{
path: ":id/edit",
element: (
<RoleGate allowed="HRD">
<KonfirmasiCutiEdit />
</RoleGate>
),
},
],
},
{
path: "revisi-cuti",
children: [
{
index: true,
element: (
<RoleGate allowed="Pegawai">
<RevisiCutiTable />
</RoleGate>
),
},
{
path: ":id",
element: (
<RoleGate allowed="Pegawai">
<RevisiCutiDetail />
</RoleGate>
),
},
{
path: ":id/edit",
element: (
<RoleGate allowed="Pegawai">
<RevisiCutiEdit />
</RoleGate>
),
},
],
},
{
path: "artikel",
children: [
{
index: true,
element: <ArtikelTable />,
},
{
path: "create",
element: <ArtikelCreate />,
},
{
path: ":id",
element: <ArtikelDetail />,
},
{
path: ":id/edit",
element: <ArtikelEdit />,
},
],
},
],
},
];
...