• 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

83.96
/src/pages/Orders/SingleAdminOrder.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 OrderNotFound from '../../components/Order/OrderNotFound';
1✔
10
import { useJwt } from 'react-jwt';
1✔
11
import { DecodedToken } from '../Authentication/Login';
1✔
12
import toast from 'react-hot-toast';
1✔
13
import { setOrderStats } from '../../redux/reducers/orderStatsReducer';
1✔
14
import { Order } from '../../redux/reducers/adminOrdersReducer';
1✔
15
import DashboardOrderStatus from '../../components/Order/DashboardOrderManagement/DashboardOrderStatus';
1✔
16
import BuyerInfo from '../../components/Order/DashboardOrderManagement/BuyerInfo';
1✔
17

1✔
18
export interface ErrorResponse {
1✔
19
  message: string;
1✔
20
}
1✔
21

1✔
22
const SingleAdminOrder = () => {
1✔
23
  const [activeStatus, setActiveStatus] = useState('');
18✔
24
  const [isEditable, setIsEditable] = useState(true);
18✔
25
  const [showMenu, setShowMenu] = useState(false);
18✔
26
  const [order, setOrder] = useState<Order>();
18✔
27
  const [loading, setLoading] = useState(true);
18✔
28
  const [submitFormLoading, setSubmitFormLoading] = useState(false);
18✔
29
  const [width, setWidth] = useState(window.innerWidth);
18✔
30
  const [notFound, setNotFound] = useState(false);
18✔
31
  let orderItemsIndex = 0;
18✔
32

18✔
33
  const dispatch = useDispatch<AppDispatch>();
18✔
34
  const orderStatsStates = useSelector((state: RootState) => state.orderStats);
18✔
35

18✔
36
  const { userToken } = useSelector((state: RootState) => state.auth);
18✔
37
  const { decodedToken } = useJwt<DecodedToken>(userToken);
18✔
38

18✔
39
  const params = useParams();
18✔
40
  const navigate = useNavigate();
18✔
41

18✔
42
  useEffect(() => {
18✔
43
    const handleResize = () => {
5✔
44
      setWidth(window.innerWidth);
1✔
45
    };
1✔
46
    window.addEventListener('resize', handleResize);
5✔
47
    return () => {
5✔
48
      window.removeEventListener('resize', handleResize);
5✔
49
    };
5✔
50
  }, []);
18✔
51

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

1✔
76
    fetchOrder();
1✔
77
    // eslint-disable-next-line react-hooks/exhaustive-deps
1✔
78
  }, [params.orderId, userToken]);
1✔
79

2✔
80
  const submitHandler = async (e: FormEvent) => {
5✔
81
    e.preventDefault();
5✔
82
    setSubmitFormLoading(true);
5✔
83
    try {
5✔
84
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
18✔
85
      const { data } = await axios.put(
18✔
86
        `${import.meta.env.VITE_APP_API_URL}/product/admin/orders/${params.orderId}`,
18✔
NEW
87
        {},
×
NEW
88
        {
×
NEW
89
          headers: {
×
NEW
90
            Authorization: `Bearer ${userToken}`,
×
NEW
91
            'Content-Type': 'application/json'
×
NEW
92
          }
×
NEW
93
        }
×
NEW
94
      );
×
NEW
95

×
NEW
96
      dispatch(
×
NEW
97
        setOrderStats({
×
NEW
98
          ...orderStatsStates,
×
NEW
99
          pendingOrder: orderStatsStates.pendingOrder + 1,
×
NEW
100
          completed: orderStatsStates.completed + 1
×
NEW
101
        })
×
NEW
102
      );
×
NEW
103

×
NEW
104
      toast.success('The order has been successfully updated.');
×
NEW
105
      setSubmitFormLoading(false);
×
NEW
106
      setIsEditable(false);
×
NEW
107
    } catch (error) {
×
NEW
108
      console.log(error);
×
NEW
109
      if ((error as AxiosError).response && [400, 404].includes((error as AxiosError).response!.status)) {
×
NEW
110
        toast.error('Unable to update your order,\n Please try again!');
×
NEW
111
      } else {
×
NEW
112
        const axiosError = error as AxiosError<ErrorResponse>;
×
NEW
113
        if (axiosError.response?.data.message) {
×
NEW
114
          toast.error(axiosError.response?.data.message);
×
NEW
115
        } else {
×
NEW
116
          toast.error('Something went wrong,\n Please try again!');
×
NEW
117
        }
×
NEW
118
      }
×
NEW
119
      setSubmitFormLoading(false);
×
NEW
120
    }
×
NEW
121
  };
