mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 01:52:04 +00:00
feat: add accounts CRUD module
This commit is contained in:
parent
60572c7e96
commit
24b123225c
13 changed files with 709 additions and 0 deletions
182
src/app/(protected)/accounts/_components/account-form.tsx
Normal file
182
src/app/(protected)/accounts/_components/account-form.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { accountSchema, type AccountFormValues } from "@/domain/accounts/schemas/account-schema";
|
||||
import { useAccountDetail } from "@/domain/accounts/use-cases/use-account-detail";
|
||||
import { useCreateAccount } from "@/domain/accounts/use-cases/use-create-account";
|
||||
import { useUpdateAccount } from "@/domain/accounts/use-cases/use-update-account";
|
||||
|
||||
const defaultValues: AccountFormValues = {
|
||||
AccountName: "",
|
||||
AccountNumber: "",
|
||||
Industry: "",
|
||||
Website: "",
|
||||
Notes: "",
|
||||
Address1: "",
|
||||
Address2: "",
|
||||
City: "",
|
||||
State: "",
|
||||
Zip: "",
|
||||
Country: "US",
|
||||
};
|
||||
|
||||
export default function AccountFormPage() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const isEdit = Boolean(id);
|
||||
|
||||
const { data: account, isLoading, error } = useAccountDetail(id);
|
||||
const createAccount = useCreateAccount();
|
||||
const updateAccount = useUpdateAccount();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<AccountFormValues>({
|
||||
resolver: zodResolver(accountSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (account && isEdit) {
|
||||
reset({
|
||||
AccountName: account.AccountName ?? "",
|
||||
AccountNumber: account.AccountNumber ?? "",
|
||||
Industry: account.Industry ?? "",
|
||||
Website: account.Website ?? "",
|
||||
Notes: account.Notes ?? "",
|
||||
Address1: account.Address1 ?? "",
|
||||
Address2: account.Address2 ?? "",
|
||||
City: account.City ?? "",
|
||||
State: account.State ?? "",
|
||||
Zip: account.Zip ?? "",
|
||||
Country: account.Country ?? "US",
|
||||
});
|
||||
}
|
||||
}, [account, isEdit, reset]);
|
||||
|
||||
const isSaving = createAccount.isPending || updateAccount.isPending;
|
||||
|
||||
const onSubmit = (values: AccountFormValues) => {
|
||||
if (isEdit && id) {
|
||||
updateAccount.mutate({ id, values });
|
||||
return;
|
||||
}
|
||||
createAccount.mutate(values);
|
||||
};
|
||||
|
||||
if (isEdit && isLoading) {
|
||||
return (
|
||||
<Box className="flex justify-center p-10">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="mx-auto max-w-4xl p-4">
|
||||
<Paper variant="outlined" className="p-6">
|
||||
<Stack spacing={4}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h5" component="h1">
|
||||
{isEdit ? "Edit Account" : "Create an Account"}
|
||||
</Typography>
|
||||
<Button variant="text" onClick={() => navigate("/accounts")}>
|
||||
Back to list
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error">
|
||||
{error instanceof Error ? error.message : "Failed to load account"}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box component="form" onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6">
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="subtitle1" className="font-semibold">
|
||||
General Details
|
||||
</Typography>
|
||||
<Stack direction={{ xs: "column", md: "row" }} spacing={2}>
|
||||
<TextField
|
||||
{...register("AccountName")}
|
||||
label="Account Name"
|
||||
required
|
||||
error={Boolean(errors.AccountName)}
|
||||
helperText={errors.AccountName?.message}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField {...register("AccountNumber")} label="Account Number" fullWidth />
|
||||
</Stack>
|
||||
<Stack direction={{ xs: "column", md: "row" }} spacing={2}>
|
||||
<TextField {...register("Industry")} label="Industry" fullWidth />
|
||||
<TextField {...register("Website")} label="Website" fullWidth />
|
||||
</Stack>
|
||||
<TextField {...register("Notes")} label="Notes" fullWidth multiline minRows={2} />
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="subtitle1" className="font-semibold">
|
||||
Address
|
||||
</Typography>
|
||||
<Stack direction={{ xs: "column", md: "row" }} spacing={2}>
|
||||
<TextField {...register("Address1")} label="Address 1" fullWidth />
|
||||
<TextField {...register("Address2")} label="Address 2" fullWidth />
|
||||
</Stack>
|
||||
<Stack direction={{ xs: "column", md: "row" }} spacing={2}>
|
||||
<TextField {...register("City")} label="City" fullWidth />
|
||||
<TextField {...register("State")} label="State / Province" fullWidth />
|
||||
<TextField {...register("Zip")} label="Zip/Postal Code" fullWidth />
|
||||
</Stack>
|
||||
<Controller
|
||||
name="Country"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="country-label">Country</InputLabel>
|
||||
<Select
|
||||
{...field}
|
||||
labelId="country-label"
|
||||
label="Country"
|
||||
value={field.value ?? "US"}
|
||||
>
|
||||
<MenuItem value="">Select Country</MenuItem>
|
||||
<MenuItem value="US">United States</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" justifyContent="flex-end" spacing={2}>
|
||||
<Button variant="outlined" onClick={() => navigate("/accounts")} disabled={isSaving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="contained" disabled={isSaving}>
|
||||
{isSaving ? "Saving..." : isEdit ? "Update Account" : "Create Account"}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
1
src/app/(protected)/accounts/edit/[id].tsx
Normal file
1
src/app/(protected)/accounts/edit/[id].tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "../_components/account-form";
|
||||
221
src/app/(protected)/accounts/index.tsx
Normal file
221
src/app/(protected)/accounts/index.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
Paper,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import type { AccountListItem } from "@/domain/accounts/types/account";
|
||||
import { useAccountsList } from "@/domain/accounts/use-cases/use-accounts-list";
|
||||
import { useDeleteAccount } from "@/domain/accounts/use-cases/use-delete-account";
|
||||
import { useDebounce } from "@/hooks/use-debounce";
|
||||
|
||||
export default function AccountsListPage() {
|
||||
const navigate = useNavigate();
|
||||
const [search, setSearch] = useState("");
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(12);
|
||||
const [deleteTarget, setDeleteTarget] = useState<AccountListItem | null>(null);
|
||||
|
||||
const debouncedSearch = useDebounce(search, 300);
|
||||
const deleteAccount = useDeleteAccount();
|
||||
|
||||
const listParams = useMemo(
|
||||
() => ({
|
||||
search: debouncedSearch,
|
||||
page: page + 1,
|
||||
pageSize,
|
||||
}),
|
||||
[debouncedSearch, page, pageSize],
|
||||
);
|
||||
|
||||
const { data, isLoading, isFetching, error } = useAccountsList(listParams);
|
||||
|
||||
const rows = data?.items ?? [];
|
||||
const totalCount = data?.totalCount ?? 0;
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (!deleteTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteAccount.mutate(deleteTarget.id, {
|
||||
onSuccess: () => setDeleteTarget(null),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col gap-4 p-4">
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
spacing={2}
|
||||
alignItems={{ xs: "stretch", md: "center" }}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack direction="row" spacing={2} alignItems="baseline">
|
||||
<Typography variant="h5" component="h1">
|
||||
Accounts
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{totalCount} total
|
||||
</Typography>
|
||||
{isFetching && !isLoading && <CircularProgress size={16} />}
|
||||
</Stack>
|
||||
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} alignItems="center">
|
||||
<TextField
|
||||
size="small"
|
||||
value={search}
|
||||
onChange={(event) => {
|
||||
setSearch(event.target.value);
|
||||
setPage(0);
|
||||
}}
|
||||
placeholder="Search accounts..."
|
||||
className="min-w-[220px]"
|
||||
/>
|
||||
<Button variant="contained" onClick={() => navigate("/accounts/new")}>
|
||||
+ New Account
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error">
|
||||
{error instanceof Error ? error.message : "Failed to load accounts"}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell className="font-semibold">Name</TableCell>
|
||||
<TableCell className="font-semibold">Number</TableCell>
|
||||
<TableCell className="font-semibold">Industry</TableCell>
|
||||
<TableCell className="font-semibold">Website</TableCell>
|
||||
<TableCell className="font-semibold">Primary Contact</TableCell>
|
||||
<TableCell className="font-semibold">Phone</TableCell>
|
||||
<TableCell className="font-semibold">Owner</TableCell>
|
||||
<TableCell align="right" className="font-semibold">
|
||||
Actions
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} align="center" className="py-10">
|
||||
<CircularProgress size={28} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : rows.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} align="center" className="py-10">
|
||||
<Typography color="text.secondary">No accounts found</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows.map((row) => (
|
||||
<TableRow key={String(row.id)} hover>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
className="font-semibold"
|
||||
onClick={() => navigate(`/accounts/edit/${row.id}`)}
|
||||
>
|
||||
{row.name || "—"}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>{row.number || "—"}</TableCell>
|
||||
<TableCell>{row.industry || "—"}</TableCell>
|
||||
<TableCell>{row.website || "—"}</TableCell>
|
||||
<TableCell>{row.primaryContact || "—"}</TableCell>
|
||||
<TableCell>{row.phoneNumber || "—"}</TableCell>
|
||||
<TableCell>{row.owner || "—"}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Edit">
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Edit account"
|
||||
onClick={() => navigate(`/accounts/edit/${row.id}`)}
|
||||
>
|
||||
<EditOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete">
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Delete account"
|
||||
onClick={() => setDeleteTarget(row)}
|
||||
>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={totalCount}
|
||||
page={page}
|
||||
onPageChange={(_event, nextPage) => setPage(nextPage)}
|
||||
rowsPerPage={pageSize}
|
||||
onRowsPerPageChange={(event) => {
|
||||
setPageSize(Number(event.target.value));
|
||||
setPage(0);
|
||||
}}
|
||||
rowsPerPageOptions={[12, 24, 48]}
|
||||
/>
|
||||
|
||||
<Dialog open={Boolean(deleteTarget)} onClose={() => setDeleteTarget(null)}>
|
||||
<DialogTitle>Delete Account</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Are you sure you want to delete "{deleteTarget?.name}"? This action cannot be
|
||||
undone.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteTarget(null)} disabled={deleteAccount.isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={handleDeleteConfirm}
|
||||
disabled={deleteAccount.isPending}
|
||||
>
|
||||
{deleteAccount.isPending ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
1
src/app/(protected)/accounts/new.tsx
Normal file
1
src/app/(protected)/accounts/new.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./_components/account-form";
|
||||
61
src/domain/accounts/api/accounts-api.ts
Normal file
61
src/domain/accounts/api/accounts-api.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { API_PATHS } from "@/api/api-paths";
|
||||
import { apiDelete, apiGet, apiPost, apiPut } from "@/api/api";
|
||||
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
||||
import {
|
||||
mapAccount,
|
||||
mapAccountListItems,
|
||||
mapAccountToBackend,
|
||||
} from "@/domain/accounts/mappers/account-mapper";
|
||||
import type {
|
||||
Account,
|
||||
AccountsListParams,
|
||||
AccountsListResult,
|
||||
} from "@/domain/accounts/types/account";
|
||||
|
||||
function buildSearchParams(params: AccountsListParams): Record<string, string | number> {
|
||||
return {
|
||||
search: params.search ?? "",
|
||||
page: params.page ?? 1,
|
||||
pageSize: params.pageSize ?? 12,
|
||||
};
|
||||
}
|
||||
|
||||
export const accountsApi = {
|
||||
getList: async (params: AccountsListParams = {}): Promise<AccountsListResult> => {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const data = await apiGet<unknown>(API_PATHS.account.getList, { searchParams });
|
||||
const paginated = handlePaginatedResponse<unknown>(data, {
|
||||
page: params.page,
|
||||
pageSize: params.pageSize,
|
||||
});
|
||||
|
||||
return {
|
||||
items: mapAccountListItems(paginated.items),
|
||||
totalCount: paginated.totalCount,
|
||||
page: paginated.page,
|
||||
pageSize: paginated.pageSize,
|
||||
};
|
||||
},
|
||||
|
||||
getById: async (id: string | number): Promise<Account> => {
|
||||
const data = await apiGet<unknown>(`${API_PATHS.rest.accounts}/${id}`);
|
||||
return mapAccount(handleApiResponse(data));
|
||||
},
|
||||
|
||||
create: async (payload: unknown): Promise<Account> => {
|
||||
const data = await apiPost<unknown>(API_PATHS.rest.accounts, mapAccountToBackend(payload));
|
||||
return mapAccount(handleApiResponse(data));
|
||||
},
|
||||
|
||||
update: async (id: string | number, payload: unknown): Promise<Account> => {
|
||||
const data = await apiPut<unknown>(
|
||||
`${API_PATHS.rest.accounts}/${id}`,
|
||||
mapAccountToBackend(payload),
|
||||
);
|
||||
return mapAccount(handleApiResponse(data));
|
||||
},
|
||||
|
||||
delete: async (id: string | number): Promise<void> => {
|
||||
await apiDelete<unknown>(`${API_PATHS.rest.accounts}/${id}`);
|
||||
},
|
||||
};
|
||||
88
src/domain/accounts/mappers/account-mapper.ts
Normal file
88
src/domain/accounts/mappers/account-mapper.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import type { Account, AccountListItem } from "@/domain/accounts/types/account";
|
||||
|
||||
function readString(record: Record<string, unknown>, ...keys: string[]): string {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function readId(record: Record<string, unknown>): string | number {
|
||||
const value = record.id ?? record.Id;
|
||||
if (typeof value === "string" || typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function mapAccountListItem(raw: unknown): AccountListItem {
|
||||
const item = (typeof raw === "object" && raw !== null ? raw : {}) as Record<string, unknown>;
|
||||
|
||||
return {
|
||||
id: readId(item),
|
||||
name: readString(item, "name", "Name", "AccountName"),
|
||||
number: readString(item, "number", "AccountNumber"),
|
||||
industry: readString(item, "industry", "Industry"),
|
||||
website: readString(item, "website", "Website"),
|
||||
primaryContact: readString(item, "primaryContact", "PrimaryContact"),
|
||||
phoneNumber: readString(item, "phoneNumber", "Phone", "PhoneNumber"),
|
||||
owner: readString(item, "owner", "Owner"),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapAccountListItems(rawItems: unknown[]): AccountListItem[] {
|
||||
return rawItems.map(mapAccountListItem);
|
||||
}
|
||||
|
||||
export function mapAccount(raw: unknown): Account {
|
||||
const item = (typeof raw === "object" && raw !== null ? raw : {}) as Record<string, unknown>;
|
||||
|
||||
return {
|
||||
id: readId(item) || undefined,
|
||||
AccountName: readString(item, "AccountName", "accountName", "name", "Name") || undefined,
|
||||
AccountNumber: readString(item, "AccountNumber", "accountNumber", "number") || undefined,
|
||||
Industry: readString(item, "Industry", "industry") || undefined,
|
||||
Website: readString(item, "Website", "website") || undefined,
|
||||
Notes: readString(item, "Notes", "notes") || undefined,
|
||||
Address1: readString(item, "Address1", "address1") || undefined,
|
||||
Address2: readString(item, "Address2", "address2") || undefined,
|
||||
City: readString(item, "City", "city") || undefined,
|
||||
State: readString(item, "State", "state") || undefined,
|
||||
Zip: readString(item, "Zip", "zip", "zipCode") || undefined,
|
||||
Country: readString(item, "Country", "country") || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapAccountToBackend(account: unknown): Record<string, unknown> {
|
||||
const item = (typeof account === "object" && account !== null ? account : {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
AccountName: item.AccountName ?? "",
|
||||
AccountNumber: item.AccountNumber ?? "",
|
||||
Industry: item.Industry ?? "",
|
||||
Website: item.Website ?? "",
|
||||
Notes: item.Notes ?? "",
|
||||
Address1: item.Address1 ?? "",
|
||||
Address2: item.Address2 ?? "",
|
||||
City: item.City ?? "",
|
||||
State: item.State ?? "",
|
||||
Zip: item.Zip ?? "",
|
||||
Country: item.Country ?? "US",
|
||||
};
|
||||
|
||||
const id = item.id ?? item.Id;
|
||||
if (typeof id === "string" || typeof id === "number") {
|
||||
payload.id = id;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
17
src/domain/accounts/schemas/account-schema.ts
Normal file
17
src/domain/accounts/schemas/account-schema.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const accountSchema = z.object({
|
||||
AccountName: z.string().min(1, "Account Name is required"),
|
||||
AccountNumber: z.string().optional(),
|
||||
Industry: z.string().optional(),
|
||||
Website: z.string().optional(),
|
||||
Notes: z.string().optional(),
|
||||
Address1: z.string().optional(),
|
||||
Address2: z.string().optional(),
|
||||
City: z.string().optional(),
|
||||
State: z.string().optional(),
|
||||
Zip: z.string().optional(),
|
||||
Country: z.string().optional(),
|
||||
});
|
||||
|
||||
export type AccountFormValues = z.infer<typeof accountSchema>;
|
||||
38
src/domain/accounts/types/account.ts
Normal file
38
src/domain/accounts/types/account.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
export interface AccountListItem {
|
||||
id: string | number;
|
||||
name: string;
|
||||
number: string;
|
||||
industry: string;
|
||||
website: string;
|
||||
primaryContact: string;
|
||||
phoneNumber: string;
|
||||
owner: string;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
id?: string | number;
|
||||
AccountName?: string;
|
||||
AccountNumber?: string;
|
||||
Industry?: string;
|
||||
Website?: string;
|
||||
Notes?: string;
|
||||
Address1?: string;
|
||||
Address2?: string;
|
||||
City?: string;
|
||||
State?: string;
|
||||
Zip?: string;
|
||||
Country?: string;
|
||||
}
|
||||
|
||||
export interface AccountsListParams {
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface AccountsListResult {
|
||||
items: AccountListItem[];
|
||||
totalCount: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
13
src/domain/accounts/use-cases/use-account-detail.ts
Normal file
13
src/domain/accounts/use-cases/use-account-detail.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
||||
import { accountsApi } from "@/domain/accounts/api/accounts-api";
|
||||
import type { Account } from "@/domain/accounts/types/account";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
import { requireQueryParam } from "@/lib/query/require-query-param";
|
||||
|
||||
export function useAccountDetail(id: string | undefined): UseQueryResult<Account, Error> {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.accounts.detail(id ?? ""),
|
||||
queryFn: () => accountsApi.getById(requireQueryParam(id, "Account id")),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
}
|
||||
14
src/domain/accounts/use-cases/use-accounts-list.ts
Normal file
14
src/domain/accounts/use-cases/use-accounts-list.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { keepPreviousData, useQuery, type UseQueryResult } from "@tanstack/react-query";
|
||||
import { accountsApi } from "@/domain/accounts/api/accounts-api";
|
||||
import type { AccountsListParams, AccountsListResult } from "@/domain/accounts/types/account";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
||||
export function useAccountsList(
|
||||
params: AccountsListParams,
|
||||
): UseQueryResult<AccountsListResult, Error> {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.accounts.list(params as Record<string, unknown>),
|
||||
queryFn: () => accountsApi.getList(params),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
}
|
||||
24
src/domain/accounts/use-cases/use-create-account.ts
Normal file
24
src/domain/accounts/use-cases/use-create-account.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { accountsApi } from "@/domain/accounts/api/accounts-api";
|
||||
import type { AccountFormValues } from "@/domain/accounts/schemas/account-schema";
|
||||
import type { Account } from "@/domain/accounts/types/account";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
||||
export function useCreateAccount(): UseMutationResult<Account, Error, AccountFormValues> {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (values: AccountFormValues) => accountsApi.create(values),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.accounts.all });
|
||||
toast.success("Account created successfully!");
|
||||
navigate("/accounts");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "Failed to create account");
|
||||
},
|
||||
});
|
||||
}
|
||||
19
src/domain/accounts/use-cases/use-delete-account.ts
Normal file
19
src/domain/accounts/use-cases/use-delete-account.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
|
||||
import { toast } from "react-toastify";
|
||||
import { accountsApi } from "@/domain/accounts/api/accounts-api";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
||||
export function useDeleteAccount(): UseMutationResult<void, Error, string | number> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string | number) => accountsApi.delete(id),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.accounts.all });
|
||||
toast.success("Account deleted successfully!");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "Failed to delete account");
|
||||
},
|
||||
});
|
||||
}
|
||||
30
src/domain/accounts/use-cases/use-update-account.ts
Normal file
30
src/domain/accounts/use-cases/use-update-account.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { accountsApi } from "@/domain/accounts/api/accounts-api";
|
||||
import type { AccountFormValues } from "@/domain/accounts/schemas/account-schema";
|
||||
import type { Account } from "@/domain/accounts/types/account";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
||||
interface UpdateAccountInput {
|
||||
id: string | number;
|
||||
values: AccountFormValues;
|
||||
}
|
||||
|
||||
export function useUpdateAccount(): UseMutationResult<Account, Error, UpdateAccountInput> {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, values }: UpdateAccountInput) => accountsApi.update(id, values),
|
||||
onSuccess: (_data, variables) => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.accounts.all });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.accounts.detail(variables.id) });
|
||||
toast.success("Account updated successfully!");
|
||||
navigate("/accounts");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "Failed to update account");
|
||||
},
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue