freshyo/apps/admin-ui/app/(drawer)/vendor-snippets/index.tsx
2026-01-25 02:05:09 +05:30

442 lines
13 KiB
TypeScript

import React, { useState } from "react";
import {
View,
ScrollView,
TouchableOpacity,
Alert,
Dimensions,
Share,
} from "react-native";
import {
theme,
AppContainer,
MyText,
tw,
useManualRefresh,
useMarkDataFetchers,
MyTouchableOpacity,
} from "common-ui";
import { trpc, trpcClient } from "../../../src/trpc-client";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { Ionicons } from "@expo/vector-icons";
import dayjs from "dayjs";
import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router";
import VendorSnippetForm from "../../../components/VendorSnippetForm";
import SnippetOrdersView from "../../../components/SnippetOrdersView";
import { SnippetMenu } from "../../../components/SnippetMenu";
import { ProductListDialog } from "../../../components/ProductListDialog";
import {
VendorSnippet,
VendorSnippetProduct,
VendorSnippetForm as VendorSnippetFormType,
} from "../../../types/vendor-snippets";
const SnippetItem = ({
snippet,
onEdit,
onDelete,
onViewOrders,
onViewProducts,
index,
}: {
snippet: VendorSnippet;
onEdit: (snippet: VendorSnippet) => void;
onDelete: (id: number) => void;
onViewOrders: (snippetCode: string) => void;
onViewProducts: (products: VendorSnippetProduct[]) => void;
index: number;
}) => {
const isExpired =
snippet.validTill && dayjs(snippet.validTill).isBefore(dayjs());
const handleCopyLink = async () => {
try {
if (!snippet.accessUrl) {
Alert.alert("Error", "No link available");
return;
}
await Share.share({
message: snippet.accessUrl,
});
} catch (error) {
Alert.alert("Error", "Failed to share link");
}
};
return (
<View style={tw``}>
<View style={tw`p-6`}>
{/* Top Header: ID & Status */}
<View style={tw`flex-row justify-between items-start mb-6`}>
<View style={tw`flex-row items-center flex-1`}>
<View
style={tw`w-12 h-12 rounded-2xl bg-brand50 items-center justify-center mr-4`}
>
<MaterialIcons
name="fingerprint"
size={24}
color={theme.colors.brand600}
/>
</View>
<View style={tw`flex-1`}>
<MyText
style={tw`text-slate-400 text-[10px] font-black uppercase tracking-widest`}
>
Identifier
</MyText>
<MyText
style={tw`text-xl font-black text-slate-900`}
numberOfLines={1}
>
{snippet.snippetCode}
</MyText>
</View>
</View>
<View style={tw`flex-row items-center`}>
<View
style={[
tw`px-3 py-1.5 rounded-full flex-row items-center mr-2`,
{ backgroundColor: isExpired ? "#FFF1F2" : "#F0FDF4" },
]}
>
<View
style={[
tw`w-1.5 h-1.5 rounded-full mr-2`,
{ backgroundColor: isExpired ? "#E11D48" : "#10B981" },
]}
/>
<MyText
style={[
tw`text-[10px] font-black uppercase tracking-tighter`,
{ color: isExpired ? "#E11D48" : "#10B981" },
]}
>
{isExpired ? "Expired" : "Active"}
</MyText>
</View>
<SnippetMenu
snippet={snippet}
onEdit={onEdit}
onDelete={onDelete}
onViewOrders={onViewOrders}
/>
</View>
</View>
{/* Middle: Delivery Banner */}
<View
style={tw`bg-slate-50 rounded-3xl p-4 flex-row items-center mb-6 border border-slate-100`}
>
<View
style={tw`bg-white w-10 h-10 rounded-2xl items-center justify-center shadow-sm`}
>
<MaterialIcons name="event-available" size={20} color="#64748B" />
</View>
<View style={tw`ml-4 flex-1`}>
{snippet.isPermanent ? (
<>
<MyText style={tw`text-slate-900 font-extrabold text-sm`}>
Permanent
</MyText>
<MyText style={tw`text-slate-500 text-[10px] font-bold uppercase`}>
Always Active
</MyText>
</>
) : snippet.slot?.deliveryTime ? (
<>
<MyText style={tw`text-slate-900 font-extrabold text-sm`}>
{dayjs(snippet.slot.deliveryTime).format("ddd, MMM DD")}
</MyText>
<MyText style={tw`text-slate-500 text-[10px] font-bold uppercase`}>
Time: {dayjs(snippet.slot.deliveryTime).format("hh:mm A")}
</MyText>
</>
) : (
<>
<MyText style={tw`text-slate-900 font-extrabold text-sm`}>
Schedule Pending
</MyText>
<MyText style={tw`text-slate-500 text-[10px] font-bold uppercase`}>
N/A
</MyText>
</>
)}
</View>
{snippet.validTill && (
<View style={tw`items-end`}>
<MyText style={tw`text-slate-400 text-[9px] font-bold uppercase`}>
Expires
</MyText>
<MyText
style={[
tw`text-xs font-black`,
{ color: isExpired ? "#E11D48" : "#64748B" },
]}
>
{dayjs(snippet.validTill).format("MMM DD")}
</MyText>
</View>
)}
</View>
{/* Stats & Actions */}
<View style={tw`flex-row items-center justify-between`}>
<View style={tw`flex-row items-center`}>
<MyTouchableOpacity
onPress={() => onViewProducts(snippet.products)}
style={tw`flex-row items-center mr-4`}
>
<MaterialIcons name="shopping-bag" size={14} color="#94A3B8" />
<MyText style={tw`text-xs font-bold text-slate-500 ml-1.5`}>
{snippet.productIds.length} Items
</MyText>
<MaterialIcons name="chevron-right" size={14} color="#94A3B8" />
</MyTouchableOpacity>
<MyTouchableOpacity
onPress={handleCopyLink}
style={tw`flex-row items-center`}
>
<MaterialIcons
name="link"
size={16}
color={theme.colors.brand500}
/>
<MyText style={tw`text-xs font-black text-brand600 ml-1`}>
Share
</MyText>
</MyTouchableOpacity>
</View>
<MyTouchableOpacity
// onPress={() => router.push(`/(drawer)/slots/slot-details?slotId=${snippet.slotId}`)}
onPress={() => {}}
activeOpacity={0.7}
style={tw`flex-row items-center`}
>
<MyText
style={tw`text-xs font-black text-slate-900 uppercase tracking-widest`}
>
View Slot
</MyText>
<MaterialIcons name="chevron-right" size={16} color="#000" />
</MyTouchableOpacity>
</View>
</View>
</View>
);
};
export default function VendorSnippets() {
// const { data: snippets, isLoading, error, refetch } = useVendorSnippets();
const {
data: snippets,
isLoading,
error,
refetch,
} = trpc.admin.vendorSnippets.getAll.useQuery();
const createSnippet = trpc.admin.vendorSnippets.create.useMutation();
const updateSnippet = trpc.admin.vendorSnippets.update.useMutation();
const deleteSnippet = trpc.admin.vendorSnippets.delete.useMutation();
// const createSnippet = useCreateVendorSnippet();
// const updateSnippet = useUpdateVendorSnippet();
// const deleteSnippet = useDeleteVendorSnippet();
const router = useRouter();
const [showCreateForm, setShowCreateForm] = useState(false);
const [editingSnippet, setEditingSnippet] =
useState<VendorSnippetFormType | null>(null);
const [showOrdersView, setShowOrdersView] = useState(false);
const [ordersData, setOrdersData] = useState<any>(null);
const [showProductList, setShowProductList] = useState(false);
const [selectedProducts, setSelectedProducts] = useState<VendorSnippetProduct[] | null>(null);
useManualRefresh(refetch);
useMarkDataFetchers(() => {
refetch();
});
const handleCreate = () => {
setShowCreateForm(true);
setEditingSnippet(null);
};
const handleViewProducts = (products: VendorSnippetProduct[]) => {
setSelectedProducts(products);
setShowProductList(true);
};
const handleEdit = (snippet: VendorSnippet) => {
// Convert to match the form's expected type
const formSnippet: VendorSnippetFormType = {
id: snippet.id,
snippetCode: snippet.snippetCode,
slotId: snippet.slotId || 0, // Convert null to number for form
isPermanent: snippet.isPermanent,
productIds: snippet.productIds,
validTill: snippet.validTill,
createdAt: snippet.createdAt,
};
setEditingSnippet(formSnippet);
setShowCreateForm(true);
};
const handleDelete = (id: number) => {
deleteSnippet.mutate(
{ id },
{
onSuccess: () => {
refetch();
},
},
);
};
const handleViewOrders = async (snippetCode: string) => {
try {
const result =
await trpcClient.admin.vendorSnippets.getOrdersBySnippet.query({
snippetCode,
});
if (result.success) {
setOrdersData({
orders: result.data,
snippetCode: snippetCode,
});
setShowOrdersView(true);
} else {
Alert.alert("Error", "Failed to fetch orders");
}
} catch (error: any) {
Alert.alert("Error", error.message || "Failed to fetch orders");
}
};
if (isLoading) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center`}>
<MyText style={tw`text-gray-600`}>Loading vendor snippets...</MyText>
</View>
</AppContainer>
);
}
if (error) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center`}>
<MyText style={tw`text-red-600`}>Error loading snippets</MyText>
</View>
</AppContainer>
);
}
if (showOrdersView && ordersData) {
return (
<SnippetOrdersView
orders={ordersData.orders}
snippetCode={ordersData.snippetCode}
onClose={() => {
setShowOrdersView(false);
setOrdersData(null);
}}
/>
);
}
if (showCreateForm) {
return (
<VendorSnippetForm
snippet={editingSnippet}
onClose={() => {
setShowCreateForm(false);
setEditingSnippet(null);
}}
onSuccess={() => {
setShowCreateForm(false);
setEditingSnippet(null);
refetch();
}}
/>
);
}
return (
<>
<AppContainer>
<View style={tw`flex-1 bg-white h-full`}>
<ScrollView
style={tw`flex-1`}
contentContainerStyle={tw` pt-2 pb-32`}
showsVerticalScrollIndicator={false}
>
{snippets && snippets.length === 0 ? (
<View style={tw`flex-1 justify-center items-center py-20`}>
<View
style={tw`w-24 h-24 bg-slate-50 rounded-full items-center justify-center mb-6`}
>
<Ionicons name="code-working" size={48} color="#94A3B8" />
</View>
<MyText
style={tw`text-slate-900 text-xl font-black tracking-tight`}
>
No Snippets Yet
</MyText>
<MyText
style={tw`text-slate-500 text-center mt-2 font-medium px-8`}
>
Start by creating your first vendor identifier using the
button below.
</MyText>
</View>
) : (
snippets?.map((snippet, index) => (
<React.Fragment key={snippet.id}>
<SnippetItem
snippet={snippet}
index={index}
onEdit={handleEdit}
onDelete={handleDelete}
onViewOrders={handleViewOrders}
onViewProducts={handleViewProducts}
/>
{index < snippets.length - 1 && (
<View style={tw`h-px bg-slate-200 w-full`} />
)}
</React.Fragment>
))
)}
</ScrollView>
{/* Global Floating Action Button - Fixed Position */}
</View>
</AppContainer>
<MyTouchableOpacity
onPress={handleCreate}
activeOpacity={0.95}
style={tw`absolute bottom-8 right-6 shadow-2xl z-50`}
>
<LinearGradient
colors={["#1570EF", "#194185"]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={tw`w-16 h-16 rounded-[24px] items-center justify-center shadow-lg shadow-brand300`}
>
<MaterialIcons name="add" size={32} color="white" />
</LinearGradient>
</MyTouchableOpacity>
<ProductListDialog
open={showProductList}
onClose={() => setShowProductList(false)}
products={selectedProducts || []}
/>
</>
);
}