Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is your plans for this - do you need help / hints etc #99

Open
vahidhedayati opened this issue May 26, 2021 · 3 comments
Open

What is your plans for this - do you need help / hints etc #99

vahidhedayati opened this issue May 26, 2021 · 3 comments

Comments

@vahidhedayati
Copy link

Hi Carl

I been going through this plugin and wondered what you planned to do with it, there is a lot about this I would change and there are things like home page component missing too ... which I guess is store front...

There is lots of stuff happening in controllers that I think could have a tidy up the product listing gives errors and that whole query I have condensed to about 70 lines locally using HQL it was very hard to follow.

Not really sure why the shoppingCart items are stored on a db I have managed to use session.cart to contain all the relevant items put in a given basket rather than physically storing on a db. Whilst I can see advantages such as a basket that can move between devices but since it was locked to session it be only valid or available so long as session.cart would be so in essence no different.

Also the product category and adding a product to all the sub categories of a given category again unsure if all that in a long run or over bigger project plans out too well...

Assume we have a new field in product category:

    String categoryPath = ""  // starting with nothing path of parent is build 10/ 10/1 10/1/1 and so on

 List<Long> getCurrentIdentifiers() {
        return categoryPath.split('/').collect{it as Long}
    }
    String getIdentifiersById() {
        return getCurrentIdentifiers().join('/')
    }
    List<ProductCategory> getCurrentIdentifiersByName() {
        return categoryPath.split('/').collect{ProductCategory.get(Long.valueOf(it))}
    }

    String getIdentifiersByName() {
        return categoryPath.split('/').collect{ProductCategory.get(Long.valueOf(it))?.description}?.join('/')
    }

    List getWalkThroughParentsById() {
        List start = [id]
        if (parent) {
            boolean reachedEnd
            Long currentId = parent.id
            while (reachedEnd == false) {
                def p = ProductCategory.get(currentId)
                if (!start.contains(p.id)) {
                    start << p.id
                }
                if (p.parent) {
                    currentId=p.parent.id
                    if (!start.contains(p.parent.description)) {
                        start << p.parent.id
                    }
                } else {
                    reachedEnd =true
                }
            }
        }
        return  start.reverse()
    }
    String getFlatWalkThroughParentsById() {
        def start = getWalkThroughParentsById()
        return start.join('/')
        //String finalOutput = start.join('/')
       // return  start?.size() > 1 ? finalOutput : finalOutput+"/"

    }
    List getWalkThroughParentsByName() {
        List start = [description]
        if (parent) {
            boolean reachedEnd
            Long currentId = parent.id
            while (reachedEnd == false) {
                def p = ProductCategory.get(currentId)
                if (!start.contains(p.description)) {
                    start << p.description
                }
                if (p?.parent?.id) {
                    currentId=p.parent.id
                    if (!start.contains(p.parent.description)) {
                        start << p.parent.description
                    }
                } else {
                    reachedEnd =true
                }
            }
        }
        return  start.reverse()
    }

    String getFlatWalkThroughParentsByName() {
        return getWalkThroughParentsByName()?.join('/')
    }

    /**
     * Used in gsps to load up categories on the page shop/index and shop/product as an example
     * @return
     */
    String getFlatHTMLWalkThroughParentsByName() {
        def start = getWalkThroughParentsByNameAndId()
        List finalList = []
        start?.each { k->
            finalList << """<a class="${this.id==k.id ? 'bold':''}" href="/shop/category/${k.id}">${k.description}</a>"""
        }
        return finalList?.join('/')
    }

    List getWalkThroughParentsByNameAndId() {
        List<Map> start = [[description:description, id:id]]
        if (parent) {
            boolean reachedEnd
            Long currentId = parent.id
            while (reachedEnd == false) {
                def p = ProductCategory.get(currentId)
                if (!start.find{it.description== p.description}) {
                    start <<  [description:p.description, id:p.id]
                }
                if (p?.parent?.id) {
                    currentId=p.parent.id
                    if (!start.find{it.description== p.description}) {
                        start <<  [description:p.parent.description, id:p.parent.id]
                    }
                } else {
                    reachedEnd =true
                }
            }
        }
        return  start.reverse()
    }

    String getFlatWalkThroughParentsByNameAndId() {
        def start = getWalkThroughParentsByNameAndId()
        return start.join('/')
       // String finalOutput = start.join('/')
        //return  start?.size() > 1 ? finalOutput : finalOutput+"/"
    }

In ProductCategoryController where you save productCategory if you then did:

     productCategory.save flush: true

        productCategory.categoryPath = productCategory.walkThroughParentsById
        productCategory.save flush: true

