iaun 3 сар өмнө
parent
commit
f7a1a16924

+ 14 - 0
backend/app/routes/work_record.py

@@ -173,6 +173,7 @@ class WorkRecordList(Resource):
     @work_record_ns.param('start_date', '开始日期 (YYYY-MM-DD)', type=str)
     @work_record_ns.param('end_date', '结束日期 (YYYY-MM-DD)', type=str)
     @work_record_ns.param('is_settled', '按结算状态筛选 (true/false)', type=str)
+    @work_record_ns.param('supplier_id', '按供应商ID筛选,使用 "none" 筛选无供应商的记录', type=str)
     @work_record_ns.param('page', '页码,从1开始', type=int, default=1)
     @work_record_ns.param('page_size', '每页数量,默认20,最大100', type=int, default=20)
     @work_record_ns.response(200, 'Success', paginated_response)
@@ -194,6 +195,18 @@ class WorkRecordList(Resource):
         if is_settled_param is not None:
             is_settled = is_settled_param.lower() == 'true'
         
+        # Parse supplier_id parameter
+        supplier_id_param = request.args.get('supplier_id')
+        supplier_id = None
+        if supplier_id_param is not None:
+            if supplier_id_param.lower() == 'none':
+                supplier_id = 'none'
+            else:
+                try:
+                    supplier_id = int(supplier_id_param)
+                except (ValueError, TypeError):
+                    pass
+        
         # 如果指定了具体日期,使用 start_date 和 end_date 来筛选同一天
         if date:
             start_date = date
@@ -206,6 +219,7 @@ class WorkRecordList(Resource):
             year=year,
             month=month,
             is_settled=is_settled,
+            supplier_id=supplier_id,
             page=page,
             page_size=page_size
         )

+ 37 - 1
backend/app/services/work_record_service.py

