442 lines
13 KiB
TypeScript
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 || []}
|
|
/>
|
|
</>
|
|
);
|
|
}
|