you end up setting a category path per category created and then rather than having all repeated product in so many categories you simply have a flat path and in your hql or query you can look up product and productCategory.categoryPath and then look for any items in products that matches same categorPath of let's say : 10/1/1* this now lists anything on from there and obviously you can go back to root by substring(0,item.indexOf('/')) etc and look for anything matching 10* and so on and so on..

Just some thoughts and I am still busy but perhaps when I finish what I am doing if you are looking to officially release this I can help you out or give you hints advice...

@cbmarcum
Copy link
Owner

Hi Vahid, thanks for your interest in the plugin!

There are definitely areas that could be improved.

It's not released (as in listed on the Grails website) but it is released to Bintray and Maven Central.
I have built client applications that use Bootstrap to theme the gsp's using different purchased themes and this plugin is used for all of the backend. I'm currently building them using the "Sell" theme available on the Bootstrap website which is why I don't have a public repo for it because of the license of the theme. I have a demo running you can see here [1].
My website codebuilders.net also uses the plugin although an older Grails 3 version.

If you look at the demo, the shop menu item shows a list of categories. Only the ones with "New" beside the category have products currently.

The product listing and search is currently designed for full text search using the hibernate-search plugin which uses a search index. This works great when it works but has caused a lot of headaches and dependency conflicts also come up. It often fails to startup the first time for some unknown reason. I would like to replace it with something and my thought was to add full-text search to my PostgreSQL database and use that special column for searching.

The complexity you see in the product search may be due to returning different results based on admin authentication. For instance if a user does a listing they will only see the primary product or a group and not all the color/size versions for instance where the admin will see all products even those after sale discontinuation date and things like that.

A lot of the domains were initially modeled from a book called The Data Model Resource Book. The Apache OfBiz project uses a much more complete implementation and I've only created what I needed so far. I plan to also include things like warehouse, inventory and things like that when time allows.

Currently the product category domain allows flexibility in changing hierarchies like adding one in the middle or adding a category like New Products or On Sale that might only be temporary. I'll have to review your code to get a better idea of your thoughts.

Unfortunately I haven't spent enough time on documentation but I'd be glad to work on that some or answer any questions.

I'm interested in the session shopping cart idea and would be interested to see how that would work. It was based on a Grails plugin at some point.

You can also email me direct at carl.marcum at codebuilders.net

[1] http://demo.codebuilders.net/

Best regards,
Carl

@vahidhedayati
Copy link
Author

vahidhedayati commented May 28, 2021

I can email you if you like, there is a lot about this that I wouldn't personally do like you say the search causes a lot of headache and perhaps a better db struture design to look up items based on the criteria using HQL would be how I would do it. This way it is going to work going forth without the headaches of revision updates "well one would hope writing own queries"

As far as your product Controller,

 def addToCart(Long id) {
        if (!session.cart) {
            session.cart = []
            session.cartCounter=[:]
        }
        List allOfThisItem = session?.cart?.findAll{it.id == id}
        Product p = Product.get(id)
        if ( p.inventoryCount - (allOfThisItem?.size()?:0)>=1 ) {
            session.cart << [id: id, name: p.name, listPrice: p.listPrice, inventoryCount: p.inventoryCount, shortDescription:p.shortDescription, photo:p?.mainPhoto ]
            session.cartCounter[id]=session?.cart?.findAll{it.id == id}?.size()
        }
        render status: 200, text: ''
    }

    def updateCart(Long id, int qty) {
        List allOfThisItem = session?.cart?.findAll{it.id == id}
        Map firstItem = [:]
        if (!allOfThisItem) {
            Product p = Product.get(id)
            allOfThisItem = [id: id, name: p.name, listPrice: p.listPrice, inventoryCount: p.inventoryCount, shortDescription:p.shortDescription, photo:p.mainPhoto ]
            firstItem = allOfThisItem[0]
        } else {
            firstItem = allOfThisItem[0]

            if (qty>firstItem.inventoryCount) {
                qty = firstItem.inventoryCount
            }

            if (qty > allOfThisItem.size() &&(firstItem.inventoryCount - (allOfThisItem?.size()?:0)>=1)) {
                int a = allOfThisItem.size()
                while (a <  qty) {
                    session.cart << firstItem
                    a++
                }
            } else if (qty < allOfThisItem.size()) {
                int a = allOfThisItem.size()
                while (qty < a) {
                    session?.cart?.remove(session.cart.find{it.id == id})
                    a--
                }
            }
        }
        session.cartCounter[id]=session?.cart?.findAll{it.id == id}?.size()
        render status: 200, text: "success"
    }

I removed add and removeFrom cart and have either a select or input box which when calling update simply does remove / add in the same function, if you prefer your older way but using session instead of DB. They are commented out and I haven't updated - what is working is above... unsure of below segment it was fine at some point...

