source: src/sections/invoice/view/invoice-list-view.tsx@ 057453c

main
Last change on this file since 057453c was 057453c, checked in by Naum Shapkarovski <naumshapkarovski@…>, 5 weeks ago

feat: implement employees

  • Property mode set to 100644
File size: 19.1 KB
Line 
1'use client';
2
3import { useState, useCallback, useEffect, useMemo } from 'react';
4// @mui
5import { useTheme, alpha } from '@mui/material/styles';
6import Tab from '@mui/material/Tab';
7import Tabs from '@mui/material/Tabs';
8import Card from '@mui/material/Card';
9import Table from '@mui/material/Table';
10import Stack from '@mui/material/Stack';
11import Button from '@mui/material/Button';
12import Divider from '@mui/material/Divider';
13import Container from '@mui/material/Container';
14import TableBody from '@mui/material/TableBody';
15import TableContainer from '@mui/material/TableContainer';
16// routes
17import { paths } from 'src/routes/paths';
18import { useRouter } from 'src/routes/hooks';
19import { RouterLink } from 'src/routes/components';
20// utils
21import { fTimestamp } from 'src/utils/format-time';
22// components
23import Label from 'src/components/label';
24import Iconify from 'src/components/iconify';
25import Scrollbar from 'src/components/scrollbar';
26import { useSettingsContext } from 'src/components/settings';
27import CustomBreadcrumbs from 'src/components/custom-breadcrumbs';
28import {
29 useTable,
30 getComparator,
31 emptyRows,
32 TableNoData,
33 TableEmptyRows,
34 TableHeadCustom,
35 TablePaginationCustom,
36} from 'src/components/table';
37// types
38import { Invoice, InvoiceStatus, InvoiceTableFilters, InvoiceTableFilterValue } from 'src/schemas';
39//
40import deleteFromFirebaseStorage from 'src/utils/delete-from-firebase-storage';
41// fetch
42import { useDeleteInvoice, useGetInvoices } from 'src/api/invoice';
43import { collections, removeDocument } from 'src/lib/firestore';
44import { mutate } from 'swr';
45import { useBoolean } from 'src/hooks/use-boolean';
46import InvoiceAnalytic from '../invoice-analytic';
47import InvoiceTableRow from '../invoice-table-row';
48import InvoiceTableFiltersResult from '../invoice-table-filters-result';
49import InvoiceTableToolbar from '../invoice-table-toolbar';
50import MailCompose from '../mail-compose';
51
52// ----------------------------------------------------------------------
53
54const TABLE_HEAD = [
55 { id: 'invoiceNumber', label: 'Customer' },
56 { id: 'price', label: 'Amount' },
57 { id: 'currency', label: 'Currency' },
58 { id: 'invoicePeriod', label: 'Invoice Period' },
59 { id: 'createDate', label: 'Create' },
60 { id: 'dueDate', label: 'Due' },
61 { id: 'sent', label: 'Sent', align: 'center' },
62 { id: 'status', label: 'Status' },
63 { id: '' },
64];
65
66const defaultFilters: InvoiceTableFilters = {
67 name: '',
68 service: [],
69 status: 'all',
70 startDate: new Date('2024-01-01T00:00:00Z'),
71 endDate: null,
72};
73
74// ----------------------------------------------------------------------
75
76interface StatusTotals {
77 EUR: number;
78 USD: number;
79}
80
81type ResultsType = {
82 [key in 'total' | InvoiceStatus]: StatusTotals;
83};
84
85const getEURtoUSDExchangeRate = async () => {
86 try {
87 // const response = await fetch(
88 // 'http://api.exchangeratesapi.io/v1/latest?access_key=248cc5d9f103fcb183ed4987f2d85b3b&base=EUR&symbols=USD'
89 // );
90 // const data = await response.json();
91 // return data.rates.USD;
92 return 1.05;
93 } catch (error) {
94 console.error('Error fetching the exchange rate:', error);
95 return null;
96 }
97};
98
99const getTotalAmountForAllStatuses = async (invoices: Invoice[]) => {
100 const eurToUsdRate = await getEURtoUSDExchangeRate();
101 if (!eurToUsdRate) {
102 throw new Error("Couldn't fetch the exchange rate.");
103 }
104
105 const results: ResultsType = {
106 total: { EUR: 0, USD: 0 },
107 // excluded
108 processing: { EUR: 0, USD: 0 },
109 paid: { EUR: 0, USD: 0 },
110 pending: { EUR: 0, USD: 0 },
111 overdue: { EUR: 0, USD: 0 },
112 draft: { EUR: 0, USD: 0 },
113 };
114
115 invoices.forEach((invoice) => {
116 const updateAmounts = (key: 'total' | InvoiceStatus) => {
117 const excludeProcessing = key === 'processing' ? 'pending' : key;
118 if (invoice.currency === 'USD') {
119 results[excludeProcessing].USD += invoice.totalAmount;
120 results[excludeProcessing].EUR += invoice.totalAmount / eurToUsdRate; // Convert USD to EUR
121 } else if (invoice.currency === 'EUR') {
122 results[excludeProcessing].EUR += invoice.totalAmount;
123 results[excludeProcessing].USD += invoice.totalAmount * eurToUsdRate; // Convert EUR to USD
124 }
125 };
126
127 updateAmounts('total');
128 updateAmounts(invoice.status as InvoiceStatus);
129 });
130
131 return results;
132};
133
134export default function InvoiceListView() {
135 const theme = useTheme();
136
137 const settings = useSettingsContext();
138
139 const [analytics, setAnalytics] = useState({
140 total: { EUR: 0, USD: 0 },
141 paid: { EUR: 0, USD: 0 },
142 pending: { EUR: 0, USD: 0 },
143 overdue: { EUR: 0, USD: 0 },
144 draft: { EUR: 0, USD: 0 },
145 });
146
147 const [sendingInvoice, setSendingInvoice] = useState<Invoice | null>(null);
148
149 const router = useRouter();
150
151 const table = useTable({
152 defaultOrderBy: 'createDate',
153 defaultOrder: 'desc',
154 defaultDense: true,
155 defaultRowsPerPage: 25,
156 });
157
158 const [filters, setFilters] = useState(defaultFilters);
159
160 const { invoices: tableData } = useGetInvoices({ startDate: filters.startDate?.toISOString() });
161
162 const invoiceMutationKey = useMemo(
163 () => [
164 collections.invoice,
165 JSON.stringify({
166 where: [['createDate', '>=', filters.startDate]],
167 orderBy: 'createDate',
168 direction: 'desc',
169 }),
170 ],
171 [filters]
172 );
173
174 const dateError =
175 filters.startDate && filters.endDate
176 ? filters.startDate.getTime() > filters.endDate.getTime()
177 : false;
178
179 const dataFiltered = applyFilter({
180 inputData: tableData,
181 comparator: getComparator(table.order, table.orderBy),
182 filters,
183 dateError,
184 });
185
186 // const dataInPage = dataFiltered.slice(
187 // table.page * table.rowsPerPage,
188 // table.page * table.rowsPerPage + table.rowsPerPage
189 // );
190
191 const denseHeight = table.dense ? 56 : 76;
192
193 const canReset =
194 !!filters.name ||
195 !!filters.service.length ||
196 filters.status !== 'all' ||
197 (!!filters.startDate && !!filters.endDate);
198
199 const notFound = (!dataFiltered.length && canReset) || !dataFiltered.length;
200
201 const getInvoiceLength = (status: string) =>
202 tableData.filter((item) => item.status === status).length;
203
204 // const getTotalAmount = (status: string) =>
205 // sumBy(
206 // tableData.filter((item) => item.status === status),
207 // 'totalAmount'
208 // );
209
210 useEffect(() => {
211 if (tableData) {
212 const getAnalytics = async () => {
213 const analyticsStats = await getTotalAmountForAllStatuses(tableData);
214 setAnalytics(analyticsStats);
215 };
216 getAnalytics();
217 }
218 }, [tableData]);
219
220 const getPercentByStatus = (status: string) =>
221 (getInvoiceLength(status) / tableData.length) * 100;
222
223 const TABS = [
224 { value: 'all', label: 'All', color: 'default', count: tableData.length },
225 {
226 value: 'paid',
227 label: 'Paid',
228 color: 'success',
229 count: getInvoiceLength('paid'),
230 },
231 {
232 value: 'pending',
233 label: 'Pending',
234 color: 'warning',
235 count: getInvoiceLength('pending'),
236 },
237 {
238 value: 'overdue',
239 label: 'Overdue',
240 color: 'error',
241 count: getInvoiceLength('overdue'),
242 },
243 {
244 value: 'draft',
245 label: 'Draft',
246 color: 'default',
247 count: getInvoiceLength('draft'),
248 },
249 ] as const;
250
251 const handleFilters = useCallback(
252 (name: string, value: InvoiceTableFilterValue) => {
253 table.onResetPage();
254 setFilters((prevState) => ({
255 ...prevState,
256 [name]: value,
257 }));
258 },
259 [table]
260 );
261
262 const { deleteInvoiceMutation } = useDeleteInvoice();
263
264 const handleDeleteRow = useCallback(
265 async (invoice: Invoice) => {
266 const serializedParams = JSON.stringify({
267 where: [['createDate', '>=', filters.startDate]],
268 orderBy: 'createDate',
269 direction: 'desc',
270 });
271
272 await deleteInvoiceMutation(invoice.id);
273 await deleteFromFirebaseStorage(
274 `invoices/${invoice.invoiceTo.name}/${invoice.id}-${invoice.invoiceNumber}.pdf`
275 );
276
277 mutate(invoiceMutationKey);
278 },
279 [filters.startDate, invoiceMutationKey]
280 );
281
282 const openCompose = useBoolean();
283
284 const handleToggleCompose = useCallback(
285 async (invoice: Invoice) => {
286 setSendingInvoice(invoice);
287 openCompose.onToggle();
288 },
289 [openCompose]
290 );
291
292 // const handleDeleteRows = useCallback(() => {
293 // const deleteRows = tableData.filter((row) => !table.selected.includes(row.id));
294 // // setTableData(deleteRows);
295
296 // table.onUpdatePageDeleteRows({
297 // totalRows: tableData.length,
298 // totalRowsInPage: dataInPage.length,
299 // totalRowsFiltered: dataFiltered.length,
300 // });
301 // }, [dataFiltered.length, dataInPage.length, table, tableData]);
302
303 const handleEditRow = useCallback(
304 (id: string) => {
305 router.push(paths.dashboard.invoice.edit(id));
306 },
307 [router]
308 );
309
310 const handleCopyRow = useCallback(
311 (id: string) => {
312 router.push(paths.dashboard.invoice.copy(id));
313 },
314 [router]
315 );
316
317 const handleViewRow = useCallback(
318 (id: string) => {
319 router.push(paths.dashboard.invoice.details(id));
320 },
321 [router]
322 );
323
324 const handleFilterStatus = useCallback(
325 (event: React.SyntheticEvent, newValue: string) => {
326 handleFilters('status', newValue);
327 },
328 [handleFilters]
329 );
330
331 const handleResetFilters = useCallback(() => {
332 setFilters(defaultFilters);
333 }, []);
334
335 return (
336 <>
337 <Container maxWidth={settings.themeStretch ? false : 'lg'}>
338 <CustomBreadcrumbs
339 heading="List"
340 links={[
341 {
342 name: 'Dashboard',
343 href: paths.dashboard.root,
344 },
345 {
346 name: 'Invoice',
347 href: paths.dashboard.invoice.root,
348 },
349 {
350 name: 'List',
351 },
352 ]}
353 action={
354 <Button
355 component={RouterLink}
356 href={paths.dashboard.invoice.new}
357 variant="contained"
358 startIcon={<Iconify icon="mingcute:add-line" />}
359 >
360 New Invoice
361 </Button>
362 }
363 sx={{
364 mb: { xs: 3, md: 5 },
365 }}
366 />
367
368 <Card
369 sx={{
370 mb: { xs: 3, md: 5 },
371 }}
372 >
373 <Scrollbar>
374 <Stack
375 direction="row"
376 divider={<Divider orientation="vertical" flexItem sx={{ borderStyle: 'dashed' }} />}
377 sx={{ py: 2 }}
378 >
379 <InvoiceAnalytic
380 title="Total"
381 total={tableData.length}
382 percent={100}
383 price={analytics.total}
384 icon="solar:bill-list-bold-duotone"
385 color={theme.palette.info.main}
386 />
387
388 <InvoiceAnalytic
389 title="Paid"
390 total={getInvoiceLength('paid')}
391 percent={getPercentByStatus('paid')}
392 price={analytics.paid}
393 icon="solar:file-check-bold-duotone"
394 color={theme.palette.success.main}
395 />
396
397 <InvoiceAnalytic
398 title="Pending"
399 total={getInvoiceLength('pending')}
400 percent={getPercentByStatus('pending')}
401 price={analytics.pending}
402 icon="solar:sort-by-time-bold-duotone"
403 color={theme.palette.warning.main}
404 />
405
406 <InvoiceAnalytic
407 title="Overdue"
408 total={getInvoiceLength('overdue')}
409 percent={getPercentByStatus('overdue')}
410 price={analytics.overdue}
411 icon="solar:bell-bing-bold-duotone"
412 color={theme.palette.error.main}
413 />
414
415 <InvoiceAnalytic
416 title="Draft"
417 total={getInvoiceLength('draft')}
418 percent={getPercentByStatus('draft')}
419 price={analytics.draft}
420 icon="solar:file-corrupted-bold-duotone"
421 color={theme.palette.text.secondary}
422 />
423 </Stack>
424 </Scrollbar>
425 </Card>
426
427 <Card>
428 <Tabs
429 value={filters.status}
430 onChange={handleFilterStatus}
431 sx={{
432 px: 2.5,
433 boxShadow: `inset 0 -2px 0 0 ${alpha(theme.palette.grey[500], 0.08)}`,
434 }}
435 >
436 {TABS.map((tab) => (
437 <Tab
438 key={tab.value}
439 value={tab.value}
440 label={tab.label}
441 iconPosition="end"
442 icon={
443 <Label
444 variant={
445 ((tab.value === 'all' || tab.value === filters.status) && 'filled') || 'soft'
446 }
447 color={tab.color}
448 >
449 {tab.count}
450 </Label>
451 }
452 />
453 ))}
454 </Tabs>
455
456 <InvoiceTableToolbar
457 filters={filters}
458 onFilters={handleFilters}
459 //
460 dateError={dateError}
461 serviceOptions={[]}
462 />
463
464 {canReset && (
465 <InvoiceTableFiltersResult
466 filters={filters}
467 onFilters={handleFilters}
468 //
469 onResetFilters={handleResetFilters}
470 //
471 results={dataFiltered.length}
472 sx={{ p: 2.5, pt: 0 }}
473 />
474 )}
475
476 <TableContainer sx={{ position: 'relative', overflow: 'unset' }}>
477 {/* <TableSelectedAction
478 dense={table.dense}
479 numSelected={table.selected.length}
480 rowCount={tableData.length}
481 onSelectAllRows={(checked) =>
482 table.onSelectAllRows(
483 checked,
484 tableData.map((row) => row.id)
485 )
486 }
487 action={
488 <Stack direction="row">
489 <Tooltip title="Sent">
490 <IconButton color="primary">
491 <Iconify icon="iconamoon:send-fill" />
492 </IconButton>
493 </Tooltip>
494
495 <Tooltip title="Download">
496 <IconButton color="primary">
497 <Iconify icon="eva:download-outline" />
498 </IconButton>
499 </Tooltip>
500
501 <Tooltip title="Print">
502 <IconButton color="primary">
503 <Iconify icon="solar:printer-minimalistic-bold" />
504 </IconButton>
505 </Tooltip>
506
507 <Tooltip title="Delete">
508 <IconButton color="primary" onClick={confirm.onTrue}>
509 <Iconify icon="solar:trash-bin-trash-bold" />
510 </IconButton>
511 </Tooltip>
512 </Stack>
513 }
514 /> */}
515
516 <Scrollbar>
517 <Table size={table.dense ? 'small' : 'medium'} sx={{ minWidth: 800 }}>
518 <TableHeadCustom
519 order={table.order}
520 orderBy={table.orderBy}
521 headLabel={TABLE_HEAD}
522 rowCount={tableData.length}
523 numSelected={table.selected.length}
524 onSort={table.onSort}
525 onSelectAllRows={(checked) =>
526 table.onSelectAllRows(
527 checked,
528 tableData.map((row) => row.id)
529 )
530 }
531 />
532
533 <TableBody>
534 {dataFiltered
535 .slice(
536 table.page * table.rowsPerPage,
537 table.page * table.rowsPerPage + table.rowsPerPage
538 )
539 .map((row) => (
540 <InvoiceTableRow
541 key={row.id}
542 row={row}
543 selected={table.selected.includes(row.id)}
544 onSelectRow={() => table.onSelectRow(row.id)}
545 onViewRow={() => handleViewRow(row.id)}
546 onEditRow={() => handleEditRow(row.id)}
547 onCopyRow={() => handleCopyRow(row.id)}
548 onDeleteRow={async () => handleDeleteRow(row)}
549 onSendRow={() => {}}
550 onToggleCompose={() => handleToggleCompose(row)}
551 />
552 ))}
553
554 <TableEmptyRows
555 height={denseHeight}
556 emptyRows={emptyRows(table.page, table.rowsPerPage, tableData.length)}
557 />
558
559 <TableNoData notFound={notFound} />
560 </TableBody>
561 </Table>
562 </Scrollbar>
563 </TableContainer>
564
565 <TablePaginationCustom
566 count={dataFiltered.length}
567 page={table.page}
568 rowsPerPage={table.rowsPerPage}
569 onPageChange={table.onChangePage}
570 onRowsPerPageChange={table.onChangeRowsPerPage}
571 //
572 dense={table.dense}
573 onChangeDense={table.onChangeDense}
574 />
575 </Card>
576 </Container>
577
578 {openCompose.value && (
579 <MailCompose
580 invoice={sendingInvoice}
581 onCloseCompose={openCompose.onFalse}
582 invoiceMutationKey={invoiceMutationKey}
583 />
584 )}
585
586 {/* <ConfirmDialog
587 open={confirm.value}
588 onClose={confirm.onFalse}
589 title="Delete"
590 content={
591 <>
592 Are you sure want to delete <strong> {table.selected.length} </strong> items?
593 </>
594 }
595 action={
596 <Button
597 variant="contained"
598 color="error"
599 onClick={() => {
600 handleDeleteRows();
601 confirm.onFalse();
602 }}
603 >
604 Delete
605 </Button>
606 }
607 /> */}
608 </>
609 );
610}
611
612// ----------------------------------------------------------------------
613
614function applyFilter({
615 inputData,
616 comparator,
617 filters,
618 dateError,
619}: {
620 inputData: Invoice[];
621 comparator: (a: any, b: any) => number;
622 filters: InvoiceTableFilters;
623 dateError: boolean;
624}) {
625 const { name, status, service, startDate, endDate } = filters;
626
627 const stabilizedThis = inputData.map((el, index) => [el, index] as const);
628
629 stabilizedThis.sort((a, b) => {
630 const order = comparator(a[0], b[0]);
631 if (order !== 0) return order;
632 return a[1] - b[1];
633 });
634
635 inputData = stabilizedThis.map((el) => el[0]);
636
637 if (name) {
638 inputData = inputData.filter(
639 (invoice) =>
640 invoice.invoiceNumber.toLowerCase().indexOf(name.toLowerCase()) !== -1 ||
641 invoice.invoiceTo.name.toLowerCase().indexOf(name.toLowerCase()) !== -1
642 );
643 }
644
645 if (status !== 'all') {
646 inputData = inputData.filter((invoice) => invoice.status === status);
647 }
648
649 if (service.length) {
650 inputData = inputData.filter((invoice) =>
651 invoice.items.some((filterItem) => service.includes(filterItem.service.id))
652 );
653 }
654
655 if (!dateError) {
656 if (startDate && endDate) {
657 inputData = inputData.filter(
658 (invoice) =>
659 fTimestamp(invoice.createDate.getTime()) >= fTimestamp(startDate.getTime()) &&
660 fTimestamp(invoice.createDate.getTime()) <= fTimestamp(endDate.getTime())
661 );
662 }
663 }
664
665 return inputData;
666}
Note: See TracBrowser for help on using the repository browser.