@@ -59,7 +59,7 @@ class WorkRecordService:
     
     @staticmethod
     def get_all(person_id=None, start_date=None, end_date=None, year=None, month=None, is_settled=None,
-                page=1, page_size=20):
+                supplier_id=None, page=1, page_size=20):
         """Get work records with pagination and filters.
         
         All filters are applied as intersection (AND logic).
@@ -71,6 +71,7 @@ class WorkRecordService:
             year: Filter by year (optional, used with month)
             month: Filter by month 1-12 (optional, used with year)
             is_settled: Filter by settlement status (optional, True/False)
+            supplier_id: Filter by supplier ID (optional), use "none" to filter items without supplier
             page: Page number, starting from 1 (default: 1)
             page_size: Number of records per page (default: 20, max: 100)
             
@@ -97,6 +98,15 @@ class WorkRecordService:
         if is_settled is not None:
             query = query.filter(WorkRecord.is_settled == is_settled)
         
+        # Apply supplier filter
+        if supplier_id is not None:
+            query = query.join(Item)
+            if supplier_id == 'none':
+                # Filter items without supplier
+                query = query.filter(Item.supplier_id.is_(None))
+            else:
+                query = query.filter(Item.supplier_id == supplier_id)
+        
         # Apply month filter if both year and month are provided
         if year is not None and month is not None:
             month_start = date(year, month, 1)
@@ -485,6 +495,31 @@ class WorkRecordService:
             reverse=True
         )
         
+        # Group by person and item for unsettled breakdown only
+        unsettled_item_breakdown_dict = {}
+        for wr in work_records:
+            if not wr.is_settled:
+                key = (wr.person_id, wr.item_id)
+                if key not in unsettled_item_breakdown_dict:
+                    supplier_name = wr.item.supplier.name if wr.item.supplier else ''
+                    unsettled_item_breakdown_dict[key] = {
+                        'person_id': wr.person_id,
+                        'person_name': wr.person.name,
+                        'item_id': wr.item_id,
+                        'item_name': wr.item.name,
+                        'supplier_name': supplier_name,
+                        'quantity': 0,
+                        'earnings': 0.0
+                    }
+                unsettled_item_breakdown_dict[key]['quantity'] += wr.quantity
+                unsettled_item_breakdown_dict[key]['earnings'] += wr.total_price
+        
+        # Sort by person_name, then item_name
+        unsettled_item_breakdown = sorted(
+            unsettled_item_breakdown_dict.values(),
+            key=lambda x: (x['person_name'], x['item_name'])
+        )
+        
         # Group by person, supplier, and settlement status for supplier_breakdown
         person_supplier_earnings = {}
         for wr in work_records:
@@ -518,6 +553,7 @@ class WorkRecordService:
             'unsettled_earnings': unsettled_earnings,
             'top_performers': top_performers,
             'item_breakdown': item_breakdown,
+            'unsettled_item_breakdown': unsettled_item_breakdown,
             'supplier_breakdown': supplier_breakdown
         }
 

+ 81 - 21
frontend/src/components/Dashboard.jsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useMemo } from 'react'
+import { useState, useEffect, useMemo } from 'react'
 import { useNavigate } from 'react-router-dom'
 import { Card, Row, Col, Statistic, Button, DatePicker, Table, Empty, Spin, message, Alert } from 'antd'
 import {
@@ -9,7 +9,6 @@ import {
   ReloadOutlined,
   TrophyOutlined,
   ShoppingOutlined,
-  CalendarOutlined,
   CheckCircleOutlined,
   ClockCircleOutlined
 } from '@ant-design/icons'
@@ -229,6 +228,39 @@ function Dashboard() {
     }
   ]
 
+  // Table columns for unsettled item breakdown (monthly)
+  const unsettledItemBreakdownColumns = [
+    {
+      title: '人员',
+      dataIndex: 'person_name',
+      key: 'person_name'
+    },
+    {
+      title: '物品',
+      dataIndex: 'item_name',
+      key: 'item_name'
+    },
+    {
+      title: '供应商',
+      dataIndex: 'supplier_name',
+      key: 'supplier_name',
+      render: (value) => value || ''
+    },
+    {
+      title: '数量',
+      dataIndex: 'quantity',
+      key: 'quantity',
+      align: 'right'
+    },
+    {
+      title: '未结算收入',
+      dataIndex: 'earnings',
+      key: 'earnings',
+      align: 'right',
+      render: (value) => <span style={{ color: '#fa8c16' }}>¥{value.toFixed(2)}</span>
+    }
+  ]
+
   // Table columns for supplier breakdown (monthly)
   const supplierBreakdownColumns = [
     {
@@ -422,31 +454,62 @@ function Dashboard() {
             </Col>
           </Row>
 
-          <Row gutter={[16, 16]}>
-            <Col xs={24} md={12}>
+          <Row gutter={[16, 16]} style={{ marginTop: 16 }}>
+            <Col xs={24}>
               <Card 
                 title={
                   <span>
-                    <TrophyOutlined style={{ marginRight: 8 }} />
-                    业绩排名
+                    <UserOutlined style={{ marginRight: 8 }} />
+                    人员按供应商收入明细
                   </span>
                 }
                 size="small"
               >
-                {monthlySummary?.top_performers?.length > 0 ? (
+                {monthlySummary?.supplier_breakdown?.length > 0 ? (
                   <Table
-                    columns={topPerformersColumns}
-                    dataSource={monthlySummary.top_performers}
-                    rowKey="person_id"
+                    columns={supplierBreakdownColumns}
+                    dataSource={monthlySummary.supplier_breakdown}
+                    rowKey={(record) => `${record.person_id}-${record.supplier_name}-${record.is_settled}`}
                     pagination={false}
                     size="small"
                     tableLayout="auto"
+                    rowClassName={(record) => record.is_settled === false ? 'unsettled-row' : ''}
                   />
                 ) : (
                   <Empty description="暂无数据" />
                 )}
               </Card>
             </Col>
+          </Row>
+
+          <Row gutter={[16, 16]} style={{ marginTop: 16 }}>
+            <Col xs={24}>
+              <Card 
+                title={
+                  <span>
+                    <ClockCircleOutlined style={{ marginRight: 8, color: '#fa8c16' }} />
+                    未结算物品收入明细
+                  </span>
+                }
+                size="small"
+              >
+                {monthlySummary?.unsettled_item_breakdown?.length > 0 ? (
+                  <Table
+                    columns={unsettledItemBreakdownColumns}
+                    dataSource={monthlySummary.unsettled_item_breakdown}
+                    rowKey={(record) => `${record.person_id}-${record.item_id}`}
+                    pagination={false}
+                    size="small"
+                    tableLayout="auto"
+                  />
+                ) : (
+                  <Empty description="暂无未结算物品" />
+                )}
+              </Card>
+            </Col>
+          </Row>
+
+          <Row gutter={[16, 16]} style={{ marginTop: 16 }}>
             <Col xs={24} md={12}>
               <Card 
                 title={
@@ -471,28 +534,24 @@ function Dashboard() {
                 )}
               </Card>
             </Col>
-          </Row>
-
-          <Row gutter={[16, 16]} style={{ marginTop: 16 }}>
-            <Col xs={24}>
+            <Col xs={24} md={12}>
               <Card 
                 title={
                   <span>
-                    <UserOutlined style={{ marginRight: 8 }} />
-                    人员按供应商收入明细
+                    <TrophyOutlined style={{ marginRight: 8 }} />
+                    业绩排名
                   </span>
                 }
                 size="small"
               >
-                {monthlySummary?.supplier_breakdown?.length > 0 ? (
+                {monthlySummary?.top_performers?.length > 0 ? (
                   <Table
-                    columns={supplierBreakdownColumns}
-                    dataSource={monthlySummary.supplier_breakdown}
-                    rowKey={(record) => `${record.person_id}-${record.supplier_name}-${record.is_settled}`}
+                    columns={topPerformersColumns}
+                    dataSource={monthlySummary.top_performers}
+                    rowKey="person_id"
                     pagination={false}
                     size="small"
                     tableLayout="auto"
-                    rowClassName={(record) => record.is_settled === false ? 'unsettled-row' : ''}
                   />
                 ) : (
                   <Empty description="暂无数据" />
@@ -500,6 +559,7 @@ function Dashboard() {
               </Card>
             </Col>
           </Row>
+
         </Spin>
       </Card>
 

+ 26 - 1
frontend/src/components/WorkRecordList.jsx

@@ -22,6 +22,7 @@ function WorkRecordList() {
   const [selectedDate, setSelectedDate] = useState(null)
   const [selectedMonth, setSelectedMonth] = useState(null)
   const [selectedSettlement, setSelectedSettlement] = useState(null)
+  const [selectedSupplierId, setSelectedSupplierId] = useState(null)
   
   // Pagination states
   const [currentPage, setCurrentPage] = useState(1)
@@ -83,6 +84,9 @@ function WorkRecordList() {
       if (selectedSettlement !== null) {
         params.is_settled = selectedSettlement
       }
+      if (selectedSupplierId !== null) {
+        params.supplier_id = selectedSupplierId
+      }
       const response = await workRecordApi.getAll(params)
       setWorkRecords(response.data || [])
       // Update pagination from backend response
@@ -105,7 +109,7 @@ function WorkRecordList() {
 
   useEffect(() => {
     fetchWorkRecords()
-  }, [selectedPersonId, selectedDate, selectedMonth, selectedSettlement, currentPage, pageSize])
+  }, [selectedPersonId, selectedDate, selectedMonth, selectedSettlement, selectedSupplierId, currentPage, pageSize])
 
   // Reset to page 1 when filter conditions change
   const handleFilterChange = (setter) => (value) => {
@@ -162,12 +166,14 @@ function WorkRecordList() {
       selectedDate === null && 
       selectedMonth === null && 
       selectedSettlement === null &&
+      selectedSupplierId === null &&
       currentPage === 1
     
     setSelectedPersonId(null)
     setSelectedDate(null)
     setSelectedMonth(null)
     setSelectedSettlement(null)
+    setSelectedSupplierId(null)
     setCurrentPage(1)
     
     // If already at default, manually refresh the table
@@ -392,6 +398,25 @@ function WorkRecordList() {
             ]}
           />
         </Col>
+        <Col xs={12} sm={6} md={4}>
+          <Select
+            placeholder="选择供应商"
+            allowClear
+            showSearch={false}
+            style={{ width: '100%' }}
+            value={selectedSupplierId}
+            onChange={handleFilterChange(setSelectedSupplierId)}
+          >
+            <Select.Option key="none" value="none">
+              无供应商
+            </Select.Option>
+            {suppliers.map(s => (
+              <Select.Option key={s.id} value={s.id}>
+                {s.name}
+              </Select.Option>
+            ))}
+          </Select>
+        </Col>
         <Col xs={12} sm={6} md={4}>
           <Button icon={<ReloadOutlined />} onClick={handleReset} style={{ width: isMobile ? '100%' : 'auto' }}>
             重置