×
NEW
122

×
NEW
123
  return (
×
NEW
124
    <div
×
NEW
125
      onClick={() => setShowMenu(false)}
×
NEW
126
      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]"
×
NEW
127
    >
×
NEW
128
      <HeaderOrderManagement
×
NEW
129
        orderTimeIdentifier={order?.createdAt ? new Date(order?.createdAt).getTime().toString() : ''}
×
NEW
130
      />
×
131
      <div className="flex flex-col xlg:flex-row min-h-0 gap-7 w-full">
18✔
132
        {!notFound && !loading && (
18✔
133
          <div className="flex flex-col gap-y-7 bg-white rounded-lg border border-neutral-300 basis-3/4 py-5 px-2 xsm:py-8 xsm:px-4">
18✔
134
            <div className="flex flex-wrap gap-y-3 gap-x-1 lg:gap-5 justify-between">
18✔
135
              <div className="flex flex-col gap-y-2 ">
18✔
136
                <div className="flex items-center gap-x-3">
18✔
137
                  <p className="font-semibold w-[6.6rem] xmd:w-[6.8rem] lg:w-32 text-sm xmd:text-[.9rem] lg:text-base text-nowrap">
18✔
138
                    Order Status
18✔
139
                  </p>
18✔
140
                  {isEditable && (
18✔
141
                    <form action="" className="flex gap-x-1 lg:gap-x-2" onSubmit={submitHandler}>
18✔
142
                      <div
18✔
143
                        onClick={(e: React.MouseEvent<HTMLDivElement>) => e.stopPropagation()}
18✔
144
                        className="relative  border-[1px] border-neutral-300 rounded-xl text-[.9rem] w-[5.5rem] xmd:w-[6.5rem]"
5✔
145
                      >
5✔
146
                        <div
5✔
147
                          onClick={() => setShowMenu((prevValue) => !prevValue)}
5✔
148
                          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`}
5✔
149
                        >
5✔
150
                          <input
5✔
151
                            type="text"
5✔
152
                            title={activeStatus}
3✔
153
                            className="w-full cursor-pointer outline-none bg-transparent capitalize text-xs xmd:text-[.8rem] lg:text-[.86rem]"
3✔
154
                            value={activeStatus.length > 8 ? activeStatus.slice(0, 7).trim() + '..' : activeStatus}
3✔
155
                            readOnly
3✔
156
                          />
3✔
157
                          <i className="fa-solid fa-chevron-down text-[.75rem] xmd:text-[.87rem]"></i>
3✔
158
                        </div>
3✔
159
                        {showMenu && (
3!
160
                          <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]">
3✔
161
                            <ul>
3✔
162
                              <li
3✔
163
                                onClick={() => {
3✔
164
                                  if (activeStatus.toLowerCase() !== 'completed') {
3✔
165
                                    setActiveStatus('completed');
3✔
166
                                    setShowMenu(false);
3!
NEW
167
                                  }
×
168
                                }}
3✔
169
                                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]`}
3✔
170
                              >
3✔
171
                                Completed
3✔
172
                              </li>
3✔
173
                            </ul>
3✔
174
                          </div>
3✔
175
                        )}
1✔
176
                      </div>
1✔
177
                      <button
1✔
178
                        type={submitFormLoading || activeStatus.toLowerCase() === 'received' ? 'button' : 'submit'}
1✔
NEW
179
                        className={`${activeStatus.toLowerCase() === 'received' ? '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`}
×
NEW
180
                      >
×
NEW
181
                        {!submitFormLoading && <span>UPDATE</span>}
×
NEW
182
                        {submitFormLoading && <PulseLoader color="#fff" size={6} />}
×
NEW
183
                      </button>
×
184
                    </form>
1!
185
                  )}
