This commit is contained in:
shafi54 2026-02-01 21:01:05 +05:30
parent edbc506062
commit 86de06078f
9 changed files with 3749 additions and 36 deletions

View file

@ -1,22 +1,43 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View, Text } from 'react-native';
import { AppContainer } from 'common-ui'; import { AppContainer } from 'common-ui';
import SlotForm from '../../../components/SlotForm'; import SlotForm from '../../../components/SlotForm';
import { useRouter } from 'expo-router'; import { useRouter, useLocalSearchParams } from 'expo-router';
import { trpc } from '../../../src/trpc-client'; import { trpc } from '../../../src/trpc-client';
export default function AddSlot() { export default function AddSlot() {
const router = useRouter(); const router = useRouter();
const { baseslot } = useLocalSearchParams();
const baseSlotId = baseslot ? parseInt(baseslot as string) : null;
const { refetch } = trpc.admin.slots.getAll.useQuery(); const { refetch } = trpc.admin.slots.getAll.useQuery();
const { data: baseSlotData, isLoading } = trpc.admin.slots.getSlotById.useQuery(
{ id: baseSlotId! },
{ enabled: !!baseSlotId }
);
const handleSlotAdded = () => { const handleSlotAdded = () => {
refetch(); refetch();
router.back(); router.back();
}; };
if (isLoading && baseSlotId) {
return (
<AppContainer>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Loading base slot...</Text>
</View>
</AppContainer>
);
}
return ( return (
<AppContainer> <AppContainer>
<SlotForm onSlotAdded={handleSlotAdded} /> <SlotForm
onSlotAdded={handleSlotAdded}
initialProductIds={baseSlotData?.slot?.products?.map(p => p.id) || []}
initialGroupIds={baseSlotData?.slot?.groupIds || []}
/>
</AppContainer> </AppContainer>
); );
} }

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons, Entypo } from '@expo/vector-icons';
import { View, TouchableOpacity, FlatList } from 'react-native'; import { View, TouchableOpacity, FlatList, Alert } from 'react-native';
import { AppContainer, MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity } from 'common-ui'; import { AppContainer, MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity } from 'common-ui';
import { trpc } from '../../../src/trpc-client'; import { trpc } from '../../../src/trpc-client';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
@ -20,6 +20,7 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
setDialogProducts, setDialogProducts,
setDialogOpen, setDialogOpen,
}) => { }) => {
const [menuOpen, setMenuOpen] = useState(false);
const slotProducts = slot.products?.map((p: any) => p.name).filter(Boolean) || []; const slotProducts = slot.products?.map((p: any) => p.name).filter(Boolean) || [];
const displayProducts = slotProducts.slice(0, 2).join(', '); const displayProducts = slotProducts.slice(0, 2).join(', ');
@ -57,9 +58,44 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
<View style={tw`px-3 py-1 rounded-full ${statusColor.split(' ')[0]}`}> <View style={tw`px-3 py-1 rounded-full ${statusColor.split(' ')[0]}`}>
<MyText style={tw`text-xs font-bold ${statusColor.split(' ')[1]}`}>{statusText}</MyText> <MyText style={tw`text-xs font-bold ${statusColor.split(' ')[1]}`}>{statusText}</MyText>
</View> </View>
<TouchableOpacity
onPress={() => setMenuOpen(true)}
style={tw`ml-2 p-1`}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Entypo name="dots-three-vertical" size={20} color="#9CA3AF" />
</TouchableOpacity>
</View> </View>
</View> </View>
{/* Replicate Menu Dialog */}
<BottomDialog open={menuOpen} onClose={() => setMenuOpen(false)}>
<View style={tw`p-4`}>
<MyText style={tw`text-lg font-bold mb-4`}>Slot #{slot.id} Actions</MyText>
<TouchableOpacity
onPress={() => {
setMenuOpen(false);
router.push(`/add-slot?baseslot=${slot.id}` as any);
}}
style={tw`py-4 border-b border-gray-200`}
>
<View style={tw`flex-row items-center`}>
<MaterialCommunityIcons name="content-copy" size={20} color="#4B5563" style={tw`mr-3`} />
<MyText style={tw`text-base text-gray-800`}>Replicate Slot</MyText>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setMenuOpen(false)}
style={tw`py-4 mt-2`}
>
<View style={tw`flex-row items-center`}>
<MaterialCommunityIcons name="close" size={20} color="#EF4444" style={tw`mr-3`} />
<MyText style={tw`text-base text-red-500`}>Cancel</MyText>
</View>
</TouchableOpacity>
</View>
</BottomDialog>
{/* Divider */} {/* Divider */}
<View style={tw`h-[1px] bg-gray-100 mb-4`} /> <View style={tw`h-[1px] bg-gray-100 mb-4`} />

