Fluent Commerce Logo
Sign In

Enable location store users to update store hours

How-to Guide
Extend

Authors:

Randy Chan, Cille Schliebitz, Anita Gu

Changed on:

23 May 2025

Key Points

  • Each location has an opening hours setup that can be used for store activities. One activity could be during the fulfillment allocation phase, where the fulfillment expiry time for a store is calculated, giving store staff sufficient time to process the order before it expires.
  • Since opening hours can change week by week, we have created this guide to showcase how you can enable Store Managers to make these changes directly in the Fluent Store Web App.
Update store hours in the Web App

Steps

Step arrow right iconWhat to Expect

Since we will be using the

`updateLocation`
mutation to update
`location`
and subsequently  
`openingSchedule`
, we can write a custom UI Component to display the opening schedule in the UI, allowing users to update opening hours.

Here is a list of actions to implement this feature:

  • Determine if it is feasible via Web App and GraphQL.
  • Create a new role with the necessary permissions.
  • Assign the new role to the user.
  • Add the manifest reference link into the store setting
  • Create a new manifest setting to display the location details and openingSchedule.
  • Create an
    `OpeningSchedule`
    Component that maps to the new manifest.

Step arrow right iconStep 1. Determine if the mutation is feasible via Web App and GraphQL.

By looking at the GraphQL reference docs, we can see that the

`updateLocation`
mutation can update the
`openingSchedule`

No alt provided

Step arrow right iconStep 2. Create a new role with the necessary permissions

Next, we need to give the necessary permissions to the user;

`LOCATION_VIEW`
,
`LOCATION_UPDATE`
,
`OPENINGSCHEDULE_VIEW`
and
`OPENINGSCHEDULE_UPDATE`
. To do this, create a new Role:

1POST {{fluentApiHost}}/graphql
2
3
4QUERY:
5mutation createStoreRole {
6    createRole (
7        input: {
8            name: "STORE_LOCATION_HOUR",
9            permissions: [
10                { name: "LOCATION_VIEW" },
11                { name: "LOCATION_UPDATE" },
12                { name: "OPENINGSCHEDULE_VIEW" },
13                { name: "OPENINGSCHEDULE_UPDATE" }
14            ]
15        }
16    ) {
17        id
18        name
19    }
20}

Language: json

Name: mutation createRole

Description:

[Warning: empty required content area]

Step arrow right iconStep 3. Assign the new role to the user

Once the new role is created, assign the new role to the store manager by using

`updateUser`
mutation:

1POST: {{fluentApiHost}}/graphql
2 
3GraphQL Variables:
4{
5  "input": {
6    "id": {{userId}},
7    "ref": "{{userRef}}",
8    "roles": [
9    {
10        "contexts": [
11            {
12                "contextId": "{{retailerID}",
13                "contextType": "RETAILER"
14            },
15                        {
16                "contextId": "{{LocationID}}",
17                "contextType": "AGENT"
18            }
19        ],
20        "role": {
21            "name": "STORE_LOCATION_HOUR"
22        }
23    }
24    ]
25  }
26}
27
28Query:
29mutation updateUser ($input: UpdateUserInput) {
30    updateUser (input: $input) {
31        id
32        ref
33        username
34        title
35        firstName
36        lastName
37        primaryEmail
38        primaryPhone
39        type
40        status
41        department
42        country
43        timezone
44        promotionOptIn
45        createdOn
46        updatedOn
47        roles{
48            contexts{
49                contextId
50                contextType
51            }
52            role{
53                name
54            }
55        }
56    }
57}
58

Language: json

Name: updateUser with a new role

Description:

[Warning: empty required content area]

Step arrow right iconStep 4. Add the manifest reference link into the store setting

Add the manifest reference link that you are about to create in the store setting:

`fc.mystique.manifest.store`

1{
2    "type": "reference",
3    "settingName": "fc.mystique.manifest.store.fragment.storelocation"
4},

Language: json

Name: Add manifest reference

Description:

reference snippet to

`fc.mystique.manifest.store`


Step arrow right iconStep 5. Create a new manifest setting to display the location details and openingSchedule

Create a new manifest setting which going to display the location details and

`openingSchedule`
:

  • Name: fc.mystique.manifest.store.fragment.storelocation
  • Context: ACCOUNT
  • Context ID: 0
  • Value Type: JSON
  • JSON Value: 
