enh
This commit is contained in:
parent
edbc506062
commit
86de06078f
9 changed files with 3749 additions and 36 deletions
|
|
@ -1,22 +1,43 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { View, Text } from 'react-native';
|
||||
import { AppContainer } from 'common-ui';
|
||||
import SlotForm from '../../../components/SlotForm';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { trpc } from '../../../src/trpc-client';
|
||||
|
||||
export default function AddSlot() {
|
||||
const router = useRouter();
|
||||
const { baseslot } = useLocalSearchParams();
|
||||
const baseSlotId = baseslot ? parseInt(baseslot as string) : null;
|
||||
|
||||
const { refetch } = trpc.admin.slots.getAll.useQuery();
|
||||
const { data: baseSlotData, isLoading } = trpc.admin.slots.getSlotById.useQuery(
|
||||
{ id: baseSlotId! },
|
||||
{ enabled: !!baseSlotId }
|
||||
);
|
||||
|
||||
const handleSlotAdded = () => {
|
||||
refetch();
|
||||
router.back();
|
||||
};
|
||||
|
||||
if (isLoading && baseSlotId) {
|
||||
return (
|
||||
<AppContainer>
|
||||
<SlotForm onSlotAdded={handleSlotAdded} />
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Text>Loading base slot...</Text>
|
||||
</View>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContainer>
|
||||
<SlotForm
|
||||
onSlotAdded={handleSlotAdded}
|
||||
initialProductIds={baseSlotData?.slot?.products?.map(p => p.id) || []}
|
||||
initialGroupIds={baseSlotData?.slot?.groupIds || []}
|
||||
/>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { View, TouchableOpacity, FlatList } from 'react-native';
|
||||
import { MaterialCommunityIcons, Entypo } from '@expo/vector-icons';
|
||||
import { View, TouchableOpacity, FlatList, Alert } from 'react-native';
|
||||
import { AppContainer, MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity } from 'common-ui';
|
||||
import { trpc } from '../../../src/trpc-client';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
|
@ -20,6 +20,7 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
|
|||
setDialogProducts,
|
||||
setDialogOpen,
|
||||
}) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const slotProducts = slot.products?.map((p: any) => p.name).filter(Boolean) || [];
|
||||
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]}`}>
|
||||
<MyText style={tw`text-xs font-bold ${statusColor.split(' ')[1]}`}>{statusText}</MyText>
|
||||
</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>
|
||||
|
||||
{/* 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 */}
|
||||
<View style={tw`h-[1px] bg-gray-100 mb-4`} />
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ interface SlotFormProps {
|
|||
initialIsActive?: boolean;
|
||||
slotId?: number;
|
||||
initialProductIds?: number[];
|
||||
initialGroupIds?: number[];
|
||||
}
|
||||
|
||||
export default function SlotForm({
|
||||
|
|
@ -29,13 +30,26 @@ export default function SlotForm({
|
|||
initialIsActive = true,
|
||||
slotId,
|
||||
initialProductIds = [],
|
||||
initialGroupIds = [],
|
||||
}: 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 = {
|
||||
deliveryTime: initialDeliveryTime || null,
|
||||
freezeTime: initialFreezeTime || null,
|
||||
selectedGroupIds: [] as number[],
|
||||
selectedProductIds: initialProductIds,
|
||||
vendorSnippetList: [] as VendorSnippet[],
|
||||
deliveryTime: initialDeliveryTime || (slotData?.slot?.deliveryTime ? new Date(slotData.slot.deliveryTime) : null),
|
||||
freezeTime: initialFreezeTime || (slotData?.slot?.freezeTime ? new Date(slotData.slot.freezeTime) : null),
|
||||
selectedGroupIds: initialGroupIds.length > 0 ? initialGroupIds : (slotData?.slot?.groupIds || []),
|
||||
selectedProductIds: initialProductIds.length > 0 ? initialProductIds : (slotData?.slot?.products?.map((p: any) => p.id) || []),
|
||||
vendorSnippetList: vendorSnippetsFromSlot,
|
||||
};
|
||||
|
||||
const { mutate: createSlot, isPending: isCreating } = trpc.admin.slots.createSlot.useMutation();
|
||||
|
|
@ -63,21 +77,15 @@ export default function SlotForm({
|
|||
deliveryTime: values.deliveryTime.toISOString(),
|
||||
freezeTime: values.freezeTime.toISOString(),
|
||||
isActive: initialIsActive,
|
||||
groupIds: values.selectedGroupIds,
|
||||
productIds: values.selectedProductIds,
|
||||
vendorSnippets: values.vendorSnippetList.map(snippet => ({
|
||||
vendorSnippets: values.vendorSnippetList.map((snippet: VendorSnippet) => ({
|
||||
name: snippet.name,
|
||||
productIds: snippet.productIds,
|
||||
validTill: snippet.validTill,
|
||||
})),
|
||||
};
|
||||
|
||||
console.log({snippetList: values.vendorSnippetList})
|
||||
|
||||
values.vendorSnippetList.forEach((snippet, index) => {
|
||||
console.log({snippet})
|
||||
|
||||
});
|
||||
|
||||
|
||||
if (isEditMode && slotId) {
|
||||
updateSlot(
|
||||
|
|
@ -160,7 +168,7 @@ export default function SlotForm({
|
|||
{({ push, remove }) => (
|
||||
<View style={tw`mb-4`}>
|
||||
<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 style={tw`mb-4`}>
|
||||
<MyTextInput
|
||||
|
|
|
|||
1
apps/backend/drizzle/0070_known_ares.sql
Normal file
1
apps/backend/drizzle/0070_known_ares.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "mf"."delivery_slot_info" ADD COLUMN "group_ids" jsonb;
|
||||
3624
apps/backend/drizzle/meta/0070_snapshot.json
Normal file
3624
apps/backend/drizzle/meta/0070_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -491,6 +491,13 @@
|
|||
"when": 1769718702463,
|
||||
"tag": "0069_violet_smiling_tiger",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 70,
|
||||
"version": "7",
|
||||
"when": 1769958949864,
|
||||
"tag": "0070_known_ares",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -192,6 +192,7 @@ export const deliverySlotInfo = mf.table('delivery_slot_info', {
|
|||
isActive: boolean('is_active').notNull().default(true),
|
||||
isFlash: boolean('is_flash').notNull().default(false),
|
||||
deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}),
|
||||
groupIds: jsonb('group_ids').$defaultFn(() => []),
|
||||
});
|
||||
|
||||
export const vendorSnippets = mf.table('vendor_snippets', {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { router, protectedProcedure } from "../trpc-index";
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
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 { ApiError } from "../../lib/api-error";
|
||||
import { appUrl } from "../../lib/env-exporter";
|
||||
|
|
@ -26,6 +26,7 @@ const createSlotSchema = z.object({
|
|||
productIds: z.array(z.number().int().positive()).min(1),
|
||||
validTill: z.string().optional(),
|
||||
})).optional(),
|
||||
groupIds: z.array(z.number()).optional(),
|
||||
});
|
||||
|
||||
const getSlotByIdSchema = z.object({
|
||||
|
|
@ -43,6 +44,7 @@ const updateSlotSchema = z.object({
|
|||
productIds: z.array(z.number().int().positive()).min(1),
|
||||
validTill: z.string().optional(),
|
||||
})).optional(),
|
||||
groupIds: z.array(z.number()).optional(),
|
||||
});
|
||||
|
||||
const deleteSlotSchema = z.object({
|
||||
|
|
@ -229,7 +231,7 @@ export const slotsRouter = router({
|
|||
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
|
||||
if (!deliveryTime || !freezeTime) {
|
||||
|
|
@ -244,6 +246,7 @@ export const slotsRouter = router({
|
|||
deliveryTime: new Date(deliveryTime),
|
||||
freezeTime: new Date(freezeTime),
|
||||
isActive: isActive !== undefined ? isActive : true,
|
||||
groupIds: groupIds !== undefined ? groupIds : [],
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
@ -348,6 +351,7 @@ export const slotsRouter = router({
|
|||
slot: {
|
||||
...slot,
|
||||
deliverySequence: slot.deliverySequence as number[],
|
||||
groupIds: slot.groupIds as number[],
|
||||
products: slot.productSlots.map((ps) => ps.product),
|
||||
vendorSnippets: slot.vendorSnippets?.map(snippet => ({
|
||||
...snippet,
|
||||
|
|
@ -364,12 +368,22 @@ export const slotsRouter = router({
|
|||
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
|
||||
}
|
||||
try{
|
||||
const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets } = input;
|
||||
const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input;
|
||||
|
||||
if (!deliveryTime || !freezeTime) {
|
||||
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) => {
|
||||
const [updatedSlot] = await tx
|
||||
.update(deliverySlotInfo)
|
||||
|
|
@ -377,6 +391,7 @@ export const slotsRouter = router({
|
|||
deliveryTime: new Date(deliveryTime),
|
||||
freezeTime: new Date(freezeTime),
|
||||
isActive: isActive !== undefined ? isActive : true,
|
||||
groupIds: validGroupIds !== undefined ? validGroupIds : [],
|
||||
})
|
||||
.where(eq(deliverySlotInfo.id, id))
|
||||
.returning();
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ function AnimatedFlag({ flagTexture, isCelebrating, time }: { flagTexture: THREE
|
|||
|
||||
// Increase segmentation for better cloth simulation
|
||||
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;
|
||||
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 z = originalPos[i + 2];
|
||||
|
||||
// Distance from flag pole
|
||||
const distanceFromPole = (x + 0.75) / 1.5;
|
||||
// Distance from flag pole (adjusted for larger flag width of 2.5)
|
||||
const distanceFromPole = (x + 1.25) / 2.5;
|
||||
|
||||
// Multiple wave layers
|
||||
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 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 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 yOffset = verticalFlutter * windStrength;
|
||||
|
|
@ -103,8 +103,8 @@ function AnimatedFlag({ flagTexture, isCelebrating, time }: { flagTexture: THREE
|
|||
});
|
||||
|
||||
return (
|
||||
<mesh ref={flagMeshRef} position={[0.75, 1.5, 0]}>
|
||||
<planeGeometry args={[1.5, 0.75, 24, 12]} />
|
||||
<mesh ref={flagMeshRef} position={[1.25, 1.8, 0]}>
|
||||
<planeGeometry args={[2.5, 1.25, 32, 16]} />
|
||||
<meshStandardMaterial
|
||||
map={flagTexture}
|
||||
side={THREE.DoubleSide}
|
||||
|
|
@ -731,23 +731,23 @@ function Character({ color, isCelebrating, delay = 0, type, hairColor = '#2D1B0E
|
|||
</group>
|
||||
</group>
|
||||
|
||||
{/* FLAG */}
|
||||
{/* FLAG - Larger and more prominent */}
|
||||
{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]}>
|
||||
<mesh>
|
||||
<sphereGeometry args={[0.1, 16, 16]} />
|
||||
<sphereGeometry args={[0.12, 16, 16]} />
|
||||
<meshToonMaterial color={skinTone} />
|
||||
</mesh>
|
||||
</group>
|
||||
|
||||
<mesh position={[0, 1.1, 0]}>
|
||||
<cylinderGeometry args={[0.018, 0.018, 2.4, 12]} />
|
||||
<mesh position={[0, 1.4, 0]}>
|
||||
<cylinderGeometry args={[0.022, 0.022, 2.8, 12]} />
|
||||
<meshToonMaterial color="#5d4037" />
|
||||
</mesh>
|
||||
|
||||
<mesh position={[0, 2.3, 0]}>
|
||||
<sphereGeometry args={[0.05, 16, 16]} />
|
||||
<mesh position={[0, 2.8, 0]}>
|
||||
<sphereGeometry args={[0.08, 16, 16]} />
|
||||
<meshStandardMaterial color="#f1c40f" metalness={0.5} roughness={0.3} />
|
||||
</mesh>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue