-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/develop' into feature/401Stuff
- Loading branch information
Showing
15 changed files
with
825 additions
and
1 deletion.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import React from 'react'; | ||
|
||
const CertificateItem = () => { | ||
return ( | ||
<tr> | ||
<td className="p-2">2024/01/01</td> | ||
<td className="p-2">Invoice 000001</td> | ||
<td className="p-2">59373022</td> | ||
<td className="p-2">23. Proof of Return or Discount for ...</td> | ||
<td className="p-2">Yes</td> | ||
<td className="p-2">1,785,000 NTD</td> | ||
<td className="p-2">Taxable 5%</td> | ||
<td className="p-2">1,785,000 NTD</td> | ||
<td className="p-2">20240417-001</td> | ||
</tr> | ||
); | ||
}; | ||
|
||
export default CertificateItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import React, { useState } from 'react'; | ||
import CertificateItem from '@/components/upload_certificate/certificate_item'; | ||
import Pagination from '@/components/upload_certificate/pagination'; // 引入 Pagination 組件 | ||
|
||
interface CertificateTableProps { | ||
data: unknown[]; // Deprecated: (20240919 - tzuhan) will be replaced by actual data type | ||
} | ||
|
||
// Deprecated: (20240919 - tzuhan) will be replaced by actual data type | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const CertificateTable: React.FC<CertificateTableProps> = ({ data }) => { | ||
const [currentPage, setCurrentPage] = useState(1); | ||
const itemsPerPage = 10; // Info: (20240919 - tzuhan) 每頁顯示的項目數 | ||
const totalItems = 100; // Info: (20240919 - tzuhan) 總項目數,實際情況中可以來自 API | ||
const totalPages = Math.ceil(totalItems / itemsPerPage); | ||
|
||
return ( | ||
<div className="rounded-lg bg-white p-4"> | ||
<div className="overflow-auto"> | ||
<table className="min-w-full bg-white"> | ||
<thead> | ||
<tr> | ||
<th className="p-2">Date</th> | ||
<th className="p-2">Invoice Name/No.</th> | ||
<th className="p-2">From / To-Tax ID</th> | ||
<th className="p-2">Business Tax Format Code</th> | ||
<th className="p-2">Deductible</th> | ||
<th className="p-2">Price before tax</th> | ||
<th className="p-2">Tax</th> | ||
<th className="p-2">Total price</th> | ||
<th className="p-2">Voucher No</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{/* Deprecated: (20240919 - tzuhan) Example of dynamic rows, should map actual data here */} | ||
{[...Array(10)].map((_, index) => ( | ||
<CertificateItem key={`certificate-item-${index + 1}`} /> | ||
))} | ||
</tbody> | ||
</table> | ||
</div> | ||
|
||
{/* Info: (20240919 - tzuhan) 分頁組件 */} | ||
<div className="mt-4 flex justify-center"> | ||
<Pagination | ||
currentPage={currentPage} | ||
totalPages={totalPages} | ||
onPageChange={(page) => setCurrentPage(page)} | ||
/> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default CertificateTable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import { IAPIName } from '@/interfaces/api_connection'; | ||
import APIHandler from '@/lib/utils/api_handler'; | ||
import React, { useState, useEffect, useCallback } from 'react'; | ||
|
||
interface FilterSectionProps { | ||
apiName: IAPIName; | ||
params?: Record<string, string | number | boolean>; | ||
types?: string[]; | ||
statuses?: string[]; | ||
sortingOptions?: string[]; | ||
onApiResponse?: (data: unknown[]) => void; // Info: (20240919 - tzuhan) 回傳 API 回應資料 | ||
viewType: 'grid' | 'list'; | ||
viewToggleHandler: (viewType: 'grid' | 'list') => void; | ||
} | ||
|
||
const FilterSection: React.FC<FilterSectionProps> = ({ | ||
apiName, | ||
params, | ||
types = [], | ||
statuses = [], | ||
sortingOptions = [], | ||
onApiResponse, | ||
viewType, | ||
viewToggleHandler, | ||
}) => { | ||
const [selectedType, setSelectedType] = useState<string | undefined>(); | ||
const [selectedStatus, setSelectedStatus] = useState<string | undefined>(); | ||
const [selectedDateRange, setSelectedDateRange] = useState<{ start?: string; end?: string }>({}); | ||
const [searchQuery, setSearchQuery] = useState<string | undefined>(); | ||
const [selectedSorting, setSelectedSorting] = useState<string | undefined>(); | ||
const [isLoading, setIsLoading] = useState<boolean>(false); | ||
const { trigger } = APIHandler<unknown[]>(apiName); | ||
|
||
// Info: (20240919 - tzuhan) 發送 API 請求 | ||
const fetchData = useCallback(async () => { | ||
try { | ||
if (isLoading) return; | ||
setIsLoading(true); | ||
const { success, code, data } = await trigger({ | ||
params, | ||
query: { | ||
type: selectedType, | ||
status: selectedStatus, | ||
startDate: selectedDateRange.start, | ||
endDate: selectedDateRange.end, | ||
search: searchQuery, | ||
sort: selectedSorting, | ||
}, | ||
}); | ||
if (success && onApiResponse) onApiResponse(data!); | ||
if (!success) { | ||
// Deprecated: (20240919 - tzuhan) Debugging purpose only | ||
// eslint-disable-next-line no-console | ||
console.error('API Request Failed:', code); | ||
} | ||
} catch (error) { | ||
// Deprecated: (20240919 - tzuhan) Debugging purpose only | ||
// eslint-disable-next-line no-console | ||
console.error('API Request error:', error); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
}, [isLoading, selectedType, selectedStatus, selectedDateRange, searchQuery, selectedSorting]); | ||
|
||
const onSearchClick = () => { | ||
setSearchQuery((document.getElementById('search') as HTMLInputElement)?.value); | ||
}; | ||
|
||
// Info: (20240919 - tzuhan) 每次狀態變更時,組合查詢條件並發送 API 請求 | ||
useEffect(() => { | ||
fetchData(); | ||
}, [selectedType, selectedStatus, selectedDateRange, searchQuery, selectedSorting]); | ||
|
||
return ( | ||
<div | ||
className="flex flex-wrap items-center justify-start space-x-4 rounded-lg bg-white p-4" | ||
style={{ maxWidth: '100%' }} | ||
> | ||
{/* Info: (20240919 - tzuhan) 類型篩選 */} | ||
{types.length > 0 && ( | ||
<div className="flex min-w-150px flex-col"> | ||
<label htmlFor="type" className="text-sm font-medium text-gray-500"> | ||
<div>Type</div> | ||
<select | ||
id="type" | ||
className="rounded-md border border-gray-300 p-2" | ||
onChange={(e) => setSelectedType(e.target.value)} | ||
> | ||
<option value={undefined}>All</option> | ||
{types.map((type) => ( | ||
<option key={type} value={type}> | ||
{type} | ||
</option> | ||
))} | ||
</select> | ||
</label> | ||
</div> | ||
)} | ||
|
||
{/* Info: (20240919 - tzuhan) 狀態篩選 */} | ||
{statuses.length > 0 && ( | ||
<div className="flex min-w-150px flex-col"> | ||
<label htmlFor="status" className="text-sm font-medium text-gray-500"> | ||
<div>Status</div> | ||
<select | ||
id="status" | ||
className="rounded-md border border-gray-300 p-2" | ||
onChange={(e) => setSelectedStatus(e.target.value)} | ||
> | ||
<option value={undefined}>All</option> | ||
{statuses.map((status) => ( | ||
<option key={status} value={status}> | ||
{status} | ||
</option> | ||
))} | ||
</select> | ||
</label> | ||
</div> | ||
)} | ||
|
||
{/* Info: (20240919 - tzuhan) 時間區間篩選 */} | ||
<div className="flex min-w-250px flex-col"> | ||
<label htmlFor="date-range" className="text-sm font-medium text-gray-500"> | ||
<div>Period</div> | ||
<input | ||
type="text" | ||
id="date-range" | ||
className="rounded-md border border-gray-300 p-2" | ||
placeholder="Start Date - End Date" | ||
onBlur={(e) => { | ||
const [startDate, endDate] = e.target.value.split(' - '); | ||
setSelectedDateRange({ start: startDate.trim(), end: endDate.trim() }); | ||
}} | ||
/> | ||
</label> | ||
</div> | ||
|
||
{/* Info: (20240919 - tzuhan) 搜索欄 */} | ||
<div className="flex min-w-200px flex-col"> | ||
<label htmlFor="search" className="text-sm font-medium text-gray-500"> | ||
<div className="flex items-center"> | ||
<input | ||
type="text" | ||
id="search" | ||
className="grow rounded-l-md border border-gray-300 p-2" | ||
placeholder="Search" | ||
onKeyDown={(e) => { | ||
if (e.key === 'Enter') { | ||
setSearchQuery(e.currentTarget.value); | ||
} | ||
}} | ||
/> | ||
<button type="button" className="rounded-r-md bg-gray-300 p-2" onClick={onSearchClick}> | ||
🔍 | ||
</button> | ||
</div> | ||
</label> | ||
</div> | ||
|
||
{/* Info: (20240919 - tzuhan) 排序選項 */} | ||
{sortingOptions.length > 0 && ( | ||
<div className="flex min-w-150px flex-col"> | ||
<label htmlFor="sort" className="text-sm font-medium text-gray-500"> | ||
{selectedSorting || 'Sort'} | ||
</label> | ||
<select | ||
id="sort" | ||
className="rounded-md border border-gray-300 p-2" | ||
onChange={(e) => setSelectedSorting(e.target.value)} | ||
> | ||
<option value={undefined}>Default</option> | ||
{sortingOptions.map((sort) => ( | ||
<option key={sort} value={sort}> | ||
{sort} | ||
</option> | ||
))} | ||
</select> | ||
</div> | ||
)} | ||
|
||
{/* Info: (20240919 - tzuhan) 顯示風格切換 */} | ||
<div className="flex min-w-150px items-center space-x-2"> | ||
<button | ||
type="button" | ||
className={`rounded border p-2 hover:bg-gray-300 ${viewType === 'grid' ? 'bg-gray-200' : ''}`} | ||
onClick={() => viewToggleHandler('grid')} | ||
> | ||
Grid View | ||
</button> | ||
<button | ||
type="button" | ||
className={`rounded border p-2 hover:bg-gray-300 ${viewType === 'list' ? 'bg-gray-200' : ''}`} | ||
onClick={() => viewToggleHandler('list')} | ||
> | ||
List View | ||
</button> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default FilterSection; |
Oops, something went wrong.