1{
2    "manifestVersion": "2.0",
3    "routes": [
4        {
5            "roles": [
6                "STORE_LOCATION_HOUR"
7            ],
8            "path": "storelocation",
9            "type": "page",
10            "component": "fc.page",
11            "nav": {
12                "label": "Location Details",
13                "icon": "person"
14            },
15            "data": {
16                "query": "query locations($id: ID!, $contextId: [Int!], $networks_first: Int, $storageAreas_first: Int) {\n  locationById(id: $id) {\n    id\n    name\n    ref\n    type\n    status\n    supportPhoneNumber\n  attributes{name value}   primaryAddress {\n      id\n      latitude\n      longitude\n      state\n      city\n      street\n      postcode\n      country\n      timeZone\n    }\n    openingSchedule {\n      allHours\n      monEnd\n      monStart\n      tueEnd\n      tueStart\n      wedEnd\n      wedStart\n      thuEnd\n      thuStart\n      friEnd\n      friStart\n      satEnd\n      satStart\n      sunEnd\n      sunStart\n    }\n    networks(first: $networks_first) {\n      edges {\n        node {\n          id\n          ref\n          type\n          status\n        }\n      }\n    }\n    storageAreas(first: $storageAreas_first) {\n      edges {\n        node {\n          id\n          name\n          status\n          type\n        }\n      }\n    }\n  }\n  settings(\n    name: \"PICK_N_PACK_TIME_LIMIT\"\n    context: \"AGENT\"\n    contextId: $contextId\n  ) {\n    edges {\n      node {\n        id\n        name\n        valueType\n        value\n        contextId\n      }\n    }\n  }\n}",
17                "variables": {
18                    "id": "{{activeLocation.id}}",
19                    "contextId": "{{activeLocation.id}}",
20                    "storageAreas_first": 100,
21                    "networks_first": 100
22                }
23            },
24            "props": {
25                "title": "Locations {{locationById.ref}}",
26                "actions": {
27                    "primary": [
28                        {
29                            "roles": [
30                                "STORE_LOCATION_HOUR"
31                            ],
32                            "type": "mutation",
33                            "label": "Update Location Hours",
34                            "name": "updateLocation",
35                            "args": {
36                                "id": "{{locationById.id}}"
37                            },
38                            "overrides": {
39                                "openingSchedule": {
40                                    "component": "OpeningSchedule"
41                                }
42                            },
43                            "filter": {
44                                "type": "exclude",
45                                "names": [
46                                    "type",
47                                    "status",
48                                    "defaultCarrier",
49                                    "name",
50                                    "networks",
51                                    "primaryAddress",
52                                    "retailer",
53                                    "storageAreas",
54                                    "supportPhoneNumber",
55                                    "attributes"
56                                ]
57                            }
58                        },
59                        {
60                            "type": "userAction",
61                            "name": "CreateWaveByUserSelection",
62                            "condition": "{{and false}}"
63                        },
64                        {
65                            "type": "userAction",
66                            "name": "CreateWaveByExpiry",
67                            "condition": "{{and false}}"
68                        }
69                    ]
70                }
71            },
72            "descendants": [
73                {
74                    "component": "fc.card.attribute",
75                    "props": {
76                        "title": "i18n:fc.stores.allLocations.detail.card.details.title",
77                        "width": "half",
78                        "dataSource": "locationById",
79                        "attributes": [
80                            {
81                                "label": "i18n:fc.stores.allLocations.detail.card.details.attribute.locationRef.label",
82                                "template": "{{ref}}"
83                            },
84                            {
85                                "label": "i18n:fc.stores.allLocations.detail.card.details.attribute.name.label",
86                                "template": "{{name}}"
87                            },
88                            {
89                                "label": "i18n:fc.stores.allLocations.detail.card.details.attribute.phone.label",
90                                "template": "{{supportPhoneNumber}}"
91                            },
92                            {
93                                "label": "i18n:fc.stores.allLocations.detail.card.details.attribute.address.label",
94                                "template": "{{primaryAddress.street}}"
95                            },
96                            {
97                                "label": "",
98                                "template": "{{primaryAddress.city}}"
99                            },
100                            {
101                                "label": "",
102                                "template": "{{primaryAddress.state}}"
103                            },
104                            {
105                                "label": "",
106                                "template": "{{primaryAddress.postcode}}"
107                            },
108                            {
109                                "label": "",
110                                "template": "{{primaryAddress.country}}"
111                            },
112                            {
113                                "label": "i18n:fc.stores.allLocations.detail.card.details.attribute.latitude.label",
114                                "template": "{{primaryAddress.latitude}}"
115                            },
116                            {
117                                "label": "i18n:fc.stores.allLocations.detail.card.details.attribute.longitude.label",
118                                "template": "{{primaryAddress.longitude}}"
119                            },
120                            {
121                                "label": "i18n:fc.stores.allLocations.detail.card.details.attribute.timezone.label",
122                                "template": "{{primaryAddress.timeZone}}"
123                            }
124                        ]
125                    }
126                },
127                {
128                    "component": "fc.card.attribute",
129                    "props": {
130                        "title": "i18n:fc.stores.allLocations.detail.card.openingHours.title",
131                        "width": "half",
132                        "dataSource": "locationById",
133                        "attributes": [
134                            {
135                                "label": "i18n:fc.stores.allLocations.detail.card.openingHours.attribute.open24Hours?.label",
136                                "template": "{{openingSchedule.allHours}}"
137                            },
138                            {
139                                "label": "i18n:fc.stores.allLocations.detail.card.openingHours.attribute.monday.label",
140                                "template": "{{openingSchedule.monStart}}  to {{openingSchedule.monEnd}}"
141                            },
142                            {
143                                "label": "i18n:fc.stores.allLocations.detail.card.openingHours.attribute.tuesday.label",
144                                "template": "{{openingSchedule.tueStart}}  to {{openingSchedule.tueEnd}}"
145                            },
146                            {
147                                "label": "i18n:fc.stores.allLocations.detail.card.openingHours.attribute.wednesday.label",
148                                "template": "{{openingSchedule.wedStart}}  to {{openingSchedule.wedEnd}}"
149                            },
150                            {
151                                "label": "i18n:fc.stores.allLocations.detail.card.openingHours.attribute.thursday.label",
152                                "template": "{{openingSchedule.thuStart}}  to {{openingSchedule.thuEnd}}"
153                            },
154                            {
155                                "label": "i18n:fc.stores.allLocations.detail.card.openingHours.attribute.friday.label",
156                                "template": "{{openingSchedule.friStart}}  to {{openingSchedule.friEnd}}"
157                            },
158                            {
159                                "label": "i18n:fc.stores.allLocations.detail.card.openingHours.attribute.saturday.label",
160                                "template": "{{openingSchedule.satStart}}  to {{openingSchedule.satEnd}}"
161                            },
162                            {
163                                "label": "i18n:fc.stores.allLocations.detail.card.openingHours.attribute.sunday.label",
164                                "template": "{{openingSchedule.sunStart}}  to {{openingSchedule.sunEnd}}"
165                            }
166                        ]
167                    }
168                }
169            ]
170        }
171    ]
172}