View file

@ -20,6 +20,7 @@ interface SlotFormProps {
initialIsActive?: boolean; initialIsActive?: boolean;
slotId?: number; slotId?: number;
initialProductIds?: number[]; initialProductIds?: number[];
initialGroupIds?: number[];
} }
export default function SlotForm({ export default function SlotForm({
@ -29,13 +30,26 @@ export default function SlotForm({
initialIsActive = true, initialIsActive = true,
slotId, slotId,
initialProductIds = [], initialProductIds = [],
initialGroupIds = [],
}: SlotFormProps) { }: SlotFormProps) {
const { data: slotData } = trpc.admin.slots.getSlotById.useQuery(
{ id: slotId! },
{ enabled: !!slotId }
);
const vendorSnippetsFromSlot = (slotData?.slot?.vendorSnippets || []).map((snippet: any) => ({
name: snippet.name || '',
groupIds: snippet.groupIds || [],
productIds: snippet.productIds || [],
validTill: snippet.validTill || undefined,
})) as VendorSnippet[];
const initialValues = { const initialValues = {
deliveryTime: initialDeliveryTime || null, deliveryTime: initialDeliveryTime || (slotData?.slot?.deliveryTime ? new Date(slotData.slot.deliveryTime) : null),
freezeTime: initialFreezeTime || null, freezeTime: initialFreezeTime || (slotData?.slot?.freezeTime ? new Date(slotData.slot.freezeTime) : null),
selectedGroupIds: [] as number[], selectedGroupIds: initialGroupIds.length > 0 ? initialGroupIds : (slotData?.slot?.groupIds || []),
selectedProductIds: initialProductIds, selectedProductIds: initialProductIds.length > 0 ? initialProductIds : (slotData?.slot?.products?.map((p: any) => p.id) || []),
vendorSnippetList: [] as VendorSnippet[], vendorSnippetList: vendorSnippetsFromSlot,
}; };
const { mutate: createSlot, isPending: isCreating } = trpc.admin.slots.createSlot.useMutation(); const { mutate: createSlot, isPending: isCreating } = trpc.admin.slots.createSlot.useMutation();
@ -63,21 +77,15 @@ export default function SlotForm({
deliveryTime: values.deliveryTime.toISOString(), deliveryTime: values.deliveryTime.toISOString(),
freezeTime: values.freezeTime.toISOString(), freezeTime: values.freezeTime.toISOString(),
isActive: initialIsActive, isActive: initialIsActive,
groupIds: values.selectedGroupIds,
productIds: values.selectedProductIds, productIds: values.selectedProductIds,
vendorSnippets: values.vendorSnippetList.map(snippet => ({ vendorSnippets: values.vendorSnippetList.map((snippet: VendorSnippet) => ({
name: snippet.name, name: snippet.name,
productIds: snippet.productIds, productIds: snippet.productIds,
validTill: snippet.validTill, validTill: snippet.validTill,
})), })),
}; };
console.log({snippetList: values.vendorSnippetList})
values.vendorSnippetList.forEach((snippet, index) => {
console.log({snippet})
});
if (isEditMode && slotId) { if (isEditMode && slotId) {
updateSlot( updateSlot(
@ -160,7 +168,7 @@ export default function SlotForm({
{({ push, remove }) => ( {({ push, remove }) => (
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
<Text style={tw`text-lg font-semibold mb-4`}>Vendor Snippets</Text> <Text style={tw`text-lg font-semibold mb-4`}>Vendor Snippets</Text>
{values.vendorSnippetList.map((snippet, index) => ( {values.vendorSnippetList.map((snippet: VendorSnippet, index: number) => (
<View key={index} style={tw`bg-gray-50 p-4 rounded-lg mb-4`}> <View key={index} style={tw`bg-gray-50 p-4 rounded-lg mb-4`}>
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
<MyTextInput <MyTextInput

View file

@ -0,0 +1 @@
ALTER TABLE "mf"."delivery_slot_info" ADD COLUMN "group_ids" jsonb;

File diff suppressed because it is too large Load diff

View file

@ -491,6 +491,13 @@
"when": 1769718702463, "when": 1769718702463,
"tag": "0069_violet_smiling_tiger", "tag": "0069_violet_smiling_tiger",
"breakpoints": true "breakpoints": true
},
{
"idx": 70,
"version": "7",
"when": 1769958949864,
"tag": "0070_known_ares",
"breakpoints": true
} }
] ]
} }

View file

@ -192,6 +192,7 @@ export const deliverySlotInfo = mf.table('delivery_slot_info', {
isActive: boolean('is_active').notNull().default(true), isActive: boolean('is_active').notNull().default(true),
isFlash: boolean('is_flash').notNull().default(false), isFlash: boolean('is_flash').notNull().default(false),
deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}), deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}),
groupIds: jsonb('group_ids').$defaultFn(() => []),
}); });
export const vendorSnippets = mf.table('vendor_snippets', { export const vendorSnippets = mf.table('vendor_snippets', {

View file

@ -2,7 +2,7 @@ import { router, protectedProcedure } from "../trpc-index";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { db } from "../../db/db_index"; import { db } from "../../db/db_index";
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets } from "../../db/schema"; import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "../../db/schema";
import { eq, inArray, and, desc } from "drizzle-orm"; import { eq, inArray, and, desc } from "drizzle-orm";
import { ApiError } from "../../lib/api-error"; import { ApiError } from "../../lib/api-error";
import { appUrl } from "../../lib/env-exporter"; import { appUrl } from "../../lib/env-exporter";
@ -26,6 +26,7 @@ const createSlotSchema = z.object({
productIds: z.array(z.number().int().positive()).min(1), productIds: z.array(z.number().int().positive()).min(1),
validTill: z.string().optional(), validTill: z.string().optional(),
})).optional(), })).optional(),
groupIds: z.array(z.number()).optional(),
}); });
const getSlotByIdSchema = z.object({ const getSlotByIdSchema = z.object({
@ -43,6 +44,7 @@ const updateSlotSchema = z.object({
productIds: z.array(z.number().int().positive()).min(1), productIds: z.array(z.number().int().positive()).min(1),
validTill: z.string().optional(), validTill: z.string().optional(),
})).optional(), })).optional(),
groupIds: z.array(z.number()).optional(),
}); });
const deleteSlotSchema = z.object({ const deleteSlotSchema = z.object({
@ -229,7 +231,7 @@ export const slotsRouter = router({
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets } = input; const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input;
// Validate required fields // Validate required fields
if (!deliveryTime || !freezeTime) { if (!deliveryTime || !freezeTime) {
@ -244,6 +246,7 @@ export const slotsRouter = router({
deliveryTime: new Date(deliveryTime), deliveryTime: new Date(deliveryTime),
freezeTime: new Date(freezeTime), freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
groupIds: groupIds !== undefined ? groupIds : [],
}) })
.returning(); .returning();
@ -348,6 +351,7 @@ export const slotsRouter = router({
slot: { slot: {
...slot, ...slot,
deliverySequence: slot.deliverySequence as number[], deliverySequence: slot.deliverySequence as number[],
groupIds: slot.groupIds as number[],
products: slot.productSlots.map((ps) => ps.product), products: slot.productSlots.map((ps) => ps.product),
vendorSnippets: slot.vendorSnippets?.map(snippet => ({ vendorSnippets: slot.vendorSnippets?.map(snippet => ({
...snippet, ...snippet,
@ -364,12 +368,22 @@ export const slotsRouter = router({
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
try{ try{
const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets } = input; const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input;
if (!deliveryTime || !freezeTime) { if (!deliveryTime || !freezeTime) {
throw new ApiError("Delivery time and orders close time are required", 400); throw new ApiError("Delivery time and orders close time are required", 400);
} }
// Filter groupIds to only include valid (existing) groups
let validGroupIds = groupIds;
if (groupIds && groupIds.length > 0) {
const existingGroups = await db.query.productGroupInfo.findMany({
where: inArray(productGroupInfo.id, groupIds),
columns: { id: true },
});
validGroupIds = existingGroups.map(g => g.id);
}
return await db.transaction(async (tx) => { return await db.transaction(async (tx) => {
const [updatedSlot] = await tx const [updatedSlot] = await tx
.update(deliverySlotInfo) .update(deliverySlotInfo)
@ -377,6 +391,7 @@ export const slotsRouter = router({
deliveryTime: new Date(deliveryTime), deliveryTime: new Date(deliveryTime),
freezeTime: new Date(freezeTime), freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
groupIds: validGroupIds !== undefined ? validGroupIds : [],
}) })
.where(eq(deliverySlotInfo.id, id)) .where(eq(deliverySlotInfo.id, id))
.returning(); .returning();

View file

@ -54,7 +54,7 @@ function AnimatedFlag({ flagTexture, isCelebrating, time }: { flagTexture: THREE
// Increase segmentation for better cloth simulation // Increase segmentation for better cloth simulation
geometry.dispose(); geometry.dispose();
const newGeometry = new THREE.PlaneGeometry(1.5, 0.75, 24, 12); const newGeometry = new THREE.PlaneGeometry(2.5, 1.25, 32, 16);
flagMeshRef.current.geometry = newGeometry; flagMeshRef.current.geometry = newGeometry;
originalPositions.current = new Float32Array(newGeometry.attributes.position.array); originalPositions.current = new Float32Array(newGeometry.attributes.position.array);
} }
@ -77,15 +77,15 @@ function AnimatedFlag({ flagTexture, isCelebrating, time }: { flagTexture: THREE
const y = originalPos[i + 1]; const y = originalPos[i + 1];
const z = originalPos[i + 2]; const z = originalPos[i + 2];
// Distance from flag pole // Distance from flag pole (adjusted for larger flag width of 2.5)
const distanceFromPole = (x + 0.75) / 1.5; const distanceFromPole = (x + 1.25) / 2.5;
// Multiple wave layers // Multiple wave layers
const wave1 = Math.sin(elapsed * flutterSpeed * 0.5 + distanceFromPole * 4) * 0.15 * distanceFromPole * windStrength; const wave1 = Math.sin(elapsed * flutterSpeed * 0.5 + distanceFromPole * 4) * 0.15 * distanceFromPole * windStrength;
const wave2 = Math.sin(elapsed * flutterSpeed + distanceFromPole * 8 + y * 3) * 0.08 * distanceFromPole * windStrength; const wave2 = Math.sin(elapsed * flutterSpeed + distanceFromPole * 8 + y * 3) * 0.08 * distanceFromPole * windStrength;
const wave3 = Math.sin(elapsed * flutterSpeed * 2 + distanceFromPole * 12 + y * 5) * 0.04 * distanceFromPole * windStrength; const wave3 = Math.sin(elapsed * flutterSpeed * 2 + distanceFromPole * 12 + y * 5) * 0.04 * distanceFromPole * windStrength;
const turbulence = noise(elapsed * 2 + distanceFromPole * 6) * 0.06 * distanceFromPole * windStrength; const turbulence = noise(elapsed * 2 + distanceFromPole * 6) * 0.06 * distanceFromPole * windStrength;
const verticalFlutter = Math.sin(elapsed * flutterSpeed * 0.7 + distanceFromPole * 3) * 0.05 * (1 - Math.abs(y) / 0.375) * distanceFromPole; const verticalFlutter = Math.sin(elapsed * flutterSpeed * 0.7 + distanceFromPole * 3) * 0.05 * (1 - Math.abs(y) / 0.625) * distanceFromPole;
const zOffset = (wave1 + wave2 + wave3 + turbulence) * (0.3 + distanceFromPole * 0.7); const zOffset = (wave1 + wave2 + wave3 + turbulence) * (0.3 + distanceFromPole * 0.7);
const yOffset = verticalFlutter * windStrength; const yOffset = verticalFlutter * windStrength;
@ -103,8 +103,8 @@ function AnimatedFlag({ flagTexture, isCelebrating, time }: { flagTexture: THREE
}); });
return ( return (
<mesh ref={flagMeshRef} position={[0.75, 1.5, 0]}> <mesh ref={flagMeshRef} position={[1.25, 1.8, 0]}>
<planeGeometry args={[1.5, 0.75, 24, 12]} /> <planeGeometry args={[2.5, 1.25, 32, 16]} />
<meshStandardMaterial <meshStandardMaterial
map={flagTexture} map={flagTexture}
side={THREE.DoubleSide} side={THREE.DoubleSide}
@ -731,23 +731,23 @@ function Character({ color, isCelebrating, delay = 0, type, hairColor = '#2D1B0E
</group> </group>
</group> </group>
{/* FLAG */} {/* FLAG - Larger and more prominent */}
{holdsFlag && ( {holdsFlag && (
<group position={[0.4, 1.4, 0.15]} rotation={[0, 0, -0.25]}> <group position={[0.5, 1.5, 0.2]} rotation={[0, 0, -0.15]}>
<group position={[0, 0, 0]}> <group position={[0, 0, 0]}>
<mesh> <mesh>
<sphereGeometry args={[0.1, 16, 16]} /> <sphereGeometry args={[0.12, 16, 16]} />
<meshToonMaterial color={skinTone} /> <meshToonMaterial color={skinTone} />
</mesh> </mesh>
</group> </group>
<mesh position={[0, 1.1, 0]}> <mesh position={[0, 1.4, 0]}>
<cylinderGeometry args={[0.018, 0.018, 2.4, 12]} /> <cylinderGeometry args={[0.022, 0.022, 2.8, 12]} />
<meshToonMaterial color="#5d4037" /> <meshToonMaterial color="#5d4037" />
</mesh> </mesh>
<mesh position={[0, 2.3, 0]}> <mesh position={[0, 2.8, 0]}>
<sphereGeometry args={[0.05, 16, 16]} /> <sphereGeometry args={[0.08, 16, 16]} />
<meshStandardMaterial color="#f1c40f" metalness={0.5} roughness={0.3} /> <meshStandardMaterial color="#f1c40f" metalness={0.5} roughness={0.3} />
</mesh> </mesh>