diff --git a/frontend/components/product/ChatBox/ChatButton.vue b/frontend/components/product/ChatBox/ChatButton.vue
new file mode 100644
index 0000000..0ad5dcc
--- /dev/null
+++ b/frontend/components/product/ChatBox/ChatButton.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/composables/api/branch/useCreateBranch.ts b/frontend/composables/api/branch/useCreateBranch.ts
new file mode 100644
index 0000000..1d891b2
--- /dev/null
+++ b/frontend/composables/api/branch/useCreateBranch.ts
@@ -0,0 +1,33 @@
+// imports
+
+import { useMutation } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS } from "~/constants";
+
+// types
+
+export type CreateBranchRequest = {
+ name: string;
+ description: string;
+};
+
+// methods
+
+export const handleCreateBranch = async ({ name, description }: CreateBranchRequest) => {
+ const payload: CreateBranchRequest = {
+ name,
+ description,
+ };
+
+ await axios.post(API_ENDPOINTS.branch.createBranch, payload);
+};
+
+// composable
+
+const useCreateBranch = () => {
+ return useMutation({
+ mutationFn: (data: CreateBranchRequest) => handleCreateBranch(data),
+ });
+};
+
+export default useCreateBranch;
diff --git a/frontend/composables/api/branch/useGetBranch.ts b/frontend/composables/api/branch/useGetBranch.ts
new file mode 100644
index 0000000..50228b2
--- /dev/null
+++ b/frontend/composables/api/branch/useGetBranch.ts
@@ -0,0 +1,54 @@
+// imports
+
+import { useQuery } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
+import type { ComputedRef } from "vue";
+
+// types
+
+export type GetBranchResponse = Branch;
+
+// methods
+
+export const handleGetBranch = async (
+ branchId: string,
+ page: string | undefined,
+ folderId: string | undefined,
+ sort: string | undefined,
+ signal: AbortSignal
+) => {
+
+ const { data } = await axios.get(`${API_ENDPOINTS.branch.get}/${branchId}`, {
+ signal,
+ params: {
+ sort_by: sort,
+ folder_id: folderId,
+ offset: ((!!page ? Number(page) : 1) * 20) - 20,
+ limit: 20
+ }
+ });
+ return data;
+};
+
+// composable
+
+const useGetBranch = (
+ branchId: ComputedRef,
+ page: ComputedRef,
+ folderId: ComputedRef,
+ sort: ComputedRef
+) => {
+ return useQuery({
+ queryKey: [QUERY_KEYS.branch, branchId, page, folderId, sort],
+ queryFn: ({ signal }) => handleGetBranch(
+ branchId.value,
+ page.value,
+ folderId.value,
+ sort.value,
+ signal
+ )
+ });
+};
+
+export default useGetBranch;
diff --git a/frontend/composables/api/branch/useGetBranches.ts b/frontend/composables/api/branch/useGetBranches.ts
new file mode 100644
index 0000000..d665d7a
--- /dev/null
+++ b/frontend/composables/api/branch/useGetBranches.ts
@@ -0,0 +1,29 @@
+// imports
+
+import { useQuery } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
+
+// types
+
+export type GetBranchesResponse = Branch[];
+
+// methods
+
+export const handleGetBranches = async () => {
+ const { data } = await axios.get(`${API_ENDPOINTS.branch.getAll}`);
+
+ return data;
+};
+
+// composable
+
+const useGetBranches = () => {
+ return useQuery({
+ staleTime: 60 * 1000,
+ queryKey: [QUERY_KEYS.branches],
+ queryFn: () => handleGetBranches()
+ });
+};
+
+export default useGetBranches;
diff --git a/frontend/composables/api/branch/useGetUserBranches.ts b/frontend/composables/api/branch/useGetUserBranches.ts
new file mode 100644
index 0000000..42d928e
--- /dev/null
+++ b/frontend/composables/api/branch/useGetUserBranches.ts
@@ -0,0 +1,31 @@
+// imports
+
+import { useQuery } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
+
+// types
+
+export type GetUserBranchesResponse = Branch[];
+
+// methods
+
+export const handleGetUserBranches = async () => {
+ const { data } = await axios.get(
+ `${API_ENDPOINTS.branch.getUserBranches}`
+ );
+
+ return data;
+};
+
+// composable
+
+const useGetUserBranches = () => {
+ return useQuery({
+ staleTime: 60 * 1000,
+ queryKey: [QUERY_KEYS.userBranches],
+ queryFn: () => handleGetUserBranches(),
+ });
+};
+
+export default useGetUserBranches;
diff --git a/frontend/composables/api/chat/useGetChat.ts b/frontend/composables/api/chat/useGetChat.ts
new file mode 100644
index 0000000..10bfc34
--- /dev/null
+++ b/frontend/composables/api/chat/useGetChat.ts
@@ -0,0 +1,60 @@
+// imports
+
+import { useInfiniteQuery, useQuery } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
+import type { ComputedRef } from "vue";
+
+// types
+
+export type GetBranchResponse = ApiPaginated;
+
+// methods
+
+export const handleGetChat = async ({ productId, limit, offset }: {
+ productId: number | string,
+ limit: number,
+ offset: number
+}) => {
+ const { data } = await axios.get(`${API_ENDPOINTS.chat.messages}/${productId}`, {
+ params: {
+ offset,
+ limit
+ },
+ headers: {
+ Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoyMTY3ODE2OTAwLCJpYXQiOjE3MzU4MTY5MDAsImp0aSI6ImQwN2E2Y2Y2NzgwZjRlNTE5NWIzOGQxMTAzYzU4NDQ3IiwidXNlcl9pZCI6NX0.slwd7ZSV7nUXEuDTYwwHUOo9ekCefwEEL4kVv2vSTFo`
+ }
+ });
+ return data;
+};
+
+// composable
+
+const useGetBranch = (
+ productId: Ref,
+ enabled: Ref
+) => {
+ return useInfiniteQuery({
+ enabled,
+ queryKey: [QUERY_KEYS.chat],
+ initialPageParam: {
+ limit: 10,
+ offset: 0
+ },
+ queryFn: ({ pageParam }) => handleGetChat({
+ limit: pageParam.limit,
+ offset: pageParam.offset,
+ productId: productId.value
+ }),
+ getNextPageParam: (lastPage, pages) => {
+ if (!lastPage.next) return undefined;
+
+ return {
+ limit: 10,
+ offset: pages.length * 10
+ };
+ }
+ });
+};
+
+export default useGetBranch;
diff --git a/frontend/composables/api/documents/useAddDoc.ts b/frontend/composables/api/documents/useAddDoc.ts
new file mode 100644
index 0000000..ed1675f
--- /dev/null
+++ b/frontend/composables/api/documents/useAddDoc.ts
@@ -0,0 +1,45 @@
+// imports
+
+import { useMutation } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS } from "~/constants";
+
+// types
+
+export type AddDocRequest = {
+ name: string,
+ parent?: string,
+ branch: string | undefined,
+ type: {
+ title: "File" | "Folder",
+ icon: "bi:folder" | "bi:file-earmark"
+ },
+ content: File | undefined
+};
+
+// methods
+
+export const handleAddDoc = async (variables: AddDocRequest & { updateUploadProgress: (p: number) => void }) => {
+ const { data } = await axios.post(`${API_ENDPOINTS.branch.getDoc}/`, {
+ ...variables,
+ type: variables.type.title.toLocaleLowerCase()
+ }, {
+ onUploadProgress: (progressEvent) => {
+ variables.updateUploadProgress(progressEvent.progress!);
+ },
+ headers: {
+ "Content-Type": "multipart/form-data"
+ }
+ });
+ return data;
+};
+
+// composable
+
+const useAddDoc = () => {
+ return useMutation({
+ mutationFn: (variables: AddDocRequest & { updateUploadProgress: (p: number) => void }) => handleAddDoc(variables)
+ });
+};
+
+export default useAddDoc;
diff --git a/frontend/composables/api/documents/useDeleteDoc.ts b/frontend/composables/api/documents/useDeleteDoc.ts
new file mode 100644
index 0000000..d241169
--- /dev/null
+++ b/frontend/composables/api/documents/useDeleteDoc.ts
@@ -0,0 +1,27 @@
+// imports
+
+import { useMutation } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS } from "~/constants";
+
+// types
+
+export type DeleteDocRequest = {
+ id: number
+};
+
+// methods
+
+export const handleDeleteDoc = async ({ id }: { id: string | undefined }) => {
+ await axios.delete(`${API_ENDPOINTS.branch.getDoc}/${id}`);
+};
+
+// composable
+
+const useDeleteDoc = (id: Ref) => {
+ return useMutation({
+ mutationFn: () => handleDeleteDoc({ id: id.value })
+ });
+};
+
+export default useDeleteDoc;
diff --git a/frontend/composables/api/documents/useEditDoc.ts b/frontend/composables/api/documents/useEditDoc.ts
new file mode 100644
index 0000000..786962e
--- /dev/null
+++ b/frontend/composables/api/documents/useEditDoc.ts
@@ -0,0 +1,29 @@
+// imports
+
+import { useMutation } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS } from "~/constants";
+
+// types
+
+export type EditDocRequest = {
+ id: number
+};
+
+// methods
+
+export const handleEditDoc = async ({ id, name }: { id: string | undefined, name: string | undefined }) => {
+ await axios.patch(`${API_ENDPOINTS.branch.getDoc}/${id}`, { name });
+};
+
+// composable
+
+const useEditDoc = (id: Ref) => {
+ return useMutation({
+ mutationFn: ({ name }: { name: Ref }) => {
+ return handleEditDoc({ id: id.value, name: name.value });
+ }
+ });
+};
+
+export default useEditDoc;
diff --git a/frontend/composables/api/documents/useGetDoc.ts b/frontend/composables/api/documents/useGetDoc.ts
new file mode 100644
index 0000000..582ab02
--- /dev/null
+++ b/frontend/composables/api/documents/useGetDoc.ts
@@ -0,0 +1,33 @@
+// imports
+
+import { useQuery } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
+
+// types
+
+export type GetDocResponse = DocumentStructure;
+
+// methods
+
+export const handleGetDoc = async (id : string | undefined) => {
+ const { data } = await axios.get(`${API_ENDPOINTS.branch.getDoc}/${id}`);
+ return data;
+};
+
+// composable
+
+const useGetDoc = (id: ComputedRef) => {
+
+ const enabled = computed(() => {
+ return !!id.value
+ });
+
+ return useQuery({
+ enabled,
+ queryKey: [QUERY_KEYS.document, id],
+ queryFn: () => handleGetDoc(id.value)
+ });
+};
+
+export default useGetDoc;
diff --git a/frontend/composables/api/documents/useMoveDoc.ts b/frontend/composables/api/documents/useMoveDoc.ts
new file mode 100644
index 0000000..91d26ce
--- /dev/null
+++ b/frontend/composables/api/documents/useMoveDoc.ts
@@ -0,0 +1,32 @@
+// imports
+
+import { useMutation } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS } from "~/constants";
+
+// types
+
+export type MoveDocRequest = {
+ itemsToMove: number[] | string[],
+ parent: number | string
+};
+
+// methods
+
+export const handleMoveDoc = async ({ itemsToMove, parent }: MoveDocRequest) => {
+ const apiUrl = `${API_ENDPOINTS.branch.moveDoc}?new_parent_id=${parent}&${itemsToMove.map(i => `patch_list=${i}&`)}`
+ const splittedUrl = apiUrl.split("");
+ splittedUrl.pop()
+
+ await axios.patch(splittedUrl.join("").replaceAll(",", ""));
+};
+
+// composable
+
+const useMoveDoc = () => {
+ return useMutation({
+ mutationFn: (variables: MoveDocRequest) => handleMoveDoc(variables)
+ });
+};
+
+export default useMoveDoc;
diff --git a/frontend/composables/api/documents/useReplyDoc.ts b/frontend/composables/api/documents/useReplyDoc.ts
new file mode 100644
index 0000000..774077d
--- /dev/null
+++ b/frontend/composables/api/documents/useReplyDoc.ts
@@ -0,0 +1,40 @@
+// imports
+
+import { useMutation } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS } from "~/constants";
+
+// types
+
+export type ReplyDocRequest = {
+ user_id: number;
+ message: string;
+ reply_id: number;
+};
+
+export type ReplyDocResponse = {
+ chat_id: number;
+};
+
+// methods
+
+export const handleReplyDoc = async ({ user_id, message, reply_id }: ReplyDocRequest) => {
+ const payload = {
+ user_id,
+ message,
+ item_id: reply_id,
+ };
+
+ const { data } = await axios.post(API_ENDPOINTS.branch.replyDoc, payload);
+ return data;
+};
+
+// composable
+
+const useReplyDoc = () => {
+ return useMutation({
+ mutationFn: (data: ReplyDocRequest) => handleReplyDoc(data),
+ });
+};
+
+export default useReplyDoc;
diff --git a/frontend/composables/api/documents/useSearchFile.ts b/frontend/composables/api/documents/useSearchFile.ts
new file mode 100644
index 0000000..4563efe
--- /dev/null
+++ b/frontend/composables/api/documents/useSearchFile.ts
@@ -0,0 +1,78 @@
+// imports
+
+import { useInfiniteQuery } from "@tanstack/vue-query";
+import axios from "~/configs/axios.config";
+import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
+import type { ComputedRef } from "vue";
+
+// types
+
+export type SearchFileResponse = Branch;
+
+type HandlerProps = typeof initialPageParam & {
+ signal: AbortSignal,
+ search: string,
+ id: number | undefined;
+ sort: string | undefined;
+}
+
+// state
+
+const initialPageParam = {
+ limit: 10,
+ offset: 0
+};
+
+// methods
+
+export const handleSearchFile = async ({ search, offset, limit, id, signal, sort }: HandlerProps) => {
+ const { data } = await axios.get(`${API_ENDPOINTS.branch.get}/${id}`, {
+ params: {
+ offset,
+ limit,
+ search,
+ sort_by: sort
+ },
+ signal
+ });
+ return data;
+};
+
+// composable
+
+const useSearchFile = (search: Ref, id: Ref, sort: ComputedRef) => {
+
+ const enabled = computed(() => {
+ return search.value.trim() != "" && !!id.value;
+ });
+
+ return useInfiniteQuery({
+ enabled,
+ retry: false,
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ queryKey: [QUERY_KEYS.searchFile, search, id, sort],
+ queryFn: ({ pageParam, signal }) => handleSearchFile({
+ ...pageParam,
+ signal,
+ search: search.value,
+ sort: sort.value,
+ id: id.value
+ }),
+ initialPageParam,
+ getNextPageParam: (lastPage, pages) => {
+ const page = pages.length + 1;
+
+ const limit = initialPageParam.limit;
+
+ const nextPageParams = {
+ offset: page * limit - limit,
+ limit
+ };
+
+ return lastPage?.structure.next ? nextPageParams : undefined;
+ }
+ });
+};
+
+export default useSearchFile;
\ No newline at end of file