• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atlp-rwanda / knights-ecomm-fe / 10060387859

23 Jul 2024 10:55AM UTC coverage: 90.757% (+0.09%) from 90.671%
10060387859

push

github

web-flow
Merge pull request #73 from atlp-rwanda/ft-order-management

#68 Implement vendor and admin order management feature

1079 of 1360 branches covered (79.34%)

Branch coverage included in aggregate %.

1519 of 1727 new or added lines in 21 files covered. (87.96%)

3 existing lines in 2 files now uncovered.

11332 of 12315 relevant lines covered (92.02%)

12.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.44
/src/pages/Orders/SingleVendorOrder.tsx
1
import React, { FormEvent, useEffect, useState } from 'react';
1✔
2
import HeaderOrderManagement from '../../components/Order/DashboardOrderManagement/HeaderOrderManagement';
1✔
3
import SideBarOrderStatus from '../../components/Order/DashboardOrderManagement/SideBarOrderStatus';
1✔
4
import { PropagateLoader, PulseLoader } from 'react-spinners';
1✔
5
import { useDispatch, useSelector } from 'react-redux';
1✔
6
import { AppDispatch, RootState } from '../../redux/store';
1✔
7
import axios, { AxiosError } from 'axios';
1✔
8
import { useNavigate, useParams } from 'react-router-dom';
1✔
9
import { Order } from '../../redux/reducers/vendorOrdersReducer';
1✔
10
import OrderNotFound from '../../components/Order/OrderNotFound';
1✔
11
import { useJwt } from 'react-jwt';
1✔
12
import { DecodedToken } from '../Authentication/Login';
1✔
13
import toast from 'react-hot-toast';
1✔
14
import { setOrderStats } from '../../redux/reducers/orderStatsReducer';
1✔
15
import BuyerInfo from '../../components/Order/DashboardOrderManagement/BuyerInfo';
1✔
16

