feat: aggrigated all api calls
This commit is contained in:
parent
524c504b70
commit
3a075c66ec
32 changed files with 597 additions and 167 deletions
|
|
@ -1,26 +1,8 @@
|
||||||
import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy";
|
|
||||||
import { requireSession } from "@/lib/auth/session";
|
import { requireSession } from "@/lib/auth/session";
|
||||||
import type { Address, City, Neighborhood } from "@/types/api";
|
import { getAddresses } from "@/lib/api/addresses";
|
||||||
|
import { getCities, getNeighborhoods } from "@/lib/api/locations";
|
||||||
import { AddressList } from "@/components/customer/address-list";
|
import { AddressList } from "@/components/customer/address-list";
|
||||||
|
|
||||||
async function getAddresses() {
|
|
||||||
const res = await winofyFetch<PaginatedResponse<Address> | Address[]>("/v1/addresses/");
|
|
||||||
return Array.isArray(res) ? res : res.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCities() {
|
|
||||||
const res = await winofyFetch<PaginatedResponse<City> | City[]>("/v1/cities/", { auth: false });
|
|
||||||
return Array.isArray(res) ? res : res.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getNeighborhoods() {
|
|
||||||
const res = await winofyFetch<PaginatedResponse<Neighborhood> | Neighborhood[]>("/v1/neighborhoods/", {
|
|
||||||
auth: false,
|
|
||||||
params: { limit: 200 },
|
|
||||||
});
|
|
||||||
return Array.isArray(res) ? res : res.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function AddressesPage() {
|
export default async function AddressesPage() {
|
||||||
await requireSession("/addresses");
|
await requireSession("/addresses");
|
||||||
const [addresses, cities, neighborhoods] = await Promise.all([getAddresses(), getCities(), getNeighborhoods()]);
|
const [addresses, cities, neighborhoods] = await Promise.all([getAddresses(), getCities(), getNeighborhoods()]);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getCart } from "@/lib/actions/cart";
|
import { getCart } from "@/lib/api/cart";
|
||||||
import { requireSession } from "@/lib/auth/session";
|
import { requireSession } from "@/lib/auth/session";
|
||||||
import { CartItemRow } from "@/components/customer/cart-item-row";
|
import { CartItemRow } from "@/components/customer/cart-item-row";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getCart } from "@/lib/actions/cart";
|
import { getCart } from "@/lib/api/cart";
|
||||||
import { requireSession } from "@/lib/auth/session";
|
import { requireSession } from "@/lib/auth/session";
|
||||||
import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy";
|
import { getAddresses } from "@/lib/api/addresses";
|
||||||
import type { Address } from "@/types/api";
|
|
||||||
import { CheckoutForm } from "@/components/customer/checkout-form";
|
import { CheckoutForm } from "@/components/customer/checkout-form";
|
||||||
|
|
||||||
async function getAddresses() {
|
|
||||||
const res = await winofyFetch<PaginatedResponse<Address> | Address[]>("/v1/addresses/");
|
|
||||||
return Array.isArray(res) ? res : res.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function CheckoutPage() {
|
export default async function CheckoutPage() {
|
||||||
await requireSession("/checkout");
|
await requireSession("/checkout");
|
||||||
const [cart, addresses] = await Promise.all([getCart(), getAddresses()]);
|
const [cart, addresses] = await Promise.all([getCart(), getAddresses()]);
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,17 @@
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { winofyFetch, ApiError } from "@/lib/api/winofy";
|
import { ApiError } from "@/lib/api/winofy";
|
||||||
import { requireSession } from "@/lib/auth/session";
|
import { requireSession } from "@/lib/auth/session";
|
||||||
|
import { getOrderGroup } from "@/lib/api/orders";
|
||||||
import { OrderCard } from "@/components/customer/order-card";
|
import { OrderCard } from "@/components/customer/order-card";
|
||||||
import { PAYMENT_STATUS_LABELS_FA } from "@/lib/order-status";
|
import { PAYMENT_STATUS_LABELS_FA } from "@/lib/order-status";
|
||||||
import type { OrderGroup } from "@/types/api";
|
|
||||||
|
|
||||||
async function getOrderGroup(uuid: string) {
|
|
||||||
try {
|
|
||||||
return await winofyFetch<OrderGroup>(`/v1/order-groups/${uuid}/`);
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof ApiError && err.status === 404) return null;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function OrderGroupPage({ params }: PageProps<"/order-groups/[uuid]">) {
|
export default async function OrderGroupPage({ params }: PageProps<"/order-groups/[uuid]">) {
|
||||||
const { uuid } = await params;
|
const { uuid } = await params;
|
||||||
await requireSession(`/order-groups/${uuid}`);
|
await requireSession(`/order-groups/${uuid}`);
|
||||||
const orderGroup = await getOrderGroup(uuid);
|
const orderGroup = await getOrderGroup(uuid).catch((err) => {
|
||||||
|
if (err instanceof ApiError && err.status === 404) return null;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
if (!orderGroup) notFound();
|
if (!orderGroup) notFound();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy";
|
|
||||||
import { requireSession } from "@/lib/auth/session";
|
import { requireSession } from "@/lib/auth/session";
|
||||||
|
import { getOrderGroups } from "@/lib/api/orders";
|
||||||
import { PAYMENT_STATUS_LABELS_FA } from "@/lib/order-status";
|
import { PAYMENT_STATUS_LABELS_FA } from "@/lib/order-status";
|
||||||
import type { OrderGroup } from "@/types/api";
|
|
||||||
|
|
||||||
async function getOrderGroups() {
|
|
||||||
const res = await winofyFetch<PaginatedResponse<OrderGroup>>("/v1/order-groups/");
|
|
||||||
return res.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function OrdersPage() {
|
export default async function OrdersPage() {
|
||||||
await requireSession("/orders");
|
await requireSession("/orders");
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,17 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { winofyFetch, ApiError } from "@/lib/api/winofy";
|
import { ApiError } from "@/lib/api/winofy";
|
||||||
import type { ProductDetail } from "@/types/api";
|
import { getProduct } from "@/lib/api/catalog";
|
||||||
import { AddToCartButton } from "@/components/customer/add-to-cart-button";
|
import { AddToCartButton } from "@/components/customer/add-to-cart-button";
|
||||||
|
|
||||||
const UNIT_LABELS_FA: Record<string, string> = { gram: "گرم", ml: "میلیلیتر", piece: "عدد" };
|
const UNIT_LABELS_FA: Record<string, string> = { gram: "گرم", ml: "میلیلیتر", piece: "عدد" };
|
||||||
|
|
||||||
async function getProduct(uuid: string) {
|
|
||||||
try {
|
|
||||||
return await winofyFetch<ProductDetail>(`/v1/products/${uuid}/`, { auth: false });
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof ApiError && err.status === 404) return null;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function ProductDetailPage({ params }: PageProps<"/products/[uuid]">) {
|
export default async function ProductDetailPage({ params }: PageProps<"/products/[uuid]">) {
|
||||||
const { uuid } = await params;
|
const { uuid } = await params;
|
||||||
const product = await getProduct(uuid);
|
const product = await getProduct(uuid).catch((err) => {
|
||||||
|
if (err instanceof ApiError && err.status === 404) return null;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
if (!product) notFound();
|
if (!product) notFound();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,21 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { winofyFetch, ApiError, type PaginatedResponse } from "@/lib/api/winofy";
|
import { ApiError } from "@/lib/api/winofy";
|
||||||
import type { ProductListItem, StoreDetail } from "@/types/api";
|
import { getStore } from "@/lib/api/stores";
|
||||||
|
import { getProducts } from "@/lib/api/catalog";
|
||||||
import { AddToCartButton } from "@/components/customer/add-to-cart-button";
|
import { AddToCartButton } from "@/components/customer/add-to-cart-button";
|
||||||
|
|
||||||
const WEEKDAY_LABELS_FA = ["شنبه", "یکشنبه", "دوشنبه", "سهشنبه", "چهارشنبه", "پنجشنبه", "جمعه"];
|
const WEEKDAY_LABELS_FA = ["شنبه", "یکشنبه", "دوشنبه", "سهشنبه", "چهارشنبه", "پنجشنبه", "جمعه"];
|
||||||
|
|
||||||
async function getStore(uuid: string) {
|
|
||||||
try {
|
|
||||||
return await winofyFetch<StoreDetail>(`/v1/stores/${uuid}/`, { auth: false });
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof ApiError && err.status === 404) return null;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStoreProducts(uuid: string) {
|
|
||||||
const res = await winofyFetch<PaginatedResponse<ProductListItem>>("/v1/products/", {
|
|
||||||
auth: false,
|
|
||||||
params: { store: uuid, limit: 100 },
|
|
||||||
});
|
|
||||||
return res.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function StoreDetailPage({ params }: PageProps<"/stores/[uuid]">) {
|
export default async function StoreDetailPage({ params }: PageProps<"/stores/[uuid]">) {
|
||||||
const { uuid } = await params;
|
const { uuid } = await params;
|
||||||
const store = await getStore(uuid);
|
const store = await getStore(uuid).catch((err) => {
|
||||||
|
if (err instanceof ApiError && err.status === 404) return null;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
if (!store) notFound();
|
if (!store) notFound();
|
||||||
|
|
||||||
const products = await getStoreProducts(uuid);
|
const products = await getProducts({ store: uuid, limit: 100 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-6 px-4 py-6">
|
<main className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-6 px-4 py-6">
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,8 @@
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy";
|
|
||||||
import { getLocationServer } from "@/lib/location/server";
|
import { getLocationServer } from "@/lib/location/server";
|
||||||
import type { Neighborhood } from "@/types/api";
|
import { getNeighborhoods } from "@/lib/api/locations";
|
||||||
import { NeighborhoodSearch } from "@/components/location/neighborhood-search";
|
import { NeighborhoodSearch } from "@/components/location/neighborhood-search";
|
||||||
|
|
||||||
async function getNeighborhoods() {
|
|
||||||
const res = await winofyFetch<PaginatedResponse<Neighborhood> | Neighborhood[]>("/v1/neighborhoods/", {
|
|
||||||
auth: false,
|
|
||||||
params: { limit: 200 },
|
|
||||||
});
|
|
||||||
return Array.isArray(res) ? res : res.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function NeighborhoodSearchPage() {
|
export default async function NeighborhoodSearchPage() {
|
||||||
const [neighborhoods, location] = await Promise.all([getNeighborhoods(), getLocationServer()]);
|
const [neighborhoods, location] = await Promise.all([getNeighborhoods(), getLocationServer()]);
|
||||||
const selectedNeighborhoodUuid = location?.mode === "manual" ? location.neighborhoodUuid : null;
|
const selectedNeighborhoodUuid = location?.mode === "manual" ? location.neighborhoodUuid : null;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { getValidSession } from "@/lib/auth/session";
|
import { getValidSession } from "@/lib/auth/session";
|
||||||
import { getLocationServer } from "@/lib/location/server";
|
import { getLocationServer } from "@/lib/location/server";
|
||||||
import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy";
|
import { getStoreCategories, getStores } from "@/lib/api/stores";
|
||||||
import { getCart } from "@/lib/actions/cart";
|
import { getCart } from "@/lib/api/cart";
|
||||||
import type { StoreCategory, StoreListItem } from "@/types/api";
|
|
||||||
import { SiteHeader } from "@/components/home/site-header";
|
import { SiteHeader } from "@/components/home/site-header";
|
||||||
import { SearchBar } from "@/components/home/search-bar";
|
import { SearchBar } from "@/components/home/search-bar";
|
||||||
import { LocationReminderBanner } from "@/components/home/location-reminder-banner";
|
import { LocationReminderBanner } from "@/components/home/location-reminder-banner";
|
||||||
|
|
@ -12,21 +11,6 @@ import { StoreCard } from "@/components/home/store-card";
|
||||||
import { SellerPromoCard } from "@/components/home/seller-promo-card";
|
import { SellerPromoCard } from "@/components/home/seller-promo-card";
|
||||||
import { BottomNav } from "@/components/home/bottom-nav";
|
import { BottomNav } from "@/components/home/bottom-nav";
|
||||||
|
|
||||||
async function getStoreCategories() {
|
|
||||||
const res = await winofyFetch<PaginatedResponse<StoreCategory> | StoreCategory[]>("/v1/store-categories/", {
|
|
||||||
auth: false,
|
|
||||||
});
|
|
||||||
return Array.isArray(res) ? res : res.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStores(params: { category?: string; search?: string; neighborhood?: string; lat?: number; lng?: number }) {
|
|
||||||
const res = await winofyFetch<PaginatedResponse<StoreListItem>>("/v1/stores/", {
|
|
||||||
auth: false,
|
|
||||||
params: { limit: 20, ...params },
|
|
||||||
});
|
|
||||||
return res.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function HomePage({ searchParams }: PageProps<"/">) {
|
export default async function HomePage({ searchParams }: PageProps<"/">) {
|
||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
const category = typeof params.category === "string" ? params.category : undefined;
|
const category = typeof params.category === "string" ? params.category : undefined;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { useRouter, usePathname } from "next/navigation";
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
import { addToCart } from "@/lib/actions/cart";
|
import { addToCart } from "@/lib/api/cart";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils/cn";
|
import { cn } from "@/lib/utils/cn";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useMemo, useState, useTransition } from "react";
|
import { useMemo, useState, useTransition } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { createAddress, deleteAddress, type AddressInput } from "@/lib/actions/addresses";
|
import { createAddress, deleteAddress, type AddressInput } from "@/lib/api/addresses";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import type { Address, AddressLabel, City, Neighborhood } from "@/types/api";
|
import type { Address, AddressLabel, City, Neighborhood } from "@/types/api";
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { updateCartItemQuantity, removeCartItem } from "@/lib/actions/cart";
|
import { updateCartItemQuantity, removeCartItem } from "@/lib/api/cart";
|
||||||
import type { CartItem } from "@/types/api";
|
import type { CartItem } from "@/types/api";
|
||||||
|
|
||||||
export function CartItemRow({ item }: { item: CartItem }) {
|
export function CartItemRow({ item }: { item: CartItem }) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { submitCheckout } from "@/lib/actions/checkout";
|
import { submitCheckout } from "@/lib/api/checkout";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import type { Address, Cart, DeliveryType, PaymentMethod } from "@/types/api";
|
import type { Address, Cart, DeliveryType, PaymentMethod } from "@/types/api";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { cancelOrder } from "@/lib/actions/orders";
|
import { cancelOrder } from "@/lib/api/orders";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ORDER_STATUS_LABELS_FA, ORDER_STATUS_SEQUENCE } from "@/lib/order-status";
|
import { ORDER_STATUS_LABELS_FA, ORDER_STATUS_SEQUENCE } from "@/lib/order-status";
|
||||||
import type { Order } from "@/types/api";
|
import type { Order } from "@/types/api";
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { winofyFetch } from "@/lib/api/winofy";
|
|
||||||
import { callApi, type ActionResult } from "./result";
|
|
||||||
import type { Order } from "@/types/api";
|
|
||||||
|
|
||||||
export async function cancelOrder(uuid: string, reason: string = ""): Promise<ActionResult<Order>> {
|
|
||||||
const result = await callApi(() =>
|
|
||||||
winofyFetch<Order>(`/v1/orders/${uuid}/cancel/`, { method: "POST", body: { reason } }),
|
|
||||||
);
|
|
||||||
if (result.ok) {
|
|
||||||
revalidatePath(`/orders/${uuid}`);
|
|
||||||
revalidatePath("/orders");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { winofyFetch } from "@/lib/api/winofy";
|
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||||
import { callApi, type ActionResult } from "./result";
|
import { callApi, type ActionResult } from "./result";
|
||||||
import type { Address, AddressLabel } from "@/types/api";
|
import type { Address, AddressLabel } from "@/types/api";
|
||||||
|
|
||||||
|
export async function getAddresses(): Promise<Address[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<Address>>("/v1/addresses/");
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AddressInput {
|
export interface AddressInput {
|
||||||
label: AddressLabel;
|
label: AddressLabel;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -22,9 +27,7 @@ export interface AddressInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAddress(input: AddressInput): Promise<ActionResult<Address>> {
|
export async function createAddress(input: AddressInput): Promise<ActionResult<Address>> {
|
||||||
const result = await callApi(() =>
|
const result = await callApi(() => winofyFetch<Address>("/v1/addresses/", { method: "POST", body: input }));
|
||||||
winofyFetch<Address>("/v1/addresses/", { method: "POST", body: input }),
|
|
||||||
);
|
|
||||||
if (result.ok) revalidatePath("/addresses");
|
if (result.ok) revalidatePath("/addresses");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { winofyFetch } from "@/lib/api/winofy";
|
import { winofyFetch } from "./winofy";
|
||||||
import { callApi, type ActionResult } from "./result";
|
import { callApi, type ActionResult } from "./result";
|
||||||
import type { Cart } from "@/types/api";
|
import type { Cart } from "@/types/api";
|
||||||
|
|
||||||
|
export async function getCart(): Promise<Cart> {
|
||||||
|
return winofyFetch<Cart>("/v1/cart/");
|
||||||
|
}
|
||||||
|
|
||||||
export async function addToCart(productUuid: string, quantity: number = 1): Promise<ActionResult> {
|
export async function addToCart(productUuid: string, quantity: number = 1): Promise<ActionResult> {
|
||||||
const result = await callApi(() =>
|
const result = await callApi(() =>
|
||||||
winofyFetch<undefined>("/v1/cart/items/", {
|
winofyFetch<undefined>("/v1/cart/items/", { method: "POST", body: { product_uuid: productUuid, quantity } }),
|
||||||
method: "POST",
|
|
||||||
body: { product_uuid: productUuid, quantity },
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
if (result.ok) revalidatePath("/cart");
|
if (result.ok) revalidatePath("/cart");
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -18,10 +19,7 @@ export async function addToCart(productUuid: string, quantity: number = 1): Prom
|
||||||
|
|
||||||
export async function updateCartItemQuantity(itemUuid: string, quantity: number): Promise<ActionResult> {
|
export async function updateCartItemQuantity(itemUuid: string, quantity: number): Promise<ActionResult> {
|
||||||
const result = await callApi(() =>
|
const result = await callApi(() =>
|
||||||
winofyFetch<undefined>(`/v1/cart/items/${itemUuid}/`, {
|
winofyFetch<undefined>(`/v1/cart/items/${itemUuid}/`, { method: "PATCH", body: { quantity } }),
|
||||||
method: "PATCH",
|
|
||||||
body: { quantity },
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
if (result.ok) revalidatePath("/cart");
|
if (result.ok) revalidatePath("/cart");
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -38,7 +36,3 @@ export async function clearCart(): Promise<ActionResult> {
|
||||||
if (result.ok) revalidatePath("/cart");
|
if (result.ok) revalidatePath("/cart");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCart(): Promise<Cart> {
|
|
||||||
return winofyFetch<Cart>("/v1/cart/");
|
|
||||||
}
|
|
||||||
30
src/lib/api/catalog.ts
Normal file
30
src/lib/api/catalog.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import "server-only";
|
||||||
|
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||||
|
import type { ProductCategory, ProductDetail, ProductListItem } from "@/types/api";
|
||||||
|
|
||||||
|
export async function getProductCategories(): Promise<ProductCategory[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<ProductCategory>>("/v1/product-categories/", {
|
||||||
|
auth: false,
|
||||||
|
params: { limit: 100 },
|
||||||
|
});
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetProductsParams {
|
||||||
|
store?: string;
|
||||||
|
category?: string;
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProducts(params: GetProductsParams = {}): Promise<ProductListItem[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<ProductListItem>>("/v1/products/", {
|
||||||
|
auth: false,
|
||||||
|
params: { limit: 100, ...params },
|
||||||
|
});
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProduct(uuid: string): Promise<ProductDetail> {
|
||||||
|
return winofyFetch<ProductDetail>(`/v1/products/${uuid}/`, { auth: false });
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { winofyFetch } from "@/lib/api/winofy";
|
import { winofyFetch } from "./winofy";
|
||||||
import { callApi, type ActionResult } from "./result";
|
import { callApi, type ActionResult } from "./result";
|
||||||
import type { DeliveryType, OrderGroup, PaymentMethod } from "@/types/api";
|
import type { DeliveryType, OrderGroup, PaymentMethod } from "@/types/api";
|
||||||
|
|
||||||
24
src/lib/api/locations.ts
Normal file
24
src/lib/api/locations.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import "server-only";
|
||||||
|
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||||
|
import type { City, Neighborhood } from "@/types/api";
|
||||||
|
|
||||||
|
export async function getCities(): Promise<City[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<City>>("/v1/cities/", { auth: false, params: { limit: 100 } });
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCity(uuid: string): Promise<City> {
|
||||||
|
return winofyFetch<City>(`/v1/cities/${uuid}/`, { auth: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNeighborhoods(params: { city?: string; limit?: number } = {}): Promise<Neighborhood[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<Neighborhood>>("/v1/neighborhoods/", {
|
||||||
|
auth: false,
|
||||||
|
params: { limit: 200, ...params },
|
||||||
|
});
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNeighborhood(uuid: string): Promise<Neighborhood> {
|
||||||
|
return winofyFetch<Neighborhood>(`/v1/neighborhoods/${uuid}/`, { auth: false });
|
||||||
|
}
|
||||||
22
src/lib/api/media.ts
Normal file
22
src/lib/api/media.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { winofyFetch } from "./winofy";
|
||||||
|
import { callApi, type ActionResult } from "./result";
|
||||||
|
|
||||||
|
export interface MediaPresignResult {
|
||||||
|
upload_url: string;
|
||||||
|
object_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 1 of the upload flow (§2 of the API guide): get a presigned MinIO PUT
|
||||||
|
* URL. Step 2 — PUT the raw file bytes to `upload_url` directly (client-side,
|
||||||
|
* no auth header, this is a MinIO URL not a Winofy one) — happens in the
|
||||||
|
* browser, not here. Step 3 — send the returned `object_key` as the value of
|
||||||
|
* whatever field you're setting (Store.logo, Product.image, etc.).
|
||||||
|
*/
|
||||||
|
export async function presignUpload(filename: string): Promise<ActionResult<MediaPresignResult>> {
|
||||||
|
return callApi(() =>
|
||||||
|
winofyFetch<MediaPresignResult>("/media/presign/", { method: "POST", body: { filename } }),
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/lib/api/notifications.ts
Normal file
31
src/lib/api/notifications.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||||
|
import { callApi, type ActionResult } from "./result";
|
||||||
|
import type { Notification } from "@/types/api";
|
||||||
|
|
||||||
|
export async function getNotifications(): Promise<Notification[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<Notification>>("/v1/notifications/");
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNotification(uuid: string): Promise<Notification> {
|
||||||
|
return winofyFetch<Notification>(`/v1/notifications/${uuid}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markNotificationRead(uuid: string): Promise<ActionResult<Notification>> {
|
||||||
|
const result = await callApi(() =>
|
||||||
|
winofyFetch<Notification>(`/v1/notifications/${uuid}/mark-read/`, { method: "POST", body: {} }),
|
||||||
|
);
|
||||||
|
if (result.ok) revalidatePath("/notifications");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markAllNotificationsRead(): Promise<ActionResult> {
|
||||||
|
const result = await callApi(() =>
|
||||||
|
winofyFetch<undefined>("/v1/notifications/mark-all-read/", { method: "POST", body: {} }),
|
||||||
|
);
|
||||||
|
if (result.ok) revalidatePath("/notifications");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
28
src/lib/api/orders.ts
Normal file
28
src/lib/api/orders.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||||
|
import { callApi, type ActionResult } from "./result";
|
||||||
|
import type { Order, OrderGroup } from "@/types/api";
|
||||||
|
|
||||||
|
export async function getOrderGroups(): Promise<OrderGroup[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<OrderGroup>>("/v1/order-groups/");
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrderGroup(uuid: string): Promise<OrderGroup> {
|
||||||
|
return winofyFetch<OrderGroup>(`/v1/order-groups/${uuid}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Single-order tracking — one store's slice of an OrderGroup. */
|
||||||
|
export async function getOrder(uuid: string): Promise<Order> {
|
||||||
|
return winofyFetch<Order>(`/v1/orders/${uuid}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelOrder(uuid: string, cancelReason: string = ""): Promise<ActionResult<Order>> {
|
||||||
|
const result = await callApi(() =>
|
||||||
|
winofyFetch<Order>(`/v1/orders/${uuid}/cancel/`, { method: "POST", body: { cancel_reason: cancelReason } }),
|
||||||
|
);
|
||||||
|
if (result.ok) revalidatePath("/orders");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import "server-only";
|
import "server-only";
|
||||||
import { ApiError } from "@/lib/api/winofy";
|
import { ApiError } from "./winofy";
|
||||||
|
|
||||||
export type ActionResult<T = undefined> =
|
export type ActionResult<T = undefined> =
|
||||||
| { ok: true; data: T }
|
| { ok: true; data: T }
|
||||||
| { ok: false; status: number; message: string; fieldErrors: Record<string, string[]> | null };
|
| { ok: false; status: number; message: string; fieldErrors: Record<string, string[]> | null };
|
||||||
|
|
||||||
|
/** Wraps a winofyFetch call for use in a Server Action — never throws, always returns a typed result. */
|
||||||
export async function callApi<T>(fn: () => Promise<T>): Promise<ActionResult<T>> {
|
export async function callApi<T>(fn: () => Promise<T>): Promise<ActionResult<T>> {
|
||||||
try {
|
try {
|
||||||
const data = await fn();
|
const data = await fn();
|
||||||
23
src/lib/api/reviews.ts
Normal file
23
src/lib/api/reviews.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||||
|
import { callApi, type ActionResult } from "./result";
|
||||||
|
import type { Review } from "@/types/api";
|
||||||
|
|
||||||
|
export async function getReviews(params: { store?: string; limit?: number } = {}): Promise<Review[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<Review>>("/v1/reviews/", {
|
||||||
|
auth: false,
|
||||||
|
params: { limit: 50, ...params },
|
||||||
|
});
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateReviewInput {
|
||||||
|
order_uuid: string;
|
||||||
|
rating: number;
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createReview(input: CreateReviewInput): Promise<ActionResult<Review>> {
|
||||||
|
return callApi(() => winofyFetch<Review>("/v1/reviews/", { method: "POST", body: input }));
|
||||||
|
}
|
||||||
8
src/lib/api/seller/analytics.ts
Normal file
8
src/lib/api/seller/analytics.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import "server-only";
|
||||||
|
import { winofyFetch } from "../winofy";
|
||||||
|
import type { SellerAnalytics } from "@/types/api";
|
||||||
|
|
||||||
|
/** No date-range params are documented — the API returns one fixed reporting window. */
|
||||||
|
export async function getSellerAnalytics(): Promise<SellerAnalytics> {
|
||||||
|
return winofyFetch<SellerAnalytics>("/v1/seller/analytics/");
|
||||||
|
}
|
||||||
60
src/lib/api/seller/orders.ts
Normal file
60
src/lib/api/seller/orders.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { winofyFetch, type PaginatedResponse } from "../winofy";
|
||||||
|
import { callApi, type ActionResult } from "../result";
|
||||||
|
import type { Order, OrderStatus } from "@/types/api";
|
||||||
|
|
||||||
|
export async function getSellerOrders(status?: OrderStatus): Promise<Order[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<Order>>("/v1/seller/orders/", {
|
||||||
|
params: { limit: 100, status },
|
||||||
|
});
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSellerOrder(uuid: string): Promise<Order> {
|
||||||
|
return winofyFetch<Order>(`/v1/seller/orders/${uuid}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sellerOrderAction(
|
||||||
|
uuid: string,
|
||||||
|
action: string,
|
||||||
|
body: Record<string, unknown> = {},
|
||||||
|
): Promise<ActionResult<Order>> {
|
||||||
|
const result = await callApi(() =>
|
||||||
|
winofyFetch<Order>(`/v1/seller/orders/${uuid}/${action}/`, { method: "POST", body }),
|
||||||
|
);
|
||||||
|
if (result.ok) {
|
||||||
|
revalidatePath("/seller/orders");
|
||||||
|
revalidatePath(`/seller/orders/${uuid}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelSellerOrder(uuid: string, cancelReason: string = ""): Promise<ActionResult<Order>> {
|
||||||
|
return sellerOrderAction(uuid, "cancel", { cancel_reason: cancelReason });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** placed → preparing */
|
||||||
|
export async function confirmSellerOrder(uuid: string): Promise<ActionResult<Order>> {
|
||||||
|
return sellerOrderAction(uuid, "confirm");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** preparing → ready_to_ship */
|
||||||
|
export async function markSellerOrderReady(uuid: string): Promise<ActionResult<Order>> {
|
||||||
|
return sellerOrderAction(uuid, "mark-ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ready_to_ship → handed_to_courier */
|
||||||
|
export async function markSellerOrderShipped(
|
||||||
|
uuid: string,
|
||||||
|
courierName: string,
|
||||||
|
courierPhone: string,
|
||||||
|
): Promise<ActionResult<Order>> {
|
||||||
|
return sellerOrderAction(uuid, "mark-shipped", { courier_name: courierName, courier_phone: courierPhone });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** handed_to_courier → delivered */
|
||||||
|
export async function markSellerOrderDelivered(uuid: string): Promise<ActionResult<Order>> {
|
||||||
|
return sellerOrderAction(uuid, "mark-delivered");
|
||||||
|
}
|
||||||
65
src/lib/api/seller/products.ts
Normal file
65
src/lib/api/seller/products.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { winofyFetch, type PaginatedResponse } from "../winofy";
|
||||||
|
import { callApi, type ActionResult } from "../result";
|
||||||
|
import type { SellerProduct } from "@/types/api";
|
||||||
|
|
||||||
|
export async function getSellerProducts(): Promise<SellerProduct[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<SellerProduct>>("/v1/seller/products/", { params: { limit: 100 } });
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSellerProduct(uuid: string): Promise<SellerProduct> {
|
||||||
|
return winofyFetch<SellerProduct>(`/v1/seller/products/${uuid}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SellerProductInput {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
image?: string | null;
|
||||||
|
category_uuid: string;
|
||||||
|
price: number;
|
||||||
|
unit_type: SellerProduct["unit_type"];
|
||||||
|
unit_value: number;
|
||||||
|
stock_quantity: number;
|
||||||
|
low_stock_threshold: number;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSellerProduct(input: SellerProductInput): Promise<ActionResult<SellerProduct>> {
|
||||||
|
const result = await callApi(() =>
|
||||||
|
winofyFetch<SellerProduct>("/v1/seller/products/", { method: "POST", body: input }),
|
||||||
|
);
|
||||||
|
if (result.ok) revalidatePath("/seller/products");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSellerProduct(
|
||||||
|
uuid: string,
|
||||||
|
input: Partial<SellerProductInput>,
|
||||||
|
): Promise<ActionResult<SellerProduct>> {
|
||||||
|
const result = await callApi(() =>
|
||||||
|
winofyFetch<SellerProduct>(`/v1/seller/products/${uuid}/`, { method: "PATCH", body: input }),
|
||||||
|
);
|
||||||
|
if (result.ok) revalidatePath("/seller/products");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSellerProduct(uuid: string): Promise<ActionResult> {
|
||||||
|
const result = await callApi(() => winofyFetch<undefined>(`/v1/seller/products/${uuid}/`, { method: "DELETE" }));
|
||||||
|
if (result.ok) revalidatePath("/seller/products");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Focused inventory-only update — returns 204, no body, unlike the general PATCH above. */
|
||||||
|
export async function updateSellerProductStock(
|
||||||
|
uuid: string,
|
||||||
|
input: { stock_quantity: number; low_stock_threshold?: number },
|
||||||
|
): Promise<ActionResult> {
|
||||||
|
const result = await callApi(() =>
|
||||||
|
winofyFetch<undefined>(`/v1/seller/products/${uuid}/stock/`, { method: "PATCH", body: input }),
|
||||||
|
);
|
||||||
|
if (result.ok) revalidatePath("/seller/products");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
26
src/lib/api/seller/reviews.ts
Normal file
26
src/lib/api/seller/reviews.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { winofyFetch, type PaginatedResponse } from "../winofy";
|
||||||
|
import { callApi, type ActionResult } from "../result";
|
||||||
|
import type { Review } from "@/types/api";
|
||||||
|
|
||||||
|
export async function getSellerReviews(): Promise<Review[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<Review>>("/v1/seller/reviews/", { params: { limit: 100 } });
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ The spec documents this action's request body as the full customer-facing
|
||||||
|
* review-write shape (order_uuid/rating/comment), which doesn't make sense for
|
||||||
|
* a reply. Sending `seller_reply` here as the best guess (matches the response
|
||||||
|
* field name) — confirm the real field name with backend (or from a 400's
|
||||||
|
* field-error keys) before relying on this.
|
||||||
|
*/
|
||||||
|
export async function replySellerReview(uuid: string, replyText: string): Promise<ActionResult<Review>> {
|
||||||
|
const result = await callApi(() =>
|
||||||
|
winofyFetch<Review>(`/v1/seller/reviews/${uuid}/reply/`, { method: "POST", body: { seller_reply: replyText } }),
|
||||||
|
);
|
||||||
|
if (result.ok) revalidatePath("/seller/reviews");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
65
src/lib/api/seller/store.ts
Normal file
65
src/lib/api/seller/store.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { winofyFetch, ApiError } from "../winofy";
|
||||||
|
import { callApi, type ActionResult } from "../result";
|
||||||
|
import type { SellerStore, WorkingHours } from "@/types/api";
|
||||||
|
|
||||||
|
/** `null` means the seller hasn't created a store yet (404) — not an error state to surface. */
|
||||||
|
export async function getSellerStore(): Promise<SellerStore | null> {
|
||||||
|
try {
|
||||||
|
return await winofyFetch<SellerStore>("/v1/seller/store/");
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError && err.status === 404) return null;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SellerStoreInput {
|
||||||
|
name: string;
|
||||||
|
category_uuid: string;
|
||||||
|
description: string;
|
||||||
|
logo?: string | null;
|
||||||
|
cover_image?: string | null;
|
||||||
|
phone_number: string;
|
||||||
|
city_uuid: string;
|
||||||
|
address: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
service_neighborhood_uuids: string[];
|
||||||
|
delivery_radius_km: string;
|
||||||
|
min_order_amount: number;
|
||||||
|
delivery_fee: number;
|
||||||
|
free_delivery_threshold?: number | null;
|
||||||
|
accepts_wallet: boolean;
|
||||||
|
accepts_online: boolean;
|
||||||
|
accepts_cash_on_delivery: boolean;
|
||||||
|
is_open: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSellerStore(input: SellerStoreInput): Promise<ActionResult<SellerStore>> {
|
||||||
|
const result = await callApi(() => winofyFetch<SellerStore>("/v1/seller/store/", { method: "POST", body: input }));
|
||||||
|
if (result.ok) revalidatePath("/seller");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSellerStore(input: Partial<SellerStoreInput>): Promise<ActionResult<SellerStore>> {
|
||||||
|
const result = await callApi(() =>
|
||||||
|
winofyFetch<SellerStore>("/v1/seller/store/", { method: "PATCH", body: input }),
|
||||||
|
);
|
||||||
|
if (result.ok) revalidatePath("/seller");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSellerWorkingHours(): Promise<WorkingHours[]> {
|
||||||
|
return winofyFetch<WorkingHours[]>("/v1/seller/store/working-hours/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full replace — send all 7 days (weekday 0–6) in one call. */
|
||||||
|
export async function setSellerWorkingHours(hours: WorkingHours[]): Promise<ActionResult<WorkingHours[]>> {
|
||||||
|
const result = await callApi(() =>
|
||||||
|
winofyFetch<WorkingHours[]>("/v1/seller/store/working-hours/", { method: "PUT", body: hours }),
|
||||||
|
);
|
||||||
|
if (result.ok) revalidatePath("/seller");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
32
src/lib/api/stores.ts
Normal file
32
src/lib/api/stores.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import "server-only";
|
||||||
|
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||||
|
import type { StoreCategory, StoreDetail, StoreListItem } from "@/types/api";
|
||||||
|
|
||||||
|
export async function getStoreCategories(): Promise<StoreCategory[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<StoreCategory>>("/v1/store-categories/", {
|
||||||
|
auth: false,
|
||||||
|
params: { limit: 100 },
|
||||||
|
});
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetStoresParams {
|
||||||
|
category?: string;
|
||||||
|
search?: string;
|
||||||
|
neighborhood?: string;
|
||||||
|
lat?: number;
|
||||||
|
lng?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStores(params: GetStoresParams = {}): Promise<StoreListItem[]> {
|
||||||
|
const res = await winofyFetch<PaginatedResponse<StoreListItem>>("/v1/stores/", {
|
||||||
|
auth: false,
|
||||||
|
params: { limit: 20, ...params },
|
||||||
|
});
|
||||||
|
return res.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStore(uuid: string): Promise<StoreDetail> {
|
||||||
|
return winofyFetch<StoreDetail>(`/v1/stores/${uuid}/`, { auth: false });
|
||||||
|
}
|
||||||
149
src/types/api.ts
149
src/types/api.ts
|
|
@ -1,13 +1,17 @@
|
||||||
/**
|
/**
|
||||||
* Shared API types mirrored from FRONTEND_GUIDE.md. Money fields are integer Toman.
|
* Shared API types, mirrored from winofyapifrontendintegrationguide.md + the
|
||||||
|
* OpenAPI spec (source of truth for field names/shapes). Money fields are
|
||||||
|
* integer Toman. Grouped to match src/lib/api/'s feature layout.
|
||||||
*
|
*
|
||||||
* Media fields (`icon`, `image`, `logo`, `cover_image`) are MinIO object keys, not
|
* Media fields (`icon`, `image`, `logo`, `cover_image`) are MinIO object keys,
|
||||||
* URLs — per FRONTEND_GUIDE.md §4.7 (feature/minio-integration), every one of them
|
* not URLs — every one has a `<field>_url` sibling that's a freshly presigned,
|
||||||
* has a `<field>_url` sibling that's a freshly presigned, already-absolute URL.
|
* already-absolute URL. Always render the `_url` field directly; never resolve
|
||||||
* Always render the `_url` field directly; never resolve the raw key yourself, and
|
* the raw key yourself, and don't cache `_url` past the response — it's
|
||||||
* don't cache `_url` past the response — it's re-signed (and can change) on every read.
|
* re-signed (and can change) on every read.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// ---- Locations --------------------------------------------------------------
|
||||||
|
|
||||||
export interface City {
|
export interface City {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -17,10 +21,13 @@ export interface City {
|
||||||
export interface Neighborhood {
|
export interface Neighborhood {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
slug?: string;
|
||||||
/** Nested object, confirmed against staging — not a bare uuid string. */
|
/** Nested object, confirmed against staging — not a bare uuid string. */
|
||||||
city: City;
|
city: City;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Stores / Catalog ---------------------------------------------------
|
||||||
|
|
||||||
export interface StoreCategory {
|
export interface StoreCategory {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -46,11 +53,10 @@ export interface StoreListItem {
|
||||||
is_open: boolean;
|
is_open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface WorkingHours {
|
export interface WorkingHours {
|
||||||
weekday: 0 | 1 | 2 | 3 | 4 | 5 | 6; // 0 = Saturday .. 6 = Friday
|
weekday: 0 | 1 | 2 | 3 | 4 | 5 | 6; // 0 = Saturday .. 6 = Friday
|
||||||
opens_at: string;
|
opens_at: string | null;
|
||||||
closes_at: string;
|
closes_at: string | null;
|
||||||
is_closed: boolean;
|
is_closed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,6 +98,8 @@ export interface ProductDetail extends ProductListItem {
|
||||||
store: StoreListItem;
|
store: StoreListItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Addresses ----------------------------------------------------------
|
||||||
|
|
||||||
export type AddressLabel = "home" | "work" | "other";
|
export type AddressLabel = "home" | "work" | "other";
|
||||||
|
|
||||||
export interface Address {
|
export interface Address {
|
||||||
|
|
@ -112,6 +120,8 @@ export interface Address {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Cart -----------------------------------------------------------------
|
||||||
|
|
||||||
export interface CartItem {
|
export interface CartItem {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
product: ProductListItem;
|
product: ProductListItem;
|
||||||
|
|
@ -133,20 +143,16 @@ export interface Cart {
|
||||||
grand_total: number;
|
grand_total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Checkout / Orders ---------------------------------------------------
|
||||||
|
|
||||||
export type DeliveryType = "express" | "scheduled";
|
export type DeliveryType = "express" | "scheduled";
|
||||||
export type PaymentMethod = "wallet" | "online" | "cash_on_delivery";
|
export type PaymentMethod = "wallet" | "online" | "cash_on_delivery";
|
||||||
export type PaymentStatus = "pending" | "paid" | "failed";
|
export type PaymentStatus = "pending" | "paid" | "failed";
|
||||||
export type OrderStatus =
|
export type OrderStatus = "placed" | "preparing" | "ready_to_ship" | "handed_to_courier" | "delivered" | "cancelled";
|
||||||
| "placed"
|
|
||||||
| "preparing"
|
|
||||||
| "ready_to_ship"
|
|
||||||
| "handed_to_courier"
|
|
||||||
| "delivered"
|
|
||||||
| "cancelled";
|
|
||||||
|
|
||||||
export interface OrderItem {
|
export interface OrderItem {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
product: string;
|
product: string | null;
|
||||||
product_name_snapshot: string;
|
product_name_snapshot: string;
|
||||||
unit_price_snapshot: number;
|
unit_price_snapshot: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|
@ -195,3 +201,112 @@ export interface OrderGroup {
|
||||||
/** `feature/payment` only. */
|
/** `feature/payment` only. */
|
||||||
payment?: { method: PaymentMethod; status: string; payment_url?: string };
|
payment?: { method: PaymentMethod; status: string; payment_url?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Reviews --------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface Review {
|
||||||
|
uuid: string;
|
||||||
|
store: string;
|
||||||
|
product: string | null;
|
||||||
|
rating: number;
|
||||||
|
comment: string;
|
||||||
|
seller_reply: string | null;
|
||||||
|
customer_name: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Notifications --------------------------------------------------------
|
||||||
|
|
||||||
|
export type NotificationType = "new_order" | "settlement_done" | "new_review" | "order_cancelled";
|
||||||
|
|
||||||
|
export interface Notification {
|
||||||
|
uuid: string;
|
||||||
|
type: NotificationType;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
related_order: string | null;
|
||||||
|
is_read: boolean;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Seller · Store ---------------------------------------------------
|
||||||
|
|
||||||
|
export type SellerStoreStatus = "pending" | "approved" | "suspended";
|
||||||
|
|
||||||
|
export interface StoreBankAccount {
|
||||||
|
iban: string;
|
||||||
|
account_holder_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SellerStore {
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
category: StoreCategory;
|
||||||
|
description: string;
|
||||||
|
logo: string | null;
|
||||||
|
logo_url: string | null;
|
||||||
|
cover_image: string | null;
|
||||||
|
cover_image_url: string | null;
|
||||||
|
phone_number: string;
|
||||||
|
city: City;
|
||||||
|
address: string;
|
||||||
|
service_neighborhoods: Neighborhood[];
|
||||||
|
/** DRF decimal-as-string, e.g. "5.00". */
|
||||||
|
delivery_radius_km: string;
|
||||||
|
min_order_amount: number;
|
||||||
|
delivery_fee: number;
|
||||||
|
free_delivery_threshold: number | null;
|
||||||
|
accepts_wallet: boolean;
|
||||||
|
accepts_online: boolean;
|
||||||
|
accepts_cash_on_delivery: boolean;
|
||||||
|
rating_avg: string;
|
||||||
|
rating_count: number;
|
||||||
|
status: SellerStoreStatus;
|
||||||
|
is_open: boolean;
|
||||||
|
bank_account: StoreBankAccount | null;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Seller · Products ---------------------------------------------------
|
||||||
|
|
||||||
|
export interface SellerProduct {
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
image: string | null;
|
||||||
|
image_url: string | null;
|
||||||
|
category: ProductCategory;
|
||||||
|
price: number;
|
||||||
|
unit_type: UnitType;
|
||||||
|
unit_value: number;
|
||||||
|
stock_quantity: number;
|
||||||
|
low_stock_threshold: number;
|
||||||
|
is_active: boolean;
|
||||||
|
sold_count: number;
|
||||||
|
is_out_of_stock: boolean;
|
||||||
|
is_low_stock: boolean;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Seller · Analytics ---------------------------------------------------
|
||||||
|
|
||||||
|
export interface SalesChartPoint {
|
||||||
|
bucket: string;
|
||||||
|
sales: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TopProduct {
|
||||||
|
name: string;
|
||||||
|
units_sold: number;
|
||||||
|
revenue: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SellerAnalytics {
|
||||||
|
period: string;
|
||||||
|
order_count: number;
|
||||||
|
total_sales: number;
|
||||||
|
cancellation_rate: number;
|
||||||
|
average_order_value: number;
|
||||||
|
chart: SalesChartPoint[];
|
||||||
|
top_products: TopProduct[];
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue