@@ -8,18 +8,24 @@ import type { CamelCasedPropertiesDeep } from 'type-fest';
88import { productTypes } from ' ~~/db/schema' ;
99import type { Tables } from ' ~~/types/database.types' ;
1010import { useBrands } from ' ~/queries/brands' ;
11+ import { useTags } from ' ~/queries/tags' ;
1112
1213const supabase = useSupabaseClient ();
1314const toast = useToast ();
1415const statusToaster = new StatusToaster (' Bulk Products' );
1516
1617const { brands } = useBrands ();
18+ const { tags } = useTags ();
1719
1820const { products } = defineProps <{
1921 products: PendingProduct [];
2022}>();
2123const emit = defineEmits ([' clearAll' , ' refetchAll' , ' selectAll' ]);
2224
25+ const availableTagsByCategory: Ref <
26+ ReturnType <typeof getTagsForProductTypeByCategory >
27+ > = ref ([]);
28+
2329const selectedProducts = computed (() => {
2430 return products .filter ((p ) => p != null && p .selected );
2531});
@@ -54,8 +60,22 @@ const productType: Ref<productTypeType> = ref('[null]');
5460const productTypeNotes = ref (' ' );
5561const productTypeOptions = ref ([' [null]' , ... productTypes ]);
5662
63+ const tagsToAdd: Ref <string []> = ref ([]);
64+ const tagsToRemove: Ref <string []> = ref ([]);
65+ const addTagToBeChanged = (tagId : string , selected : boolean | string ) => {
66+ // Selected is reversed because it is the value of the checkbox before the
67+ // change.
68+ if (selected === false || selected === ' indeterminate' ) {
69+ tagsToAdd .value = _ .uniq ([... tagsToAdd .value , tagId ]);
70+ tagsToRemove .value = tagsToRemove .value .filter ((t ) => t !== tagId );
71+ } else if (selected === true ) {
72+ tagsToRemove .value = _ .uniq ([... tagsToRemove .value , tagId ]);
73+ tagsToAdd .value = tagsToAdd .value .filter ((t ) => t !== tagId );
74+ }
75+ };
76+
5777watch (
58- [() => products , () => brands ],
78+ [() => products , () => brands , () => tags ],
5979 () => {
6080 // Brand
6181 if (brands .value .data ) {
@@ -82,6 +102,35 @@ watch(
82102 productType .value = ' ' ;
83103 productTypeNotes .value = ` ${currentTypes .length } current types ` ;
84104 }
105+
106+ // Tags
107+ if (tags ?.value ?.data && currentTypes .length === 1 ) {
108+ availableTagsByCategory .value = getTagsForProductTypeByCategory (
109+ tags .value .data ?? [],
110+ currentTypes [0 ] ?? null ,
111+ );
112+ for (const tagCat of availableTagsByCategory .value ) {
113+ if (tagCat .tags ) {
114+ for (const tag of tagCat .tags ) {
115+ const checkedCount = selectedProducts .value .reduce ((acc , p ) => {
116+ if (p .tags .map ((t ) => t .id ).includes (tag .id )) {
117+ return acc + 1 ;
118+ }
119+ return acc ;
120+ }, 0 );
121+
122+ if (checkedCount === selectedProducts .value .length ) {
123+ tag .selected = true ;
124+ } else if (checkedCount === 0 ) {
125+ tag .selected = false ;
126+ } else {
127+ tag .selected = ' indeterminate' ;
128+ }
129+ tag .selectedCount = checkedCount ;
130+ }
131+ }
132+ }
133+ }
85134 },
86135 { immediate: true , deep: true },
87136);
@@ -145,6 +194,67 @@ const { mutate: updateProductType } = useMutation({
145194 },
146195 onSettled : async () => emit (' refetchAll' ),
147196});
197+
198+ const { mutate : updateTags } = useMutation ({
199+ mutation : async () => {
200+ sending .value = true ;
201+
202+ let toAddErrored = false ;
203+ let toRemoveErrored = false ;
204+ if (tagsToAdd .value .length > 0 ) {
205+ const { error } = await supabase .from (' product_tags' ).insert (
206+ tagsToAdd .value
207+ .filter ((t ) => t != null )
208+ .map ((tag ) =>
209+ selectedProducts .value .map ((sp ) =>
210+ objectToSnake ({
211+ productId: sp .id ,
212+ tagId: tag ,
213+ }),
214+ ),
215+ )
216+ .flat (),
217+ );
218+
219+ if (error ) {
220+ statusToaster .error (' Update Failed!' , error .message );
221+ console .log (error );
222+ toAddErrored = true ;
223+ } else {
224+ statusToaster .success (' Updated!' );
225+ }
226+ }
227+
228+ if (! toAddErrored && tagsToRemove .value .length > 0 ) {
229+ for (const toRemove of tagsToRemove .value ) {
230+ const { error } = await supabase
231+ .from (' product_tags' )
232+ .delete ()
233+ .eq (' tag_id' , toRemove )
234+ .in (
235+ ' product_id' ,
236+ selectedProducts .value .map ((sp ) => sp .id ),
237+ );
238+
239+ if (error ) {
240+ statusToaster .error (' Update Failed!' , error .message );
241+ console .log (error );
242+ toRemoveErrored = true ;
243+ break ;
244+ }
245+ }
246+
247+ if (! toAddErrored && ! toRemoveErrored ) {
248+ tagsToAdd .value = [];
249+ tagsToRemove .value = [];
250+ statusToaster .success (' Updated!' );
251+ }
252+ }
253+
254+ sending .value = false ;
255+ },
256+ onSettled : async () => emit (' refetchAll' ),
257+ });
148258 </script >
149259<template >
150260 <div >
@@ -201,6 +311,75 @@ const { mutate: updateProductType } = useMutation({
201311 :products =" products"
202312 @refetch-all =" emit('refetchAll')"
203313 />
314+ <div >
315+ <div class =" flex flex-wrap" >
316+ <div
317+ v-for =" tagCat of availableTagsByCategory"
318+ :key =" tagCat.categoryName"
319+ class =" mx-1 flex flex-col flex-wrap"
320+ >
321+ {{ tagCat.categoryName }}
322+ <div v-for =" tag of tagCat.tags" :key =" tag.id" >
323+ <UCheckbox
324+ v-model =" tag.selected"
325+ color =" primary"
326+ variant =" card"
327+ :disabled =" sending"
328+ class =" py-1"
329+ @click =" addTagToBeChanged(tag.id, tag.selected)"
330+ >
331+ <template #label >
332+ {{ tag.name }}
333+ <UBadge
334+ variant =" outline"
335+ :color ="
336+ tag.selectedCount === 0
337+ ? 'neutral'
338+ : tag.selectedCount === selectedProducts.length
339+ ? 'success'
340+ : 'info'
341+ "
342+ class =" ml-2"
343+ >{{ tag.selectedCount }}/{{
344+ selectedProducts.length
345+ }}</UBadge
346+ >
347+ <UBadge
348+ v-if =" tagsToAdd.includes(tag.id)"
349+ color =" success"
350+ class =" ml-2"
351+ ><UIcon name =" i-lucide-plus"
352+ /></UBadge >
353+ <UBadge
354+ v-if =" tagsToRemove.includes(tag.id)"
355+ color =" error"
356+ class =" ml-2"
357+ ><UIcon name =" i-lucide-minus"
358+ /></UBadge >
359+ </template >
360+ </UCheckbox >
361+ </div >
362+ </div >
363+ </div >
364+ <UButton
365+ type =" button"
366+ color =" info"
367+ class =" mt-2 h-fit self-end"
368+ @click =" updateTags()"
369+ >Update {{ tagsToAdd.length + tagsToRemove.length }} tags</UButton
370+ >
371+ <UButton
372+ type =" button"
373+ color =" info"
374+ variant =" outline"
375+ class =" mx-2 mt-2 h-fit self-end"
376+ @click ="
377+ tagsToAdd = [];
378+ tagsToRemove = [];
379+ "
380+ >Clear Tag Changes</UButton
381+ >
382+ </div >
204383 </div >
205384 </div >
206385</template >
0 commit comments