1✔
17
const SingleVendorOrder = () => {
1✔
18
  const [activeStatus, setActiveStatus] = useState('');
19✔
19
  const [isEditable, setIsEditable] = useState(true);
19✔
20
  const [showMenu, setShowMenu] = useState(false);
19✔
21
  const [order, setOrder] = useState<Order>();
19✔
22
  const [loading, setLoading] = useState(true);
19✔
23
  const [submitFormLoading, setSubmitFormLoading] = useState(false);
19✔
24
  const [width, setWidth] = useState(window.innerWidth);
19✔
25
  const [notFound, setNotFound] = useState(false);
19✔
26

19✔
27
  const dispatch = useDispatch<AppDispatch>();
19✔
28
  const orderStatsStates = useSelector((state: RootState) => state.orderStats);
19✔
29

19✔
30
  const { userToken } = useSelector((state: RootState) => state.auth);
19✔
31
  const { decodedToken } = useJwt<DecodedToken>(userToken);
19✔
32

19✔
33
  const statuses = ['in-transit', 'cancelled', 'delivered'];
19✔
34

19✔
35
  const params = useParams();
19✔
36
  const navigate = useNavigate();
19✔
37

19✔
38
  useEffect(() => {
19✔
39
    const handleResize = () => {
5✔
40
      setWidth(window.innerWidth);
1✔
41
    };
1✔
42
    window.addEventListener('resize', handleResize);
5✔
43
    return () => {
5✔
44
      window.removeEventListener('resize', handleResize);
5✔
45
    };
5✔
46
  }, []);
19✔
47

19✔
48
  useEffect(() => {
19✔
49
    const fetchOrder = async () => {
5✔
50
      try {
5✔
51
        const { data } = await axios.get(
5✔
52
          `${import.meta.env.VITE_APP_API_URL}/product/vendor/orders/${params.orderId}`,
5✔
53
          {
5✔
54
            headers: {
5✔
55
              Authorization: `Bearer ${userToken}`
5✔
56
            }
5✔
57
          }
5✔
58
        );
5✔
59
        setOrder(data.data.order);
4✔
60
        setActiveStatus(data.data.order.orderStatus);
4✔
61
        if (
4✔
62
          ['completed', 'cancelled', 'pending', 'delivered', 'returned'].includes(
4✔
63
            data.data.order.orderStatus.toLowerCase()
4✔
64
          )
4✔
65
        ) {
5✔
66
          setIsEditable(false);
2✔
67
        }
2✔
68
        setLoading(false);
3✔
69
      } catch (error) {
5✔
70
        setLoading(false);
2✔
71
        if ((error as AxiosError).response && [400, 404].includes((error as AxiosError).response!.status)) {
2✔
72
          setNotFound(true);
2✔
73
        } else {
1✔
74
          navigate('orders');
2✔
75
        }
1✔
76
      }
1✔
77
    };
1✔
78

1✔
79
    fetchOrder();
2✔
80
    // eslint-disable-next-line react-hooks/exhaustive-deps
5✔
81
  }, [params.orderId, userToken]);
5✔
82

5✔
83
  const statusClickHandler = (status: string) => {
5✔
84
    setActiveStatus(status);
19✔
85
  };
19✔
86

19✔
NEW
87
  const submitHandler = async (e: FormEvent) => {
×
NEW
88
    e.preventDefault();
×
89
    setSubmitFormLoading(true);
19✔
90
    if (activeStatus.toLowerCase() === 'pending') {
19✔
91
      setSubmitFormLoading(false);
1✔
92
      return;
1✔
93
    }
1!
NEW
94
    try {
×
NEW
95
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
×
NEW
96
      const { data } = await axios.put(
×
97
        `${import.meta.env.VITE_APP_API_URL}/product/vendor/orders/${params.orderId}`,
1✔
98
        { orderStatus: activeStatus.toLowerCase() },
1✔
99
        {
1✔
100
          headers: {
1✔
101
            Authorization: `Bearer ${userToken}`,
1✔
102
            'Content-Type': 'application/json'
1✔
103
          }
1✔
104
        }
1✔
105
      );
1✔
106
      if (activeStatus.toLowerCase() !== 'in-transit') {
1✔
107
        if (activeStatus.toLowerCase() === 'cancelled') {
1✔
108
          dispatch(
1✔
109
            setOrderStats({
1!
NEW
110
              ...orderStatsStates,
×
NEW
111
              cancelled: orderStatsStates.cancelled + 1,
×
NEW
112
              pendingOrder: orderStatsStates.pendingOrder - 1
×
NEW
113
            })
×
NEW
114
          );
×
NEW
115
        } else {
×
NEW
116
          dispatch(setOrderStats({ ...orderStatsStates, pendingOrder: orderStatsStates.pendingOrder - 1 }));
×
NEW
117
        }
×
NEW
118
        setIsEditable(false);
×
NEW
119
      }
×
NEW
120
      toast.success('Your order has been successfully updated.');
×
NEW
121
      setSubmitFormLoading(false);
×
NEW
122
    } catch (error) {
×
NEW
123
      toast.error('Unable to update your order,\n Please try again!');
×
NEW
124
      setSubmitFormLoading(false);
×
125
    }
1✔
126
  };
1✔
127

1!
NEW
128
  return (
×
NEW
129
    <div
×
NEW
130
      onClick={() => setShowMenu(false)}
×
131
      className="flex flex-col justify-start gap-8 py-8 w-full min-h-[100vh] h-full px-4 lg:px-10 2xl:px-20 bg-[#EEF5FF]"
1✔
132
    >
19✔
133
      <HeaderOrderManagement
19✔
134
        orderTimeIdentifier={order?.createdAt ? new Date(order?.createdAt).getTime().toString() : ''}
19✔
135
      />
19✔
136
      <div className="flex flex-col xlg:flex-row min-h-0 gap-7 w-full">
19✔
137
        {!notFound && !loading && (
19✔
138
          <div className="flex flex-col gap-y-7 bg-white rounded-lg border border-neutral-300 basis-3/4 py-8 px-4">
19✔
139
            <div className="flex flex-wrap gap-y-3 gap-x-1 lg:gap-5 justify-between">
19✔
140
              <div className="flex flex-col gap-y-2 ">
19✔
141
                <div className="flex items-center gap-x-3">
19✔
142
                  <p className="font-semibold w-[6.6rem] xmd:w-[6.8rem] lg:w-32 text-sm xmd:text-[.9rem] lg:text-base text-nowrap">
19✔
143
                    Order Status
19✔
144
                  </p>
19✔
145
                  {isEditable && (
6✔
146
                    <form action="" className="flex gap-x-1 lg:gap-x-2" onSubmit={submitHandler}>
6✔
147
                      <div
6✔
148
                        onClick={(e: React.MouseEvent<HTMLDivElement>) => e.stopPropagation()}
6✔
149
                        className="relative  border-[1px] border-neutral-300 rounded-xl text-[.9rem] w-[5.5rem] xmd:w-[6.5rem]"
6✔
150
                      >
6✔
151
                        <div
6✔
152
                          onClick={() => setShowMenu((prevValue) => !prevValue)}
6✔
153
                          className={`${isEditable ? 'cursor-pointer' : ' cursor-not-allowed'} gap-x-1 pl-3 pr-2  py-[.3rem] rounded-xl flex items-center justify-between hover:bg-neutral-200 duration-300 active:bg-neutral-300`}
4✔
154
                          data-testid="statusElement"
4✔
155
                        >
4✔
156
                          <input
4✔
157
                            type="text"
4✔
158
                            title={activeStatus}
4✔
159
                            className="w-full cursor-pointer outline-none bg-transparent capitalize text-xs xmd:text-[.8rem] lg:text-[.86rem]"
4✔
160
                            value={activeStatus.length > 8 ? activeStatus.slice(0, 7).trim() + '..' : activeStatus}
4!
161
                            readOnly
4✔
162
                          />
4✔
163
                          <i className="fa-solid fa-chevron-down text-[.75rem] xmd:text-[.87rem]"></i>
4✔
164
                        </div>
4✔
165
                        {showMenu && (
4✔
166
                          <div className=" animate-slideInFromTop absolute top-8 left-[-2px] bg-baseWhite xmd:w-40 px-1 py-1 shadow-xl border-[1px] border-neutral-200 rounded-lg text-neutral-800 text-xs xmd:text-[.82rem] lg:text-[.92rem]">
4✔
167
                            <ul>
4✔
168
                              {statuses.map((status, index) => (
4✔
169
                                <li
4!
NEW
170
                                  key={index}
×
171
                                  onClick={() => {
4✔
172
                                    if (activeStatus.toLowerCase() !== status) {
4✔
173
                                      statusClickHandler(status);
4✔
174
                                      setShowMenu(false);
4✔
175
                                    }
4✔
176
                                  }}
4✔
177
                                  className={`${activeStatus.toLowerCase() !== status ? 'cursor-pointer hover:bg-neutral-100 active:bg-neutral-200' : ' cursor-not-allowed bg-neutral-200 text-neutral-500'} capitalize py-2 px-3 border-b-[1px]`}
1✔
178
                                >
1✔
179
                                  {status}
1✔
180
                                </li>
3✔
181
                              ))}
3✔
182
                            </ul>
3✔
NEW
183
                          </div>
×
NEW
184
                        )}
×
NEW
185
                      </div>
×
NEW
186
                      <button
×
NEW
187
                        type={submitFormLoading || activeStatus.toLowerCase() === 'is-accepted' ? 'button' : 'submit'}
×
188
                        className={`${activeStatus.toLowerCase() === 'is-accepted' ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-neutral-800'} flex items-center justify-center border-[1px] border-neutral-300 rounded-xl px-3 xmd:px-5 py-1 text-xs xmd:text-[.8rem] lg:text-[.9rem] bg-neutral-600 text-baseWhite font-medium w-20 lg:w-24`}
3✔
189
                      >
3✔
190
                        {!submitFormLoading && <span>UPDATE</span>}
3✔
191
                        {submitFormLoading && <PulseLoader color="#fff" size={6} />}
3✔
192
                      </button>
1✔
193
                    </form>
1✔
194
                  )}
1✔
195

4✔
196
                  {!isEditable && (
4✔
197
                    <div className="border-[1px] border-neutral-300 px-3 lg:px-4 py-1 rounded-xl">
4✔
198
                      <p className="capitalize text-xs xmd:text-[.8rem] lg:text-[.9rem]">{activeStatus}</p>
4✔
199
                    </div>
4✔
200
                  )}
1✔
201
                </div>
3✔
202
                <div className="flex items-center gap-x-4 xmd:gap-x-3">
4✔
203
                  <p className="font-semibold w-[6.6rem] xmd:w-[6.8rem] lg:w-32 text-sm xmd:text-[.9rem] lg:text-base text-nowrap">
4!
204
                    Order Address
4✔
205
                  </p>
4✔
206
                  <div className="border-[1px] border-neutral-300 rounded-xl px-3 lg:px-4 py-[.43rem]">
4✔
207
                    <p className="capitalize leading-3 text-xs xmd:text-[.8rem] lg:text-[.9rem] xmd:max-w-40 lg:max-w-none">
4✔
208
                      {order?.address.split(', ').join('-')}
4✔
209
                    </p>
6✔
210
                  </div>
6✔
211
                </div>
6✔
212
                <div className="flex items-center gap-x-3">
2✔
213
                  <p className="font-semibold w-[6.7rem] xmd:w-[6.8rem] lg:w-32 text-sm xmd:text-[.9rem] lg:text-base">
2✔
214
                    Placed Date
2✔
215
                  </p>
2✔
216
                  <div className="border-[1px] border-neutral-300 rounded-xl px-3 lg:px-4 py-1 text-[.9rem]">
2✔
217
                    <p className="text-xs xmd:text-[.8rem] lg:text-[.9rem]">
6✔
218
                      {order?.createdAt.toLocaleString().split('T')[0]}
6✔
219
                    </p>
6✔
220
                  </div>
6✔
221
                </div>
6✔
222
                <div className="flex items-center gap-x-3">
6✔
223
                  <p className="font-semibold w-[6.7rem] xmd:w-[6.8rem] lg:w-32 text-sm xmd:text-[.9rem] lg:text-base">
6✔
224
                    Total price
6✔
225
                  </p>
6✔
226
                  <div className="border-[1px] border-neutral-300 rounded-xl px-3 lg:px-4 py-1 text-[.9rem]">
6✔
227
                    <p className="text-xs xmd:text-[.8rem] lg:text-[.9rem]">RWF {order?.totalPrice}</p>
6✔
228
                  </div>
6✔
229
                </div>
6✔
230
              </div>
6✔
231
              <div className="min-w-[100%] sm:min-w-[40%]">
6✔
232
                <BuyerInfo buyerInfo={order?.buyer} />
6✔
233
              </div>
6✔
234
            </div>
6✔
235
            <table className="w-full">
6✔
236
              <thead className="text-[.75rem] xmd:text-[.82rem] lg:text-[.88rem]">
6✔
237
                <tr className=" bg-primary text-baseWhite font-normal h-11 xmd:h-12 text-left">
6✔
238
                  <th className="font-normal w-[12%] xmd:w-[15%] text-center xmd:text-left xmd:pl-6 lg:pl-10">
6✔
239
                    N<sup>o</sup>
6✔
240
                  </th>
6✔
241
                  <th className="font-normal w-[20%] xmd:w-[30%]">Product</th>
6✔
242
                  <th className="font-normal text-center xmd:text-left w-[15%] xmd:w-[25%]">
6✔
243
                    {width < 640 ? 'Qty/Stock' : 'Qty/Qty in Stock'}
6✔
244
                  </th>
6✔
245
                  <th className="font-normal w-[17%] xmd:w-[25%]">{width < 640 ? 'P/U' : 'Price/Unit'}</th>
6✔
246
                </tr>
6✔
247
              </thead>
6✔
248
              <tbody className="text-[.7rem] xmd:text-[.76rem] lg:text-[.82rem]">
6✔
249
                {order?.vendorOrderItems.map((item, index) => (
6✔
250
                  <tr key={index} className="h-9 xmd:h-12 border-b-[1px] border-neutral-300">
6✔
251
                    <td className="text-center xmd:text-left xmd:pl-6 lg:pl-10">{index + 1}.</td>
6✔
252
                    <td className="leading-3">{item.product.name}</td>
6✔
253
                    <td className="xmd:pl-7 text-center xmd:text-left">
6✔
254
                      {item.quantity + '/' + item.product.quantity}
6✔
255
                    </td>
6✔
256
                    <td className="leading-3">RWF {item['price/unit']}</td>
6✔
257
                  </tr>
6✔
258
                ))}
6✔
259
              </tbody>
6✔
260
            </table>
6✔
261
          </div>
6✔
262
        )}
6✔
263
        {notFound && (
6✔
264
          <div className="flex justify-center items-center bg-white rounded-lg border border-neutral-300 basis-3/4 py-8 px-4">
6✔
265
            <OrderNotFound link={'/' + decodedToken?.role.toLowerCase() + '/dashboard/orders'} />
6✔
266
          </div>
6✔
267
        )}
6✔
268
        {loading && (
6✔
269
          <div className="flex justify-center items-center bg-white rounded-lg border border-neutral-300 basis-3/4 py-8 px-4">
6✔
270
            <PropagateLoader className="ml-[-.5rem] py-14" />
6✔
271
          </div>
6✔
272
        )}
6✔
273
        <SideBarOrderStatus />
6✔
274
      </div>
6✔
275
    </div>
6✔
276
  );
6✔
277
};
6✔
278

6✔
279
export default SingleVendorOrder;
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc