diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 71768e7..256e2bb 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine AS build +FROM docker.m.daocloud.io/library/node:20-alpine AS build WORKDIR /app @@ -10,7 +10,7 @@ ARG VITE_API_BASE_URL= ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} RUN npm run build -FROM nginx:1.27-alpine +FROM docker.m.daocloud.io/library/nginx:1.27-alpine COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/dist /usr/share/nginx/html diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..ced3272 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,53 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + client_max_body_size 50m; + + location /api/ { + proxy_pass http://backend:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /health { + proxy_pass http://backend:8000/health; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /docs { + proxy_pass http://backend:8000/docs; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /openapi.json { + proxy_pass http://backend:8000/openapi.json; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /static/ { + proxy_pass http://backend:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/package.json b/frontend/package.json index f019e5b..603bf76 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,9 +4,9 @@ "private": true, "type": "module", "scripts": { - "dev": "vite --host 127.0.0.1 --port 5173", + "dev": "vite --host 0.0.0.0 --port 5173", "build": "vue-tsc --noEmit && vite build", - "preview": "vite preview --host 127.0.0.1 --port 4173" + "preview": "vite preview --host 0.0.0.0 --port 4173" }, "dependencies": { "@lucide/vue": "1.18.0", diff --git a/frontend/src/api/employees.ts b/frontend/src/api/employees.ts index 5b187b2..2f72cb0 100644 --- a/frontend/src/api/employees.ts +++ b/frontend/src/api/employees.ts @@ -1,7 +1,9 @@ import { apiFetch } from "./http"; import type { EmployeeListResponse, EmployeeNoResponse, EmployeeRecord, EmployeeRequest } from "./types"; -export function listEmployees(params: { keyword?: string; employment_status?: string } = {}): Promise { +export function listEmployees( + params: { keyword?: string; employee_keyword?: string; organization_keyword?: string; employment_status?: string } = {}, +): Promise { const search = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null && String(value).trim() !== "") { @@ -13,7 +15,14 @@ export function listEmployees(params: { keyword?: string; employment_status?: st } export function listEmployeePage( - params: { page?: number; page_size?: number; keyword?: string; employment_status?: string } = {}, + params: { + page?: number; + page_size?: number; + keyword?: string; + employee_keyword?: string; + organization_keyword?: string; + employment_status?: string; + } = {}, ): Promise { const search = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { diff --git a/frontend/src/api/monthlyPayroll.ts b/frontend/src/api/monthlyPayroll.ts index 781b2dc..d6700a1 100644 --- a/frontend/src/api/monthlyPayroll.ts +++ b/frontend/src/api/monthlyPayroll.ts @@ -10,24 +10,31 @@ export function getMonthlyRun(runId: number): Promise(`/api/payroll/monthly/runs/${runId}`); } -export function calculateMonthlyFromExcel(file: File, salaryMonth: string): Promise { +export function calculateMonthlyFromExcel( + file: File, + salaryMonth: string, + exportExcel = true, +): Promise { const formData = new FormData(); formData.append("file", file); formData.append("salary_month", salaryMonth); - formData.append("export_excel", "true"); + formData.append("export_excel", String(exportExcel)); return apiFetch("/api/payroll/monthly/excel", { method: "POST", body: formData, }); } -export function calculateMonthlyFromDingTalk(salaryMonth: string): Promise { +export function calculateMonthlyFromDingTalk( + salaryMonth: string, + exportExcel = true, +): Promise { return apiFetch("/api/payroll/monthly/dingtalk", { method: "POST", body: JSON.stringify({ salary_month: salaryMonth, source_type: "dingtalk", - export_excel: true, + export_excel: exportExcel, }), }); } diff --git a/frontend/src/api/payroll.ts b/frontend/src/api/payroll.ts index de560fc..4d6f4a2 100644 --- a/frontend/src/api/payroll.ts +++ b/frontend/src/api/payroll.ts @@ -1,7 +1,6 @@ import { ApiError, apiFetch, buildApiUrl, getStoredToken } from "./http"; import type { DingTalkPayrollRequest, - PayrollJobResponse, PayrollResponse, RecentPayrollJob, } from "./types"; @@ -33,10 +32,6 @@ export function calculateFromDingTalk(payload: DingTalkPayrollRequest): Promise< }); } -export function getPayrollJob(jobId: string): Promise { - return apiFetch(`/api/payroll/jobs/${encodeURIComponent(jobId)}`); -} - export function getDownloadUrl(downloadUrl: string | null, outputFile?: string | null): string { if (downloadUrl) { return buildApiUrl(downloadUrl); diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 9991cc3..f5953e1 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -106,19 +106,6 @@ export interface PayrollResponse { results: PayrollResult[]; } -export interface PayrollJobResponse { - job_id: string; - source_type: string; - status: string; - input_file: string | null; - output_file: string | null; - employee_count: number; - error_message: string | null; - created_at: string; - updated_at: string; - results: PayrollResult[]; -} - export interface DingTalkPayrollRequest { user_ids: string[]; start_date: string; diff --git a/frontend/src/assets/login-background.png b/frontend/src/assets/login-background.png new file mode 100644 index 0000000..07dd0d7 Binary files /dev/null and b/frontend/src/assets/login-background.png differ diff --git a/frontend/src/assets/styles.css b/frontend/src/assets/styles.css index f9448cf..a93eb6a 100644 --- a/frontend/src/assets/styles.css +++ b/frontend/src/assets/styles.css @@ -70,11 +70,14 @@ html, body, #app { + max-width: 100%; min-height: 100%; + overflow-x: hidden; } body { margin: 0; + overflow-x: hidden; background: var(--surface); color: var(--text); font-size: var(--base-font-size); @@ -1438,6 +1441,35 @@ a { flex-wrap: wrap; } +.realtime-page-header { + align-items: flex-end; +} + +.realtime-header-actions { + display: grid; + grid-template-columns: minmax(210px, 242px) 140px; + align-items: end; + justify-content: end; + gap: 10px; + flex-wrap: nowrap; +} + +.realtime-date-field { + min-width: 0; + margin: 0; +} + +.realtime-date-field input { + width: 100%; +} + +.realtime-header-actions .primary-button, +.realtime-header-actions .secondary-button { + min-height: 38px; + padding: 0 14px; + white-space: nowrap; +} + .align-right { justify-content: flex-end; } @@ -1625,49 +1657,195 @@ a { color: var(--muted); } +.commissions-page { + gap: 14px; +} + +.commission-summary-strip { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + padding: 8px; +} + +.commission-summary-strip .summary-metric { + min-height: 58px; + min-width: 0; + flex: unset; + padding: 8px 12px; +} + +.commission-summary-strip .summary-icon { + width: 32px; + height: 32px; +} + +.commission-summary-strip .summary-copy strong { + font-size: 20px; +} + +.commission-summary-strip .summary-meta { + justify-self: end; +} + +.commission-list-panel { + overflow: hidden; +} + +.commission-list-header { + align-items: center; +} + +.commission-filter-bar { + grid-template-columns: minmax(220px, 420px) 180px 104px minmax(0, 1fr); + align-items: end; +} + +.commission-keyword-field { + min-width: 0; +} + +.commission-month-field { + width: 180px; +} + +.commission-search-button { + width: 104px; + min-height: 36px; + justify-self: start; + padding: 0 14px; +} + +.commission-action-cell { + text-align: right; + white-space: nowrap; +} + +.commission-row-actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 6px; +} + +.table-action-icon { + width: 34px; + height: 34px; + border: 1px solid var(--line-soft); + border-radius: var(--radius-md); + background: var(--card); + color: var(--blue); +} + +.table-action-icon:hover { + border-color: rgba(var(--accent-rgb), 0.28); + background: var(--nav-active-bg); + color: var(--blue); +} + +.table-action-icon.danger { + color: var(--red); +} + +.table-action-icon.danger:hover { + border-color: var(--red-soft); + background: var(--red-soft); + color: var(--red); +} + +.commission-modal-backdrop { + align-items: start; +} + +.commission-modal { + width: min(900px, 100%); + max-height: min(720px, calc(100vh - 48px)); +} + +.commission-form { + padding: 14px var(--panel-padding) var(--panel-padding); +} + +.commission-form-grid { + display: grid; + grid-template-columns: minmax(220px, 1.2fr) minmax(160px, 0.75fr) minmax(190px, 1fr) minmax(150px, 0.7fr); + align-items: end; + gap: 10px 14px; +} + +.commission-business-field { + grid-column: 1 / 2; +} + +.commission-remark-field { + grid-column: 2 / 4; +} + +.commission-form-actions { + grid-column: 3 / 5; + align-self: end; + justify-content: flex-end; + margin: 0; +} + +.commission-modal-message { + grid-column: 1 / -1; + margin: 0; +} + +.commission-form-actions .primary-button { + min-width: 138px; +} + .status-chip.green { background: var(--green-soft); color: var(--green); } +.status-chip.warning { + background: var(--red-soft); + color: var(--red); +} + .status-chip.red { background: var(--red-soft); color: var(--red); } -.employee-header-actions { - display: flex; +.employees-page { + gap: 14px; +} + +.employee-list-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; +} + +.employee-list-title { + display: flex; + min-width: 0; align-items: center; - justify-content: flex-end; gap: 12px; + flex-wrap: wrap; } .employee-inline-metric { - display: flex; - min-height: 42px; + display: inline-flex; + min-height: 30px; align-items: center; - gap: 8px; - border: var(--card-inner-border); - border-radius: var(--radius-md); - background: var(--card-inner-bg); - box-shadow: var(--card-inner-shadow); + gap: 7px; + border: 1px solid var(--line-soft); + border-radius: 999px; + background: var(--surface); + box-shadow: none; color: var(--primary); - padding: 6px 10px; + padding: 0 10px; + white-space: nowrap; } -.employee-inline-icon { - display: grid; - width: 30px; - height: 30px; - place-items: center; - border-radius: var(--radius-sm); - background: var(--blue-soft); - color: var(--blue); -} - -.employee-inline-metric div { - display: grid; - gap: 1px; +.employee-inline-metric svg { + color: var(--muted); } .employee-inline-metric span { @@ -1678,21 +1856,58 @@ a { .employee-inline-metric strong { color: var(--primary); - font-size: 18px; + font-size: 17px; line-height: 1; } .employee-inline-metric em { - border-radius: var(--radius-sm); - background: var(--surface-soft); color: var(--muted); - padding: 3px 7px; font-size: 12px; font-style: normal; font-weight: 800; +} + +.employee-filter-bar { + grid-template-columns: minmax(180px, 260px) minmax(180px, 260px) minmax(130px, 160px) 104px minmax(0, 1fr); +} + +.employee-search-button { + width: 104px; + min-height: 36px; + justify-self: start; + padding: 0 14px; +} + +.employee-status-pill { + display: inline-flex; + min-height: 22px; + min-width: 0; + align-items: center; + gap: 7px; + background: transparent; + color: var(--muted); + padding: 0 2px; + font-size: 13px; + font-weight: 800; + line-height: 1.2; white-space: nowrap; } +.employee-status-dot { + width: 7px; + height: 7px; + border-radius: 999px; + background: currentColor; +} + +.employee-status-pill.is-active { + color: var(--green); +} + +.employee-status-pill.is-inactive { + color: var(--red); +} + .dashboard-grid, .action-grid { display: grid; @@ -1717,6 +1932,37 @@ a { padding: 14px var(--panel-padding); } +.realtime-overview-panel { + overflow: hidden; +} + +.realtime-overview-header { + align-items: center; + padding: 12px var(--panel-padding) 10px; +} + +.realtime-overview-title { + display: flex; + min-width: 0; + align-items: baseline; + gap: 10px; + flex-wrap: wrap; +} + +.realtime-overview-title h2, +.realtime-overview-title p { + margin: 0; +} + +.realtime-overview-title p { + font-size: 14px; +} + +.realtime-sync-chip { + min-width: 96px; + justify-content: center; +} + .panel-header h2, .action-card h2 { margin: 0; @@ -1762,6 +2008,43 @@ a { text-align: right; } +.monthly-technical-panel { + border-style: dashed; + background: var(--surface); +} + +.monthly-technical-panel summary { + cursor: pointer; + list-style: none; + padding: 12px var(--panel-padding); + color: var(--muted); + font-size: 13px; + font-weight: 800; +} + +.monthly-technical-panel summary::-webkit-details-marker { + display: none; +} + +.monthly-technical-panel summary::after { + content: "+"; + float: right; + color: var(--primary); + font-size: 16px; +} + +.monthly-technical-panel[open] summary::after { + content: "-"; +} + +.technical-note { + margin: 0; + border-top: 1px solid var(--line-soft); + padding: 0 var(--panel-padding) 4px; + color: var(--muted); + font-size: 12px; +} + .sync-panel .primary-button { margin: 0 var(--panel-padding) var(--panel-padding); width: calc(100% - (var(--panel-padding) * 2)); @@ -1843,6 +2126,497 @@ a { padding: calc(var(--panel-padding) + 4px); } +.source-workflow { + display: grid; + gap: 10px; +} + +.section-title-row { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 12px; +} + +.section-title-row h2 { + margin: 0; + color: var(--primary); + font-size: 18px; + line-height: 1.25; +} + +.section-title-row p { + margin: 4px 0 0; + color: var(--muted); + font-size: 13px; +} + +.source-strip { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--content-gap); +} + +.source-option { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + min-height: 106px; + border: var(--card-border); + border-radius: var(--radius-lg); + background: var(--card); + box-shadow: var(--card-shadow); + padding: 14px var(--panel-padding); +} + +.source-option-main { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.source-option h2 { + margin: 0; + color: var(--primary); + font-size: 17px; + line-height: 1.25; +} + +.source-option p { + margin: 5px 0 0; + color: var(--muted); + font-size: 13px; + line-height: 1.45; +} + +.source-note { + display: block; + margin-top: 7px; + color: var(--muted); + font-size: 12px; + font-weight: 700; + line-height: 1.35; +} + +.source-note.warning { + color: var(--red); +} + +.source-option .primary-button, +.source-option .secondary-button, +.source-option .file-button { + flex: 0 0 auto; + white-space: nowrap; +} + +.source-actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex-wrap: wrap; +} + +.monthly-command-panel { + overflow: hidden; +} + +.monthly-command-header { + align-items: center; + padding-bottom: 12px; +} + +.monthly-settings-grid { + grid-template-columns: minmax(150px, 0.75fr) minmax(190px, 1fr) minmax(260px, 1.4fr) minmax(170px, 0.75fr); + border-top: 1px solid var(--line-soft); + padding-block: 12px; +} + +.monthly-source-strip { + gap: 10px; + padding: 0 var(--panel-padding) 12px; +} + +.monthly-source-option { + min-height: 88px; + border-color: var(--line-soft); + background: var(--surface); + box-shadow: none; + padding: 12px; +} + +.monthly-source-option .action-icon { + width: 42px; + height: 42px; +} + +.monthly-source-option h2 { + font-size: 15px; +} + +.monthly-source-option p { + margin-top: 3px; + font-size: 12px; +} + +.monthly-source-option .source-note { + margin-top: 4px; + font-size: 11px; +} + +.monthly-stats-grid { + grid-template-columns: repeat(6, minmax(0, 1fr)); + padding-top: 0; + padding-bottom: 12px; +} + +.monthly-final-actions { + justify-content: flex-end; + border-top: 1px solid var(--line-soft); + padding: 12px var(--panel-padding) var(--panel-padding); +} + +.payroll-compact-toolbar { + display: grid; + grid-template-columns: minmax(190px, 240px) minmax(0, 1fr) auto; + align-items: stretch; + gap: 14px; + min-width: 0; + padding: 12px var(--panel-padding); +} + +.payroll-toolbar-month { + display: grid; + align-content: start; + gap: 8px; + min-width: 0; +} + +.payroll-toolbar-month > span, +.payroll-toolbar-summary span, +.payroll-toolbar-summary small, +.payroll-detail-summary span { + color: var(--muted); + font-size: 12px; + font-weight: 800; +} + +.payroll-toolbar-month .field { + margin: 0; +} + +.payroll-toolbar-summary { + display: grid; + grid-template-columns: minmax(110px, 0.7fr) minmax(110px, 0.7fr) minmax(110px, 0.7fr) minmax(270px, 1.45fr); + gap: 0; + min-width: 0; + border-inline: 1px solid var(--line-soft); +} + +.payroll-toolbar-summary div, +.payroll-detail-summary div { + display: grid; + align-content: center; + gap: 4px; + min-width: 0; + border-right: 1px solid var(--line-soft); + padding: 0 14px; +} + +.payroll-toolbar-summary div:last-child, +.payroll-detail-summary div:last-child { + border-right: 0; +} + +.payroll-toolbar-summary strong, +.payroll-detail-summary strong { + min-width: 0; + overflow: hidden; + color: var(--primary); + font-size: 16px; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.payroll-toolbar-summary small { + min-width: 0; + overflow: hidden; + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; +} + +.payroll-toolbar-range strong, +.payroll-toolbar-range small { + overflow: visible; + text-overflow: clip; + white-space: normal; +} + +.payroll-toolbar-range strong { + font-size: 15px; +} + +.payroll-toolbar-actions { + display: grid; + grid-template-columns: repeat(2, 138px); + align-content: start; + justify-content: end; + gap: 8px; + min-width: 0; +} + +.payroll-toolbar-actions .primary-button, +.payroll-toolbar-actions .secondary-button { + min-height: 34px; + padding: 0 10px; + font-size: 13px; + white-space: nowrap; +} + +.payroll-toolbar-actions svg { + width: 16px; + height: 16px; +} + +.payroll-toolbar-actions .link-button { + grid-column: 1 / -1; + justify-self: end; + min-height: 30px; +} + +.payroll-toolbar-export { + display: inline-flex; + grid-column: 1 / -1; + align-items: center; + justify-self: start; + gap: 8px; + min-height: 30px; + border: 1px solid var(--line-soft); + border-radius: var(--radius-md); + background: var(--surface); + color: var(--muted); + padding: 0 10px; + font-size: 12px; + font-weight: 800; + white-space: nowrap; +} + +.payroll-toolbar-export input { + width: 15px; + height: 15px; + accent-color: var(--blue); +} + +.payroll-detail-panel { + overflow: hidden; + border-color: rgba(var(--accent-rgb), 0.2); +} + +.payroll-detail-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 18px; + border-bottom: 1px solid var(--line-soft); + padding: var(--panel-padding); +} + +.payroll-detail-header h2 { + margin: 0; + color: var(--primary); + font-size: 22px; + line-height: 1.25; +} + +.payroll-detail-header p { + margin: 5px 0 0; + color: var(--muted); + font-size: 13px; + line-height: 1.45; +} + +.payroll-detail-summary { + display: grid; + grid-template-columns: repeat(4, minmax(110px, auto)); + align-items: stretch; + min-width: 0; + border: 1px solid var(--line-soft); + border-radius: var(--radius-md); + background: var(--surface); + padding: 10px 0; +} + +.salary-detail-table-shell { + min-width: 0; +} + +.salary-summary-toolbar { + display: grid; + grid-template-columns: minmax(220px, 1fr) repeat(3, minmax(140px, 180px)); + gap: 10px; + border-bottom: 1px solid var(--line-soft); + padding: 12px var(--panel-padding); +} + +.salary-summary-toolbar .field { + margin: 0; +} + +.salary-summary-layout { + display: grid; + min-width: 0; +} + +.salary-summary-table { + min-width: 860px; + font-size: 13px; +} + +.salary-summary-table th, +.salary-summary-table td { + padding: 12px 14px; + vertical-align: middle; +} + +.salary-summary-table tbody tr { + cursor: pointer; +} + +.salary-summary-table tbody tr.has-warning td { + background: rgba(var(--red-rgb), 0.035); +} + +.salary-name-button { + min-height: auto; + padding: 0; +} + +.salary-anomaly-note { + display: block; + margin-top: 5px; + color: var(--muted); + font-size: 12px; + line-height: 1.35; +} + +.salary-detail-drawer { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + border-top: 1px solid var(--line-soft); + background: var(--surface); + padding: 14px var(--panel-padding); +} + +.salary-detail-drawer-header { + display: flex; + grid-column: 1 / -1; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + border-bottom: 1px solid var(--line-soft); + padding-bottom: 10px; +} + +.salary-detail-drawer-header span, +.salary-detail-drawer > div:not(.salary-detail-drawer-header) span { + color: var(--muted); + font-size: 12px; + font-weight: 800; +} + +.salary-detail-drawer-header h3 { + margin: 3px 0 0; + color: var(--primary); + font-size: 18px; + line-height: 1.25; +} + +.salary-detail-drawer-header p { + margin: 4px 0 0; + color: var(--muted); + font-size: 13px; +} + +.salary-detail-drawer > div:not(.salary-detail-drawer-header) { + display: grid; + gap: 4px; + min-width: 0; + border: 1px solid var(--line-soft); + border-radius: var(--radius-md); + background: var(--card); + padding: 10px 12px; +} + +.salary-detail-drawer > div:not(.salary-detail-drawer-header) strong { + min-width: 0; + overflow: hidden; + color: var(--primary); + font-size: 14px; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.salary-detail-note { + grid-column: 1 / -1; + margin: 0; + border: 1px solid var(--line-soft); + border-radius: var(--radius-md); + background: var(--card); + color: var(--muted); + padding: 10px 12px; + font-size: 13px; + line-height: 1.45; +} + +.salary-detail-table { + font-size: 13px; + min-width: 1560px; +} + +.salary-detail-table th, +.salary-detail-table td { + padding: 10px 12px; + vertical-align: middle; +} + +.salary-detail-table td { + height: 54px; +} + +.salary-detail-table td strong { + font-size: 14px; +} + +.salary-detail-table th, +.salary-detail-table td:not(.note-cell) { + white-space: nowrap; +} + +.salary-detail-table .note-cell { + min-width: 150px; + max-width: 180px; + line-height: 1.45; +} + +.payroll-secondary-grid { + display: grid; + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); + align-items: start; + gap: var(--content-gap); + min-width: 0; +} + +.monthly-history-panel, +.payroll-exception-panel { + min-width: 0; +} + .action-icon { width: 50px; height: 50px; @@ -1878,6 +2652,47 @@ a { padding: var(--panel-padding); } +.settings-grid { + display: grid; + grid-template-columns: minmax(180px, 0.85fr) minmax(220px, 1fr) minmax(280px, 1.35fr) minmax(190px, 0.9fr); + gap: 12px; + padding: var(--panel-padding); +} + +.readonly-field { + display: grid; + align-content: center; + gap: 6px; + min-height: var(--control-height); + border: 1px solid var(--line); + border-radius: var(--radius-md); + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); + padding: 8px 12px; +} + +.readonly-field span { + color: var(--muted); + font-size: 12px; + font-weight: 800; +} + +.readonly-field strong { + min-width: 0; + overflow: hidden; + color: var(--primary); + font-size: 14px; + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; +} + +.secondary-file-button { + border: 1px solid var(--line); + background: var(--control-bg); + color: var(--blue); +} + .compact-form { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -1902,7 +2717,6 @@ a { .salary-profile-summary-item, .organization-summary-strip div, .organization-node-summary div, -.employee-inline-metric, .workflow-steps span, .recent-item, .salary-profile-modal-section, @@ -1937,6 +2751,50 @@ a { line-height: 1.2; } +.realtime-overview-stats { + padding-top: 0; +} + +.realtime-overview-stats .compact-stat { + padding: 12px 16px; +} + +.realtime-overview-stats .compact-stat strong { + margin-top: 6px; + font-size: 24px; + line-height: 1.05; +} + +.realtime-table-filter-bar { + display: grid; + grid-template-columns: minmax(180px, 220px) minmax(240px, 1fr) 78px auto; + align-items: end; + gap: 10px; + border-bottom: 1px solid var(--line-soft); + background: var(--surface); + padding: 10px var(--panel-padding); +} + +.realtime-table-filter-bar .field { + margin: 0; +} + +.realtime-clear-filter { + min-height: var(--control-height); + width: 78px; + justify-self: start; + padding: 0 12px; + white-space: nowrap; +} + +.realtime-filter-result { + justify-self: end; + color: var(--muted); + font-size: 12px; + font-weight: 800; + white-space: nowrap; +} + .salary-profile-page { gap: 14px; } @@ -2612,6 +3470,53 @@ a { text-align: center; } +.users-page { + gap: 14px; +} + +.user-list-panel { + overflow: hidden; +} + +.user-list-header { + align-items: center; +} + +.user-modal-backdrop { + align-items: start; +} + +.user-modal { + width: min(920px, 100%); + max-height: min(760px, calc(100vh - 48px)); +} + +.user-modal-form { + padding: 14px var(--panel-padding) var(--panel-padding); +} + +.user-modal-fields { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + align-items: end; + gap: 10px 14px; +} + +.user-modal-message { + grid-column: 1 / -1; + margin: 0; +} + +.user-modal-actions { + grid-column: 1 / -1; + justify-content: flex-end; + margin: 0; +} + +.user-modal-actions .primary-button { + min-width: 132px; +} + .user-form { grid-template-columns: repeat(4, minmax(0, 1fr)); } @@ -2654,12 +3559,206 @@ a { padding: 0 12px; } +.date-picker { + position: relative; + width: 100%; + min-width: 0; +} + +.date-picker-trigger { + display: flex; + min-height: var(--control-height); + width: 100%; + align-items: center; + justify-content: space-between; + gap: 10px; + border: 1px solid var(--line); + border-radius: var(--radius-md); + background: var(--control-bg); + color: var(--text); + padding: 0 12px; + text-align: left; + transition: + border-color 0.18s ease, + box-shadow 0.18s ease, + background-color 0.18s ease; +} + +.date-picker-trigger span { + min-width: 0; + overflow: hidden; + color: var(--text); + font-size: 14px; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.date-picker-trigger span.muted { + color: var(--muted); + font-weight: 600; +} + +.date-picker-trigger svg { + flex: 0 0 auto; + color: var(--muted); +} + +.date-picker-trigger:hover, +.date-picker.is-open .date-picker-trigger { + border-color: rgba(var(--accent-rgb), 0.42); + box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.1); +} + +.date-picker.is-disabled { + opacity: 0.66; +} + +.date-picker-panel { + position: fixed; + z-index: 1100; + width: min(292px, calc(100vw - 32px)); + max-height: min(420px, calc(100vh - 24px)); + overflow: auto; + border: 1px solid var(--line); + border-radius: var(--radius-lg); + background: var(--card); + box-shadow: var(--shadow); + padding: 12px; + overscroll-behavior: contain; +} + +.date-picker-panel-head { + display: grid; + grid-template-columns: 34px minmax(0, 1fr) 34px; + align-items: center; + gap: 8px; + margin-bottom: 10px; +} + +.date-picker-panel-head strong { + color: var(--primary); + font-size: 15px; + text-align: center; +} + +.date-picker-nav { + display: grid; + width: 34px; + height: 34px; + place-items: center; + border: 1px solid var(--line-soft); + border-radius: var(--radius-md); + background: var(--surface); + color: var(--muted); +} + +.date-picker-nav:hover { + border-color: rgba(var(--accent-rgb), 0.32); + background: var(--nav-active-bg); + color: var(--blue); +} + +.month-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; +} + +.calendar-shell { + display: grid; + gap: 8px; +} + +.calendar-week-row, +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 6px; +} + +.calendar-week-row span { + color: var(--muted); + font-size: 12px; + font-weight: 800; + text-align: center; +} + +.date-picker-cell { + min-height: 34px; + border: 1px solid transparent; + border-radius: var(--radius-md); + background: var(--surface); + color: var(--text); + font-size: 13px; + font-weight: 800; +} + +.date-picker-cell:hover { + border-color: rgba(var(--accent-rgb), 0.28); + background: var(--nav-active-bg); + color: var(--blue); +} + +.date-picker-cell.active { + border-color: var(--blue); + background: var(--blue); + color: white; + box-shadow: 0 8px 16px rgba(var(--accent-rgb), 0.18); +} + +.date-picker-cell.today:not(.active) { + color: var(--blue); +} + +.date-picker-cell.muted:not(.active) { + color: var(--muted); + opacity: 0.62; +} + +.calendar-day { + min-height: 32px; + padding: 0; +} + +.date-picker-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + border-top: 1px solid var(--line-soft); + margin-top: 12px; + padding-top: 10px; +} + +.date-picker-text-button { + min-height: 30px; + border: 0; + background: transparent; + color: var(--muted); + padding: 0 6px; + font-size: 13px; + font-weight: 800; +} + +.date-picker-text-button:hover, +.date-picker-text-button.primary { + color: var(--blue); +} + .field textarea { min-height: 96px; resize: vertical; padding: 10px 12px; } +.commission-remark-field textarea { + min-height: 44px; + max-height: 76px; + resize: vertical; + padding: 8px 12px; +} + .field input:focus, .field select:focus, .field textarea:focus, @@ -2717,6 +3816,30 @@ a { min-height: 36px; } +.commission-filter-bar { + grid-template-columns: minmax(220px, 420px) 180px 104px minmax(0, 1fr); + align-items: end; +} + +.commission-keyword-field { + min-width: 0; +} + +.commission-month-field { + width: 180px; +} + +.commission-search-button { + width: 104px; + min-height: 36px; + justify-self: start; + padding: 0 14px; +} + +.table-filter-bar.employee-filter-bar { + grid-template-columns: minmax(180px, 260px) minmax(180px, 260px) minmax(130px, 160px) 104px minmax(0, 1fr); +} + .table-pagination { display: flex; align-items: center; @@ -2769,7 +3892,10 @@ a { } .table-wrap { + max-width: 100%; + min-width: 0; width: 100%; + contain: inline-size; overflow-x: auto; } @@ -2810,6 +3936,10 @@ a { background: var(--table-row-hover-bg); } +.data-table tbody tr.is-selected td { + background: rgba(var(--accent-rgb), 0.08); +} + .data-table td strong { display: block; color: var(--primary); @@ -2822,6 +3952,123 @@ a { font-size: 12px; } +.rules-panel .panel-header { + align-items: flex-start; +} + +.rules-table { + min-width: 920px; +} + +.rules-table th:nth-child(1) { + width: 22%; +} + +.rules-table th:nth-child(2) { + width: 30%; +} + +.rules-table th:nth-child(3) { + width: 12%; +} + +.rules-table th:nth-child(4) { + width: 26%; +} + +.rules-table th:nth-child(5) { + width: 10%; +} + +.rule-name-cell strong { + font-size: 14px; +} + +.rule-group-chip { + display: inline-flex !important; + width: fit-content; + margin-top: 8px !important; + border: 1px solid rgba(var(--accent-rgb), 0.18); + border-radius: 999px; + background: rgba(var(--accent-rgb), 0.08); + padding: 3px 8px; + color: var(--primary) !important; + font-size: 12px; + font-weight: 800; +} + +.rule-value-cell { + min-width: 240px; +} + +.rule-value-preview { + display: block; + margin: 0 0 8px !important; + color: var(--primary) !important; + font-size: 13px !important; + font-weight: 800; +} + +.rule-value-input { + width: min(360px, 100%); +} + +.rule-value-cell .table-textarea { + min-height: 42px; +} + +.weekday-toggle-group { + display: flex; + max-width: 380px; + gap: 6px; + flex-wrap: wrap; +} + +.weekday-toggle { + position: relative; + display: inline-flex; + min-height: 30px; + align-items: center; + border: 1px solid var(--line); + border-radius: 999px; + background: var(--control-bg); + color: var(--muted); + padding: 0 10px; + font-size: 12px; + font-weight: 800; + transition: + border-color var(--motion-duration-fast) ease, + background var(--motion-duration-fast) ease, + color var(--motion-duration-fast) ease; +} + +.weekday-toggle input { + position: absolute; + inset: 0; + margin: 0; + cursor: pointer; + opacity: 0; +} + +.weekday-toggle span { + margin: 0 !important; + color: inherit !important; +} + +.weekday-toggle:hover, +.weekday-toggle.is-selected { + border-color: rgba(var(--accent-rgb), 0.36); + background: rgba(var(--accent-rgb), 0.1); + color: var(--primary); +} + +.rule-purpose p { + max-width: 360px; + margin: 0; + color: var(--muted); + line-height: 1.6; +} + .mono { font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace; } @@ -2866,6 +4113,17 @@ a { padding: var(--panel-padding); } +.inline-alert { + margin: 12px var(--panel-padding) 0; + border: 1px solid var(--red-soft); + border-radius: var(--radius-md); + background: var(--red-soft); + color: var(--red); + padding: 10px 12px; + font-size: 13px; + font-weight: 700; +} + .recent-list { display: grid; gap: 10px; @@ -2959,20 +4217,27 @@ a { } .login-screen { + position: relative; display: grid; min-height: 100vh; - grid-template-columns: minmax(400px, 40vw) minmax(0, 1fr); + grid-template-columns: minmax(340px, 34vw) minmax(0, 1fr); + overflow: hidden; background: var(--surface); + isolation: isolate; } .login-form-side { + position: relative; + z-index: 1; display: grid; place-items: center; - padding: 42px 48px; + padding: 42px clamp(36px, 5vw, 84px); + background: linear-gradient(180deg, #f8fafc 0%, #f3f6fa 100%); + box-shadow: inset -1px 0 0 rgba(var(--primary-rgb), 0.08); } .login-card { - width: min(440px, 100%); + width: min(360px, 100%); } .login-brand { @@ -2982,7 +4247,7 @@ a { .login-brand h1 { margin: 18px 0 7px; color: var(--primary); - font-size: 26px; + font-size: 24px; line-height: 1.2; } @@ -3049,44 +4314,84 @@ a { .login-visual { position: relative; display: grid; - place-items: center; + align-items: center; + justify-items: start; overflow: hidden; - border-left: 1px solid var(--line); background: var(--surface-soft); color: var(--text); - padding: 44px; + padding: clamp(32px, 4.5vw, 58px); + isolation: isolate; +} + +.login-background-layer { + position: absolute; + inset: 0; + z-index: 0; + background-image: url("./login-background.png"); + background-position: 70% center; + background-size: cover; + filter: saturate(1.04) contrast(1.04); + transform: scale(1.02); +} + +.login-visual::before, +.login-visual::after { + position: absolute; + inset: 0; + content: ""; + pointer-events: none; +} + +.login-visual::before { + z-index: 1; + background: + linear-gradient(90deg, rgba(245, 247, 250, 0.9) 0%, rgba(245, 247, 250, 0.5) 40%, rgba(245, 247, 250, 0.14) 100%), + linear-gradient(180deg, rgba(255, 255, 255, 0.34) 0%, rgba(236, 243, 251, 0.16) 100%); +} + +.login-visual::after { + z-index: 2; + background: + radial-gradient(circle at 18% 18%, rgba(var(--accent-rgb), 0.14), transparent 30%), + linear-gradient(135deg, rgba(255, 255, 255, 0.28), transparent 52%); } .ledger-visual { - width: min(560px, 100%); - border: 1px solid var(--line); + position: relative; + z-index: 3; + width: min(360px, 100%); + margin-left: clamp(18px, 5vw, 78px); + transform: translateY(-8px); + border: 1px solid rgba(var(--primary-rgb), 0.12); border-radius: var(--radius-lg); background: var(--card); - padding: 32px; - box-shadow: var(--shadow); + background: rgba(255, 255, 255, 0.68); + backdrop-filter: blur(10px); + padding: 22px; + box-shadow: 0 14px 30px rgba(var(--primary-rgb), 0.08); } .ledger-visual h2 { - margin: 22px 0 14px; + margin: 16px 0 10px; color: var(--primary); - font-size: 31px; + font-size: 23px; line-height: 1.25; } .ledger-visual p { margin: 0; color: var(--muted); - font-size: 15px; - line-height: 1.65; + font-size: 13px; + line-height: 1.6; } .visual-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 18px; - margin-top: 28px; + gap: 12px; + margin-top: 18px; border-top: 1px solid var(--line); - padding-top: 22px; + padding-top: 14px; } .visual-metrics strong, @@ -3096,24 +4401,27 @@ a { .visual-metrics strong { color: var(--primary); - font-size: 25px; + font-size: 19px; } .visual-metrics span { - margin-top: 4px; + margin-top: 3px; color: var(--muted); + font-size: 12px; } .security-line { position: absolute; - right: 32px; - bottom: 28px; + right: 30px; + bottom: 24px; + z-index: 4; display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace; - font-size: 13px; + font-size: 12px; + opacity: 0.82; } @keyframes spin { @@ -3122,6 +4430,41 @@ a { } } +@media (max-width: 1320px) { + .realtime-header-actions { + grid-template-columns: 1fr; + } + + .payroll-compact-toolbar { + grid-template-columns: 1fr; + } + + .payroll-toolbar-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + border-inline: 0; + border-block: 1px solid var(--line-soft); + padding-block: 8px; + } + + .payroll-toolbar-actions { + justify-content: start; + } + + .payroll-secondary-grid { + grid-template-columns: 1fr; + } + + .commission-form-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .commission-business-field, + .commission-remark-field, + .commission-form-actions { + grid-column: auto; + } +} + @media (max-width: 1120px) { .appearance-panel { width: min(480px, calc(100vw - 16px)); @@ -3142,6 +4485,7 @@ a { .action-grid, .compact-stats-grid, .form-grid, + .settings-grid, .salary-profile-modal-fields, .workflow-steps, .password-form { @@ -3160,10 +4504,34 @@ a { grid-template-columns: 1fr; } + .payroll-detail-header { + grid-template-columns: 1fr; + } + + .payroll-detail-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .login-screen { grid-template-columns: 1fr; } + .login-screen::before { + position: absolute; + inset: 0; + z-index: 0; + background: + linear-gradient(180deg, rgba(245, 247, 250, 0.9) 0%, rgba(245, 247, 250, 0.84) 100%), + url("./login-background.png") center / cover no-repeat; + content: ""; + pointer-events: none; + } + + .login-form-side { + background: rgba(245, 247, 250, 0.74); + backdrop-filter: blur(2px); + } + .login-visual { display: none; } @@ -3193,6 +4561,55 @@ a { display: grid; } + .source-strip { + grid-template-columns: 1fr; + } + + .source-option { + align-items: stretch; + flex-direction: column; + } + + .source-actions { + justify-content: flex-start; + } + + .payroll-toolbar-summary, + .payroll-toolbar-actions, + .payroll-detail-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .payroll-toolbar-summary div, + .payroll-detail-summary div { + border-top: 1px solid var(--line-soft); + border-right: 0; + padding: 8px 10px; + } + + .payroll-toolbar-summary div:first-child, + .payroll-toolbar-summary div:nth-child(2), + .payroll-detail-summary div:first-child, + .payroll-detail-summary div:nth-child(2) { + border-top: 0; + } + + .payroll-toolbar-summary div:nth-child(odd), + .payroll-detail-summary div:nth-child(odd) { + border-right: 1px solid var(--line-soft); + } + + .payroll-toolbar-actions .primary-button, + .payroll-toolbar-actions .secondary-button, + .payroll-toolbar-actions .link-button { + width: 100%; + } + + .payroll-toolbar-export, + .payroll-toolbar-actions .link-button { + grid-column: 1 / -1; + } + .topbar { padding: 0 18px; } @@ -3215,6 +4632,64 @@ a { flex-direction: column; } + .realtime-overview-title { + align-items: flex-start; + flex-direction: column; + gap: 4px; + } + + .realtime-table-filter-bar { + grid-template-columns: 1fr; + } + + .realtime-filter-result { + justify-self: start; + } + + .commission-summary-strip, + .commission-form-grid, + .commission-filter-bar { + grid-template-columns: 1fr; + } + + .table-filter-bar.employee-filter-bar { + grid-template-columns: 1fr; + } + + .commission-month-field, + .commission-search-button { + width: 100%; + } + + .employee-search-button { + width: 100%; + } + + .commission-search-button { + justify-self: stretch; + } + + .employee-search-button { + justify-self: stretch; + } + + .commission-form-actions .primary-button { + width: 100%; + } + + .user-modal-fields { + grid-template-columns: 1fr; + } + + .user-modal-actions { + justify-content: stretch; + } + + .user-modal-actions .primary-button, + .user-modal-actions .secondary-button { + width: 100%; + } + .mode-grid, .font-grid, .sidebar-grid, @@ -3228,11 +4703,6 @@ a { width: 100%; } - .employee-header-actions { - justify-content: flex-start; - flex-wrap: wrap; - } - .employee-inline-metric { flex: 0 1 auto; } @@ -3278,6 +4748,7 @@ a { .action-grid, .compact-stats-grid, .form-grid, + .settings-grid, .salary-profile-modal-fields, .user-form, .password-form, @@ -3288,11 +4759,6 @@ a { grid-template-columns: 1fr; } - .employee-header-actions { - align-items: stretch; - flex-direction: column; - } - .employee-inline-metric { justify-content: flex-start; } @@ -3348,6 +4814,11 @@ a { padding: 12px; } + .payroll-compact-toolbar, + .payroll-detail-header { + padding: 12px; + } + .report-ledger-title { align-items: flex-start; flex-direction: column; diff --git a/frontend/src/components/ResultsTable.vue b/frontend/src/components/ResultsTable.vue index 9c9d821..0c4d4db 100644 --- a/frontend/src/components/ResultsTable.vue +++ b/frontend/src/components/ResultsTable.vue @@ -1,9 +1,72 @@