Language: json

Name: fc.mystique.manifest.store.fragment.storelocation

Description:

Note:          

`"roles": [`

`                "STORE_LOCATION_HOUR"`

`            ],`

In the route section, you should define which roles will be able to see this page. Also, in the primary actions section, include an override which will call the openingSchedule component. (We will create this in the next step.)

If you open the Fluent Store Web App and navigate to the location detail screen:

No alt provided

The default update location mutation does provide a high-level UI for updating the opening schedule. The next step will demonstrate how to fine tune the visibility of the field.

No alt provided

Step arrow right iconStep 6: Create the openingSchedule component

Open the Component SDK and create a new file:

`OpeningSchedule.tsx`
:

1import { FC } from 'react';
2import { FieldRegistry, FormFieldProps } from 'mystique/registry/FieldRegistry';
3import { useI18n } from 'mystique/hooks/useI18n';
4import { useEffect, useState } from 'react';
5import { FormControl } from '@material-ui/core';
6
7export interface OpeningSchedule {
8  id: string | number;
9  allHours?: boolean;
10  monStart: number;
11  monEnd: number;
12  tueStart: number;
13  tueEnd: number;
14  wedStart: number;
15  wedEnd: number;
16  thuStart: number;
17  thuEnd: number;
18  friStart: number;
19  friEnd: number;
20  satStart: number;
21  satEnd: number;
22  sunStart: number;
23  sunEnd: number;
24}
25
26interface LocationSchedulePayload {
27  openingSchedule: OpeningSchedule | null;
28}
29
30export const OpeningSchedule: FC<FormFieldProps<LocationSchedulePayload>> = (
31  props,
32  entityContext
33) => {
34  const [openingSchedule] = useState<OpeningSchedule>(props.value);
35  const [allHours, setallHours] = useState<OpeningSchedule['allHours']>(
36    props.value.allHours
37  );
38
39  const [monStart, setmonStart] = useState<OpeningSchedule['monStart']>(
40    props.value.monStart
41  );
42  const [monEnd, setmonEnd] = useState<OpeningSchedule['monEnd']>(
43    props.value.monEnd
44  );
45  const [tueStart, settueStart] = useState<OpeningSchedule['tueStart']>(
46    props.value.tueStart
47  );
48  const [tueEnd, settueEnd] = useState<OpeningSchedule['tueEnd']>(
49    props.value.tueEnd
50  );
51  const [wedStart, setwedStart] = useState<OpeningSchedule['wedStart']>(
52    props.value.wedStart
53  );
54  const [wedEnd, setwedEnd] = useState<OpeningSchedule['wedEnd']>(
55    props.value.wedEnd
56  );
57  const [thuStart, setthuStart] = useState<OpeningSchedule['thuStart']>(
58    props.value.thuStart
59  );
60  const [thuEnd, setthuEnd] = useState<OpeningSchedule['thuEnd']>(
61    props.value.thuEnd
62  );
63  const [friStart, setfriStart] = useState<OpeningSchedule['friStart']>(
64    props.value.friStart
65  );
66  const [friEnd, setfriEnd] = useState<OpeningSchedule['friEnd']>(
67    props.value.friEnd
68  );
69  const [satStart, setsatStart] = useState<OpeningSchedule['satStart']>(
70    props.value.satStart
71  );
72  const [satEnd, setsatEnd] = useState<OpeningSchedule['satEnd']>(
73    props.value.satEnd
74  );
75  const [sunStart, setsunStart] = useState<OpeningSchedule['sunStart']>(
76    props.value.sunStart
77  );
78  const [sunEnd, setsunEnd] = useState<OpeningSchedule['sunEnd']>(
79    props.value.sunEnd
80  );
81
82  const QuantitySelector = FieldRegistry.get<number>('number');
83  const BooleanSelector = FieldRegistry.get<boolean>('boolean');
84
85  const { translate, translateOr } = useI18n();
86
87  useEffect(() => {
88    openingSchedule.allHours = allHours;
89    openingSchedule.monStart = monStart;
90    openingSchedule.monEnd = monEnd;
91    openingSchedule.tueStart = tueStart;
92    openingSchedule.tueEnd = tueEnd;
93    openingSchedule.wedStart = wedStart;
94    openingSchedule.wedEnd = wedEnd;
95    openingSchedule.thuStart = thuStart;
96    openingSchedule.thuEnd = thuEnd;
97    openingSchedule.friStart = friStart;
98    openingSchedule.friEnd = friEnd;
99    openingSchedule.satStart = satStart;
100    openingSchedule.satEnd = satEnd;
101    openingSchedule.sunStart = sunStart;
102    openingSchedule.sunEnd = sunEnd;
103    props.onChange(openingSchedule);
104  }, [
105    allHours,
106    monStart,
107    monEnd,
108    tueStart,
109    tueEnd,
110    wedStart,
111    wedEnd,
112    thuStart,
113    thuEnd,
114    friStart,
115    friEnd,
116    satStart,
117    satEnd,
118    sunStart,
119    sunEnd,
120  ]);
121
122  return (
123    <div>
124      <div>
125        <BooleanSelector
126          name='allHours'
127          value={props.value.allHours}
128          label={translateOr([
129            'fc.openingSchedule.allHours.subLabel',
130            'all Hours',
131          ])}
132          onChange={(v) => setallHours(v)}
133        />
134      </div>
135      <FormControl component='fieldset' style={{ width: '100%' }}>
136        <div>
137          <QuantitySelector
138            name='monStart'
139            value={props.value.monStart}
140            label={translateOr([
141              'fc.openingSchedule.monStart.subLabel',
142              'Monday Start',
143            ])}
144            onChange={(v) => setmonStart(v)}
145          />
146        </div>
147      </FormControl>
148      <div>
149        <QuantitySelector
150          name='monEnd'
151          value={props.value.monEnd}
152          label={translateOr([
153            'fc.openingSchedule.monEnd.subLabel',
154            'Monday End',
155          ])}
156          onChange={(v) => setmonEnd(v)}
157        />
158      </div>
159      <div>
160        <QuantitySelector
161          name='tueStart'
162          value={props.value.tueStart}
163          label={translateOr([
164            'fc.openingSchedule.tueStart.subLabel',
165            'Tuesday Start',
166          ])}
167          onChange={(v) => settueStart(v)}
168        />
169      </div>
170      <div>
171        <QuantitySelector
172          name='tueEnd'
173          value={props.value.tueEnd}
174          label={translateOr([
175            'fc.openingSchedule.tueEnd.subLabel',
176            'Tuesday End',
177          ])}
178          onChange={(v) => settueEnd(v)}
179        />
180      </div>
181      <div>
182        <QuantitySelector
183          name='wedStart'
184          value={props.value.wedStart}
185          label={translateOr([
186            'fc.openingSchedule.wedStart.subLabel',
187            'Wednesday Start',
188          ])}
189          onChange={(v) => setwedStart(v)}
190        />
191      </div>
192      <div>
193        <QuantitySelector
194          name='wedEnd'
195          value={props.value.wedEnd}
196          label={translateOr([
197            'fc.openingSchedule.wedEnd.subLabel',
198            'Wesnesday End',
199          ])}
200          onChange={(v) => setwedEnd(v)}
201        />
202      </div>
203      <div>
204        <QuantitySelector
205          name='thuStart'
206          value={props.value.thuStart}
207          label={translateOr([
208            'fc.openingSchedule.thuStart.subLabel',
209            'Thursday Start',
210          ])}
211          onChange={(v) => setthuStart(v)}
212        />
213      </div>
214      <div>
215        <QuantitySelector
216          name='thuEnd'
217          value={props.value.thuEnd}
218          label={translateOr([
219            'fc.openingSchedule.thuEnd.subLabel',
220            'Thursday End',
221          ])}
222          onChange={(v) => setthuEnd(v)}
223        />
224      </div>
225      <div>
226        <QuantitySelector
227          name='friStart'
228          value={props.value.friStart}
229          label={translateOr([
230            'fc.openingSchedule.friStart.subLabel',
231            'Friday Start',
232          ])}
233          onChange={(v) => setfriStart(v)}
234        />
235      </div>
236      <div>
237        <QuantitySelector
238          name='friEnd'
239          value={props.value.friEnd}
240          label={translateOr([
241            'fc.openingSchedule.friEnd.subLabel',
242            'Friday End',
243          ])}
244          onChange={(v) => setfriEnd(v)}
245        />
246      </div>
247      <div>
248        <QuantitySelector
249          name='satStart'
250          value={props.value.satStart}
251          label={translateOr([
252            'fc.openingSchedule.satStart.subLabel',
253            'Saturday Start',
254          ])}
255          onChange={(v) => setsatStart(v)}
256        />
257      </div>
258      <div>
259        <QuantitySelector
260          name='satEnd'
261          value={props.value.satEnd}
262          label={translateOr([
263            'fc.openingSchedule.satEnd.subLabel',
264            'Saturday End',
265          ])}
266          onChange={(v) => setsatEnd(v)}
267        />
268      </div>
269      <div>
270        <QuantitySelector
271          name='sunStart'
272          value={props.value.sunStart}
273          label={translateOr([
274            'fc.openingSchedule.sunStart.subLabel',
275            'Sunday Start',
276          ])}
277          onChange={(v) => setsunStart(v)}
278        />
279      </div>
280      <div>
281        <QuantitySelector
282          name='sunEnd'
283          value={props.value.sunEnd}
284          label={translateOr([
285            'fc.openingSchedule.sunEnd.subLabel',
286            'Sunday End',
287          ])}
288          onChange={(v) => setsunEnd(v)}
289        />
290      </div>
291    </div>
292  );
293};
294

Language: tsx

Name: OpeningSchedule.tsx

Description:

[Warning: empty required content area]

Add the following code snippet to

`index.tsx`
:

1import { OpeningSchedule } from './fields/location/OpeningSchedule';
2
3FieldRegistry.register(['OpeningSchedule'], OpeningSchedule);

Language: tsx

Name: Register new field in index.tsx

Description:

[Warning: empty required content area]

Once everything is saved, use the

`yarn start`
command and refresh the Fluent Store browser. The new component should now be displayed:

No alt provided

Now, the store user can update the location opening schedule hours via UI.

Randy Chan

Randy Chan

Contributors:
Cille Schliebitz
Anita Gu