Enable location store users to update store hours
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.

Steps
What to Expect
Since we will be using the
`updateLocation`
`location`
`openingSchedule`
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 Component that maps to the new manifest.
`OpeningSchedule`
Step 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`
`openingSchedule`

Step 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`
`OPENINGSCHEDULE_UPDATE`
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 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`
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 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 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:

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.

Step 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`

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