1✔
186

1✔
187
                  {!isEditable && (
1✔
188
                    <div className="border-[1px] border-neutral-300 px-3 lg:px-4 py-1 rounded-xl">
1✔
189
                      <p className="capitalize text-xs xmd:text-[.8rem] lg:text-[.9rem]">{activeStatus}</p>
1✔
190
                    </div>
3✔
191
                  )}
3✔
192
                </div>
3✔
193
                <div className="flex items-center gap-x-4 xmd:gap-x-3">
3✔
194
                  <p className="font-semibold w-[6.6rem] xmd:w-[6.8rem] lg:w-32 text-sm xmd:text-[.9rem] lg:text-base text-nowrap">
3✔
195
                    Order Address
3!
NEW
196
                  </p>
×
197
                  <div className="border-[1px] border-neutral-300 rounded-xl px-3 lg:px-4 py-[.43rem]">
3✔
198
                    <p className="capitalize leading-3 text-xs xmd:text-[.8rem] lg:text-[.9rem] xmd:max-w-40 lg:max-w-none">
3!
199
                      {order?.address.split(', ').join('-')}
3✔
200
                    </p>
3✔
201
                  </div>
3!
202
                </div>
3✔
203
                <div className="flex items-center gap-x-3">
3✔
204
                  <p className="font-semibold w-[6.7rem] xmd:w-[6.8rem] lg:w-32 text-sm xmd:text-[.9rem] lg:text-base">
5✔
205
                    Placed Date
5✔
206
                  </p>
5✔
207
                  <div className="border-[1px] border-neutral-300 rounded-xl px-3 lg:px-4 py-1 text-[.9rem]">
2✔
208
                    <p className="text-xs xmd:text-[.8rem] lg:text-[.9rem]">
2✔
209
                      {order?.createdAt.toLocaleString().split('T')[0]}
2✔
210
                    </p>
2✔
211
                  </div>
2✔
212
                </div>
5✔
213
                <div className="flex items-center gap-x-3">
5✔
214
                  <p className="font-semibold w-[6.7rem] xmd:w-[6.8rem] lg:w-32 text-sm xmd:text-[.9rem] lg:text-base">
5✔
215
                    Total price
5✔
216
                  </p>
5✔
217
                  <div className="border-[1px] border-neutral-300 rounded-xl px-3 lg:px-4 py-1 text-[.9rem]">
5✔
218
                    <p className="text-xs xmd:text-[.8rem] lg:text-[.9rem]">RWF {order?.totalPrice}</p>
5✔
219
                  </div>
5✔
220
                </div>
5✔
221
              </div>
5✔
222
              <div className="min-w-[100%] sm:min-w-[40%]">
5✔
223
                <BuyerInfo buyerInfo={order?.buyer} />
5✔
224
              </div>
5✔
225
            </div>
5✔
226
            <table className="w-full">
5✔
227
              <thead className="text-[.75rem] xmd:text-[.82rem] lg:text-[.88rem]">
5✔
228
                <tr className=" bg-primary text-baseWhite font-normal h-11 xmd:h-12 text-left">
5✔
229
                  <th className="font-normal w-[12%] xmd:w-[10%] text-center xmd:text-left pl-2 lg:pl-3">
5✔
230
                    N<sup>o</sup>
5✔
231
                  </th>
5✔
232
                  <th className="font-normal w-[20%] xmd:w-[20%]">Product</th>
5✔
233
                  <th className="font-normal text-center xmd:text-left w-[15%] hidden xsm:table-cell">
5✔
234
                    {width < 640 ? 'Qty' : 'Quantity'}
5✔
235
                  </th>