//Older functions add etc 

def removeFromCart(Long id) {
        session.cart = session?.cart?.remove(session.cart.find{it.id == id})
        render status: 200, text: 'good'
    }

    def removeAllFromCart(Long id) {
        session?.cart?.removeAll {it.id == id}
        render status: 200, text: 'good'
    }

    def addToCart(Long id) {
        if (!session.hasProperty('cart')) {
            session.cart = []
            Product p = Product.get(id)
            session.cart << [id: id, name: p.name, price: p.listPrice, photo:p.photos[0], qty:1 ]
        } else {
            def found = session.cart?.find{p-> p.id == id}
            if (found) {
                found.qty ++
            }
        }
        render status: 200, text: 'success'
        return
    }
    def removeFromCart(Long id) {
        def found = session.cart.find{it.id == id}
        if (found) {
            found.qty --
        }
        render status: 200, text: 'success'
    }
    def removeAllFromCart(Long id) {
        session?.cart?.removeAll {it.id == id}
        render status: 200, text: 'success'
    }

Then I think in your home controller:

def index() {
        def notices = noticeService.getCurrentNoticesByPage("shop")
        //def feeds = rssFeedService.getFeedsByDisplay()
        if (!session?.cartCounter||!session?.cart) {
            session.cartCounter=[:]
            session.cart=[:]
        }

then in gsp:

   <g:set var="currentlyInCart" value="${(session?.cartCounter[product?.id]?:0)}"/>
                        <g:set var="availableStock" value="${((product.inventoryCount)-currentlyInCart)}"/>
  <div class="hover">
                            <g:if test="${availableStock>=1}">
                                <button onclick="ajaxCall('addToCart', ${product.id},${availableStock})" class="btn btn-primary btn-lg waves-effect"><i class="fa fa-plus"></i></button>
                            </g:if>
                            <button onclick="ajaxCall('checkout', ${product.id},${availableStock})" class="checkOut btn btn-primary btn-lg waves-effect"><i class="fa fa-shopping-cart"></i></button>
                        </div>
                        <div class="row text-center">
                            <g:message code="inCartInStock.label" args="[currentlyInCart,availableStock]"/>
                        </div>

<script>
    function ajaxCall(method,id,available) {
            if (available>=1) {
            var url="${createLink(controller:'shoppingCart', action: "addToCart")}/?id="+id
            $.ajax({
                type: 'POST',
                url: url,
                success: function(data){
                    if (method=='addToCart') {
                        location.reload();
                    } else {
                        window.location.href="${createLink(controller:'shoppingCart', action: "cart")}";
                        e.stopPropagation();
                        e.preventDefault();
                    }
                },
                error: function (xhr, textStatus, error) {
                    $("#failSegment").html("${g.message(code:'formError.label')}"+xhr.status).addClass('has-warning').delay(10).fadeIn('normal', function () {
                        $(this).delay(1500);
                    }).hide();
                }
            });
        } else {
            if (method=='addToCart') {
                location.reload();
            } else {
                window.location.href="${createLink(controller:'shoppingCart', action: "cart")}";
            }
        }
        }
    </script>

This should do all that your existing ShoppingCart would do and would save persisting a given cart to a db.

The concepts of such things and having lots of hasMany relationships is all easy and logical to follow but and the real but is headaches down the road when it comes to managing an ever growing DB.

Burt Beckwith did a nice video and in it he described how or why he created the UserRole class simply due to well it's a plugin assume an end user using it ends up with 10k of elements hanging off the hasMany. Imagine if you had 10k categories would your current select box for managing categories and child categories work well ? I think trying to manage that from a select box and a simple query to list each element of a 10K would cause some issues or hanging around..

You can create your own hasMany relations and make them work how you want it , short queries wrapped around getter methods etc. I think I would probably write a lot of this differently which is why if I get time I may release a different variation

To give some more ideas a product feature really hangs off the product category or its generic traits and it may have custom ones but most importantly a feature is likely to come at a cost, an example a shop selling cars, same car one has super jet engine feature which comes at £500 more for that feature if selected.

The problem is all the little things that hang off a product weight vs shipping costs etc vs shipping destination and it's costs - these are also very difficult issues to consider

@cbmarcum
Copy link
Owner

cbmarcum commented Jun 7, 2021

@vahidhedayati I have created issue #100 and session-cart branch if you are interested in doing a PR. Otherwise I'll work on it in a week or two as I'm in the middle of a project currently.
Anyway, thanks for the input!
As for the product features and costs, I would agree if it were custom selected features like special order type products but this has been designed for every variation has a product entry, sku, etc. and a the associated cost set already just like merchandise in a store.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants