443 lines
14 KiB
TypeScript
443 lines
14 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { View, ScrollView, Alert, FlatList, TouchableOpacity } from 'react-native';
|
|
import {
|
|
theme,
|
|
AppContainer,
|
|
MyText,
|
|
tw,
|
|
useManualRefresh,
|
|
useMarkDataFetchers,
|
|
MyTouchableOpacity,
|
|
RawBottomDialog,
|
|
BottomDialog,
|
|
} from 'common-ui';
|
|
import { trpc } from '../../../src/trpc-client';
|
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|
import { Ionicons, Entypo } from '@expo/vector-icons';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
|
|
import AvailabilityScheduleForm from '../../../components/AvailabilityScheduleForm';
|
|
|
|
interface Schedule {
|
|
id: number;
|
|
scheduleName: string;
|
|
time: string;
|
|
action: 'in' | 'out';
|
|
createdAt: string;
|
|
lastUpdated: string;
|
|
productIds: number[];
|
|
groupIds: number[];
|
|
productCount: number;
|
|
groupCount: number;
|
|
}
|
|
|
|
const ScheduleItem = ({
|
|
schedule,
|
|
onDelete,
|
|
index,
|
|
onViewProducts,
|
|
onViewGroups,
|
|
onReplicate,
|
|
}: {
|
|
schedule: Schedule;
|
|
onDelete: (id: number) => void;
|
|
index: number;
|
|
onViewProducts: (productIds: number[]) => void;
|
|
onViewGroups: (groupIds: number[]) => void;
|
|
onReplicate: (schedule: Schedule) => void;
|
|
}) => {
|
|
const isIn = schedule.action === 'in';
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
|
|
return (
|
|
<View style={tw``}>
|
|
<View style={tw`p-6`}>
|
|
{/* Top Header: Name & Action Badge */}
|
|
<View style={tw`flex-row justify-between items-start mb-4`}>
|
|
<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="schedule"
|
|
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`}
|
|
>
|
|
Schedule Name
|
|
</MyText>
|
|
<MyText
|
|
style={tw`text-xl font-black text-slate-900`}
|
|
numberOfLines={1}
|
|
>
|
|
{schedule.scheduleName}
|
|
</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: isIn ? '#F0FDF4' : '#FFF1F2' },
|
|
]}
|
|
>
|
|
<View
|
|
style={[
|
|
tw`w-1.5 h-1.5 rounded-full mr-2`,
|
|
{ backgroundColor: isIn ? '#10B981' : '#E11D48' },
|
|
]}
|
|
/>
|
|
<MyText
|
|
style={[
|
|
tw`text-[10px] font-black uppercase tracking-tighter`,
|
|
{ color: isIn ? '#10B981' : '#E11D48' },
|
|
]}
|
|
>
|
|
{isIn ? 'In Stock' : 'Out of Stock'}
|
|
</MyText>
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={() => setMenuOpen(true)}
|
|
style={tw`p-1`}
|
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
>
|
|
<Entypo name="dots-three-vertical" size={20} color="#9CA3AF" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Menu Dialog */}
|
|
<BottomDialog open={menuOpen} onClose={() => setMenuOpen(false)}>
|
|
<View style={tw`p-4`}>
|
|
<MyText style={tw`text-lg font-bold mb-4`}>{schedule.scheduleName}</MyText>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
setMenuOpen(false);
|
|
onReplicate(schedule);
|
|
}}
|
|
style={tw`py-4 border-b border-gray-200`}
|
|
>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MaterialIcons name="content-copy" size={20} color="#4B5563" style={tw`mr-3`} />
|
|
<MyText style={tw`text-base text-gray-800`}>Replicate items</MyText>
|
|
</View>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
setMenuOpen(false);
|
|
Alert.alert('Coming Soon', 'Edit functionality will be available soon');
|
|
}}
|
|
style={tw`py-4 border-b border-gray-200`}
|
|
>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MaterialIcons name="edit" size={20} color="#4B5563" style={tw`mr-3`} />
|
|
<MyText style={tw`text-base text-gray-800`}>Edit</MyText>
|
|
</View>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
setMenuOpen(false);
|
|
onDelete(schedule.id);
|
|
}}
|
|
style={tw`py-4 border-b border-gray-200`}
|
|
>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MaterialIcons name="delete" size={20} color="#E11D48" style={tw`mr-3`} />
|
|
<MyText style={tw`text-base text-red-500`}>Delete</MyText>
|
|
</View>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => setMenuOpen(false)}
|
|
style={tw`py-4 mt-2`}
|
|
>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MaterialIcons name="close" size={20} color="#6B7280" style={tw`mr-3`} />
|
|
<MyText style={tw`text-base text-gray-600`}>Cancel</MyText>
|
|
</View>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</BottomDialog>
|
|
|
|
{/* Middle: Time Banner */}
|
|
<View
|
|
style={tw`bg-slate-50 rounded-3xl p-4 flex-row items-center mb-4 border border-slate-100`}
|
|
>
|
|
<View
|
|
style={tw`bg-white w-10 h-10 rounded-2xl items-center justify-center shadow-sm`}
|
|
>
|
|
<MaterialIcons name="access-time" size={20} color="#64748B" />
|
|
</View>
|
|
<View style={tw`ml-4 flex-1`}>
|
|
<MyText style={tw`text-slate-900 font-extrabold text-sm`}>
|
|
{schedule.time}
|
|
</MyText>
|
|
<MyText style={tw`text-slate-500 text-[10px] font-bold uppercase`}>
|
|
Daily at this time
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Stats & Actions */}
|
|
<View style={tw`flex-row items-center justify-between`}>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MyTouchableOpacity
|
|
onPress={() => onViewProducts(schedule.productIds)}
|
|
style={tw`flex-row items-center mr-4`}
|
|
>
|
|
<MaterialIcons name="shopping-bag" size={14} color="#94A3B8" />
|
|
<MyText style={tw`text-xs font-bold text-brand600 ml-1.5`}>
|
|
{schedule.productCount} Products
|
|
</MyText>
|
|
</MyTouchableOpacity>
|
|
{schedule.groupCount > 0 && (
|
|
<MyTouchableOpacity
|
|
onPress={() => onViewGroups(schedule.groupIds)}
|
|
style={tw`flex-row items-center`}
|
|
>
|
|
<MaterialIcons name="category" size={14} color="#94A3B8" />
|
|
<MyText style={tw`text-xs font-bold text-brand600 ml-1.5`}>
|
|
{schedule.groupCount} Groups
|
|
</MyText>
|
|
</MyTouchableOpacity>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default function StockingSchedules() {
|
|
const {
|
|
data: schedules,
|
|
isLoading,
|
|
error,
|
|
refetch,
|
|
} = trpc.admin.productAvailabilitySchedules.getAll.useQuery();
|
|
|
|
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
|
|
const { data: groupsData } = trpc.admin.product.getGroups.useQuery();
|
|
|
|
const deleteSchedule = trpc.admin.productAvailabilitySchedules.delete.useMutation();
|
|
|
|
const [showCreateForm, setShowCreateForm] = useState(false);
|
|
|
|
// Dialog state
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [dialogType, setDialogType] = useState<'products' | 'groups'>('products');
|
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
|
|
|
// Replication state
|
|
const [replicatingSchedule, setReplicatingSchedule] = useState<Schedule | null>(null);
|
|
|
|
useManualRefresh(refetch);
|
|
|
|
useMarkDataFetchers(() => {
|
|
refetch();
|
|
});
|
|
|
|
const handleCreate = () => {
|
|
setShowCreateForm(true);
|
|
};
|
|
|
|
const handleDelete = (id: number) => {
|
|
Alert.alert(
|
|
'Delete Schedule',
|
|
'Are you sure you want to delete this schedule? This action cannot be undone.',
|
|
[
|
|
{ text: 'Cancel', style: 'cancel' },
|
|
{
|
|
text: 'Delete',
|
|
style: 'destructive',
|
|
onPress: () => {
|
|
deleteSchedule.mutate(
|
|
{ id },
|
|
{
|
|
onSuccess: () => {
|
|
refetch();
|
|
},
|
|
onError: (error: any) => {
|
|
Alert.alert('Error', error.message || 'Failed to delete schedule');
|
|
},
|
|
},
|
|
);
|
|
},
|
|
},
|
|
],
|
|
);
|
|
};
|
|
|
|
const handleViewProducts = (productIds: number[]) => {
|
|
setDialogType('products');
|
|
setSelectedIds(productIds);
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const handleViewGroups = (groupIds: number[]) => {
|
|
setDialogType('groups');
|
|
setSelectedIds(groupIds);
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const handleReplicate = (schedule: Schedule) => {
|
|
setReplicatingSchedule(schedule);
|
|
setShowCreateForm(true);
|
|
};
|
|
|
|
const handleCloseForm = () => {
|
|
setShowCreateForm(false);
|
|
setReplicatingSchedule(null);
|
|
};
|
|
|
|
// Get product/group names from IDs
|
|
const getProductNames = () => {
|
|
const allProducts = productsData?.products || [];
|
|
return selectedIds.map(id => {
|
|
const product = allProducts.find(p => p.id === id);
|
|
return product?.name || `Product #${id}`;
|
|
});
|
|
};
|
|
|
|
const getGroupNames = () => {
|
|
const allGroups = groupsData?.groups || [];
|
|
return selectedIds.map(id => {
|
|
const group = allGroups.find(g => g.id === id);
|
|
return group?.groupName || `Group #${id}`;
|
|
});
|
|
};
|
|
|
|
if (showCreateForm) {
|
|
return (
|
|
<AvailabilityScheduleForm
|
|
onClose={handleCloseForm}
|
|
onSuccess={() => {
|
|
refetch();
|
|
handleCloseForm();
|
|
}}
|
|
initialProductIds={replicatingSchedule?.productIds}
|
|
initialGroupIds={replicatingSchedule?.groupIds}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 justify-center items-center`}>
|
|
<MyText style={tw`text-gray-600`}>Loading schedules...</MyText>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 justify-center items-center`}>
|
|
<MyText style={tw`text-red-600`}>Error loading schedules</MyText>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<AppContainer>
|
|
<View style={tw`flex-1 bg-white h-full`}>
|
|
<ScrollView
|
|
style={tw`flex-1`}
|
|
contentContainerStyle={tw`pt-2 pb-32`}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{schedules && schedules.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="time-outline" size={48} color="#94A3B8" />
|
|
</View>
|
|
<MyText
|
|
style={tw`text-slate-900 text-xl font-black tracking-tight`}
|
|
>
|
|
No Schedules Yet
|
|
</MyText>
|
|
<MyText
|
|
style={tw`text-slate-500 text-center mt-2 font-medium px-8`}
|
|
>
|
|
Start by creating your first availability schedule using the
|
|
button below.
|
|
</MyText>
|
|
</View>
|
|
) : (
|
|
schedules?.map((schedule, index) => (
|
|
<React.Fragment key={schedule.id}>
|
|
<ScheduleItem
|
|
schedule={schedule}
|
|
index={index}
|
|
onDelete={handleDelete}
|
|
onViewProducts={handleViewProducts}
|
|
onViewGroups={handleViewGroups}
|
|
onReplicate={handleReplicate}
|
|
/>
|
|
{index < schedules.length - 1 && (
|
|
<View style={tw`h-px bg-slate-200 w-full`} />
|
|
)}
|
|
</React.Fragment>
|
|
))
|
|
)}
|
|
</ScrollView>
|
|
</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>
|
|
|
|
{/* Products/Groups Dialog */}
|
|
<RawBottomDialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
|
|
<View style={tw`p-4`}>
|
|
<MyText style={tw`text-lg font-bold mb-4`}>
|
|
{dialogType === 'products' ? 'Products' : 'Groups'}
|
|
</MyText>
|
|
<FlatList
|
|
data={dialogType === 'products' ? getProductNames() : getGroupNames()}
|
|
keyExtractor={(item, index) => index.toString()}
|
|
renderItem={({ item }) => (
|
|
<View style={tw`py-3 border-b border-gray-100`}>
|
|
<MyText style={tw`text-base text-gray-800`}>{item}</MyText>
|
|
</View>
|
|
)}
|
|
showsVerticalScrollIndicator={false}
|
|
style={tw`max-h-80`}
|
|
ListEmptyComponent={
|
|
<View style={tw`py-8 items-center`}>
|
|
<MyText style={tw`text-gray-500`}>
|
|
No {dialogType} found
|
|
</MyText>
|
|
</View>
|
|
}
|
|
/>
|
|
</View>
|
|
</RawBottomDialog>
|
|
</>
|
|
);
|
|
}
|