5✔
236
                  <th className="font-normal w-[17%] xmd:w-[13%] hidden xsm:table-cell">
5✔
237
                    {width < 640 ? 'P/U' : 'Price/Unit'}
5✔
238
                  </th>
5✔
239
                  <th className="font-normal w-[20%] xmd:w-[20%] pl-2">Vendor</th>
5✔
240
                  <th className="font-normal w-[17%] xmd:w-[20%] pl-2 leading-3">Vendor status</th>
5✔
241
                </tr>
5✔
242
              </thead>
5✔
243
              <tbody className="text-[.7rem] xmd:text-[.76rem] lg:text-[.82rem]">
5✔
244
                {order?.vendors.map((vendor, index) =>
5✔
245
                  vendor.order.orderItems.map((item, itemIndex) => {
5✔
246
                    orderItemsIndex++;
5✔
247
                    return (
5✔
248
                      <tr key={`${index}${itemIndex}`} className="h-9 xmd:h-11 border-b-[1px] border-neutral-300">
5✔
249
                        <td className="text-center xmd:text-left pl-2 lg:pl-3">{orderItemsIndex}.</td>
5✔
250
                        <td className="leading-3" title={item.product.name}>
5✔
251
                          {width < 450 ? item.product.name.slice(0, 10) + '...' : item.product.name}
5✔
252
                        </td>
5✔
253
                        <td className="xmd:pl-3 text-center xmd:text-left hidden xsm:table-cell">{item.quantity}</td>
5✔
254
                        <td className="leading-3 hidden xsm:table-cell">RWF {item['price/unit']}</td>
5✔
255
                        {itemIndex === 0 && (
5✔
256
                          <>
5✔
257
                            <td
5✔
258
                              className={
5✔
259
                                (vendor.order.orderItems.length > 1
5✔
260
                                  ? 'leading-3 capitalize border-l-[1px] border-r-[1px] border-neutral-300'
5✔
261
                                  : '') + ' p-1 xsm:p-2 leading-3 capitalize'
5✔
262
                              }
5✔
263
                              rowSpan={vendor.order.orderItems.length}
5✔
264
                            >
5✔
265
                              <p className="leading-3 capitalize">{vendor.firstName + ' ' + vendor.firstName}</p>
5✔
266
                              <p className="leading-5 text-[.67rem] xmd:text-[.73rem] lg:text-[.8rem] text-neutral-600">
5✔
267
                                {vendor.phoneNumber}
5✔
268
                              </p>
5✔
269
                            </td>
5✔
270
                            <td className="leading-3 capitalize p-1 xsm:p-2" rowSpan={vendor.order.orderItems.length}>
5✔
271
                              {/* {vendor.order.orderItems.length} */}
5✔
272
                              <DashboardOrderStatus status={vendor.order.orderStatus} />
5✔
273
                            </td>
5✔
274
                          </>
5✔
275
                        )}
5✔
276
                      </tr>
5✔
277
                    );
5✔
278
                  })
5✔
279
                )}
5✔
280
              </tbody>
5✔
281
            </table>
5✔
282
          </div>
5✔
283
        )}
5✔
284
        {notFound && (
5✔
285
          <div className="flex justify-center items-center bg-white rounded-lg border border-neutral-300 basis-3/4 py-8 px-4">
5✔
286
            <OrderNotFound link={'/' + decodedToken?.role.toLowerCase() + '/dashboard/orders'} />
5✔
287
          </div>
5✔
288
        )}
5✔
289
        {loading && (
5✔
290
          <div className="flex justify-center items-center bg-white rounded-lg border border-neutral-300 basis-3/4 py-8 px-4">
5✔
291
            <PropagateLoader className="ml-[-.5rem] py-14" />
5!
NEW
292
          </div>
×
293
        )}
5✔
294
        <SideBarOrderStatus />
5✔
295
      </div>
5✔
296
    </div>
5✔
297
  );
5✔
298
};
5✔
299

5✔
300
export default SingleAdminOrder;
5✔
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