feat(考勤管理): 添加手动打卡记录功能和加班计算
- 在考勤记录页面添加手动打卡表单 - 实现后端API处理手动打卡记录 - 新增工人仪表盘显示姓名 - 在考勤报表中添加加班工资计算功能
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="attendance-reporting-layout">
|
||||
<!-- Left Panel: Worker Selection -->
|
||||
<div class="selection-panel">
|
||||
<section class="card">
|
||||
<section class="card" style="height: 70vh; display: flex; flex-direction: column">
|
||||
<h3 class="panel-header">1. Select Workers</h3>
|
||||
<div class="search-box">
|
||||
<input
|
||||
@@ -28,9 +27,12 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="selected-workers-list">
|
||||
<div
|
||||
class="selected-workers-list"
|
||||
style="flex-grow: 1; display: flex; flex-direction: column; overflow: hidden"
|
||||
>
|
||||
<h4>Selected for Report ({{ selectedWorkers.length }})</h4>
|
||||
<ul v-if="selectedWorkers.length > 0">
|
||||
<ul v-if="selectedWorkers.length > 0" style="flex-grow: 1; overflow-y: auto">
|
||||
<li v-for="worker in selectedWorkers" :key="worker.id">
|
||||
<span>{{ worker.full_name }}</span>
|
||||
<button @click="removeWorker(worker.id)" class="remove-btn">×</button>
|
||||
@@ -41,10 +43,9 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Filters & Results -->
|
||||
<div class="results-panel">
|
||||
<section class="card">
|
||||
<h3 class="panel-header">2. Set Filters & Generate</h3>
|
||||
<h3 class="panel-header">2. Report Settings</h3>
|
||||
<div class="filters">
|
||||
<div class="form-group">
|
||||
<label for="start-date">Start Date</label>
|
||||
@@ -54,17 +55,118 @@
|
||||
<label for="end-date">End Date</label>
|
||||
<input type="date" id="end-date" v-model="filters.endDate" class="form-input" />
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button @click="generateReport" :disabled="!canGenerate">Generate Report</button>
|
||||
<button @click="exportReport" :disabled="!canGenerate" class="button-secondary">
|
||||
Export (CSV)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overtime-settings-section" v-if="selectedWorkers.length > 0">
|
||||
<h4 class="settings-header">Overtime Settings</h4>
|
||||
<div class="worker-salaries">
|
||||
<h5>Monthly Salary (RM)</h5>
|
||||
<p class="subtitle">Enter the monthly salary to be applied to all selected workers.</p>
|
||||
<div class="form-group">
|
||||
<input
|
||||
id="monthly-salary"
|
||||
type="number"
|
||||
v-model.number="monthlySalary"
|
||||
class="form-input"
|
||||
placeholder="e.g., 3000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="factors-and-holidays">
|
||||
<div class="factor-input">
|
||||
<h5>OT Factors</h5>
|
||||
<div class="form-group">
|
||||
<label for="rest-day-factor">Rest Day OT Factor</label>
|
||||
<input
|
||||
id="rest-day-factor"
|
||||
type="number"
|
||||
v-model.number="overtimeSettings.restDayFactor"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="holiday-factor">Holiday Day OT Factor</label>
|
||||
<input
|
||||
id="holiday-factor"
|
||||
type="number"
|
||||
v-model.number="overtimeSettings.publicHolidayFactor"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="holiday-picker">
|
||||
<h5>Select Public Holidays</h5>
|
||||
<div class="calendar">
|
||||
<div class="calendar-header">
|
||||
<button @click="changeMonth(-1)">‹</button>
|
||||
<span>{{ calendarGrid.monthName }} {{ calendarGrid.year }}</span>
|
||||
<button @click="changeMonth(1)">›</button>
|
||||
</div>
|
||||
<div class="calendar-weekdays">
|
||||
<div v-for="day in calendarGrid.weekdayLabels" :key="day" class="weekday">
|
||||
{{ day }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-days">
|
||||
<div
|
||||
v-for="(day, index) in calendarGrid.grid"
|
||||
:key="index"
|
||||
class="day-cell"
|
||||
:class="{
|
||||
padding: day.type === 'padding',
|
||||
holiday: day.isHoliday,
|
||||
}"
|
||||
@click="day.type === 'day' && toggleHoliday(day.dateString)"
|
||||
>
|
||||
<span v-if="day.type === 'day'">{{ day.date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button @click="generateReport" :disabled="!canGenerate">
|
||||
Generate Attendance & OT Report
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" style="margin-top: 2rem" v-if="reportGenerated">
|
||||
<div class="results-display">
|
||||
<h4>Report Results</h4>
|
||||
<div v-if="loadingReport" class="loading-state">Loading...</div>
|
||||
<div v-else-if="reportData.length > 0">
|
||||
<div v-if="overtimeReport" class="overtime-report">
|
||||
<div class="report-header">
|
||||
<h4>Overtime Pay Summary</h4>
|
||||
<button
|
||||
@click="exportOtSummaryCsv"
|
||||
:disabled="!overtimeReport"
|
||||
class="button-primary"
|
||||
>
|
||||
Export OT Summary (CSV)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Worker</th>
|
||||
<th>Total Hours Worked</th>
|
||||
<th>Total OT Pay (RM)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(report, name) in overtimeReport" :key="name">
|
||||
<td>{{ name }}</td>
|
||||
<td>{{ report.totalHours.toFixed(2) }}</td>
|
||||
<td>{{ report.totalOtPay.toFixed(2) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="raw-logs" v-if="reportData.length > 0">
|
||||
<h4 style="margin-top: 2rem">Raw Attendance Data</h4>
|
||||
<div
|
||||
v-for="(group, workerName) in groupedReportData"
|
||||
:key="workerName"
|
||||
@@ -93,9 +195,11 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="empty-state">Generate a report to see results.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card" style="margin-top: 2rem; text-align: center" v-if="loadingReport">
|
||||
<p>Loading Report...</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -112,12 +216,31 @@ const selectedWorkers = ref([])
|
||||
const filters = ref({ startDate: '', endDate: '' })
|
||||
|
||||
const loadingReport = ref(false)
|
||||
const reportGenerated = ref(false)
|
||||
const reportData = ref([])
|
||||
const overtimeReport = ref(null)
|
||||
|
||||
// --- OT & SALARY STATE ---
|
||||
const monthlySalary = ref(null)
|
||||
const overtimeSettings = ref({
|
||||
restDayFactor: 2,
|
||||
publicHolidayFactor: 3,
|
||||
publicHolidays: [],
|
||||
})
|
||||
|
||||
// --- CALENDAR STATE ---
|
||||
const calendarDate = ref(new Date())
|
||||
|
||||
// --- COMPUTED ---
|
||||
const canGenerate = computed(
|
||||
() => selectedWorkers.value.length > 0 && filters.value.startDate && filters.value.endDate,
|
||||
)
|
||||
const canGenerate = computed(() => {
|
||||
return (
|
||||
selectedWorkers.value.length > 0 &&
|
||||
filters.value.startDate &&
|
||||
filters.value.endDate &&
|
||||
monthlySalary.value &&
|
||||
monthlySalary.value > 0
|
||||
)
|
||||
})
|
||||
|
||||
const groupedReportData = computed(() => {
|
||||
return reportData.value.reduce((groups, record) => {
|
||||
@@ -130,6 +253,39 @@ const groupedReportData = computed(() => {
|
||||
}, {})
|
||||
})
|
||||
|
||||
const calendarGrid = computed(() => {
|
||||
const year = calendarDate.value.getFullYear()
|
||||
const month = calendarDate.value.getMonth()
|
||||
const firstDayOfMonth = new Date(year, month, 1).getDay() // 0=Sun
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate()
|
||||
const grid = []
|
||||
const weekdayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
|
||||
for (let i = 0; i < firstDayOfMonth; i++) {
|
||||
grid.push({ type: 'padding' })
|
||||
}
|
||||
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const dateString = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(
|
||||
2,
|
||||
'0',
|
||||
)}`
|
||||
grid.push({
|
||||
type: 'day',
|
||||
date: day,
|
||||
dateString: dateString,
|
||||
isHoliday: overtimeSettings.value.publicHolidays.includes(dateString),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
grid,
|
||||
weekdayLabels,
|
||||
monthName: calendarDate.value.toLocaleString('default', { month: 'long' }),
|
||||
year,
|
||||
}
|
||||
})
|
||||
|
||||
// --- METHODS ---
|
||||
const handleSearch = () => {
|
||||
if (highlightedIndex.value >= 0 && searchResults.value[highlightedIndex.value]) {
|
||||
@@ -180,42 +336,197 @@ const removeWorker = (workerId) => {
|
||||
}
|
||||
|
||||
const generateReport = async () => {
|
||||
if (!canGenerate.value) return
|
||||
if (!canGenerate.value) {
|
||||
alert('Please select workers, set valid date range, and enter a salary.')
|
||||
return
|
||||
}
|
||||
loadingReport.value = true
|
||||
reportGenerated.value = false
|
||||
reportData.value = []
|
||||
overtimeReport.value = null
|
||||
|
||||
const workerIds = selectedWorkers.value.map((w) => w.id).join(',')
|
||||
const url = `http://localhost:3000/api/managers/attendance-records?workerIds=${workerIds}&startDate=${filters.value.startDate}&endDate=${filters.value.endDate}`
|
||||
|
||||
try {
|
||||
const res = await fetch(url)
|
||||
if (res.ok) {
|
||||
reportData.value = await res.json()
|
||||
if (!res.ok) throw new Error('Failed to fetch attendance records')
|
||||
|
||||
const fetchedRecords = await res.json()
|
||||
reportData.value = fetchedRecords
|
||||
|
||||
// --- OT CALCULATION FOR UI SUMMARY ---
|
||||
const otResults = {}
|
||||
const hourlyRate = monthlySalary.value / 26 / 8
|
||||
|
||||
for (const worker of selectedWorkers.value) {
|
||||
const workerRecords = fetchedRecords
|
||||
.filter((r) => r.full_name === worker.full_name)
|
||||
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
|
||||
|
||||
const dailyBreakdown = []
|
||||
let clockInTime = null
|
||||
|
||||
for (const record of workerRecords) {
|
||||
if (record.event_type === 'clock_in' && !clockInTime) {
|
||||
clockInTime = new Date(record.timestamp)
|
||||
} else if (record.event_type === 'clock_out' && clockInTime) {
|
||||
const clockOutTime = new Date(record.timestamp)
|
||||
const hours = (clockOutTime - clockInTime) / (1000 * 60 * 60)
|
||||
|
||||
if (hours > 0) {
|
||||
const date = clockInTime.toISOString().split('T')[0]
|
||||
const isPublicHoliday = overtimeSettings.value.publicHolidays.includes(date)
|
||||
const factor = isPublicHoliday
|
||||
? overtimeSettings.value.publicHolidayFactor
|
||||
: overtimeSettings.value.restDayFactor
|
||||
const otPay = hours * hourlyRate * factor
|
||||
dailyBreakdown.push({ hours, otPay })
|
||||
}
|
||||
clockInTime = null // Reset for the next session
|
||||
}
|
||||
}
|
||||
|
||||
const totalHours = dailyBreakdown.reduce((sum, day) => sum + day.hours, 0)
|
||||
const totalOtPay = dailyBreakdown.reduce((sum, day) => sum + day.otPay, 0)
|
||||
otResults[worker.full_name] = { totalHours, totalOtPay }
|
||||
}
|
||||
overtimeReport.value = otResults
|
||||
reportGenerated.value = true
|
||||
} catch (err) {
|
||||
console.error('Failed to generate report', err)
|
||||
alert('An error occurred while generating the report.')
|
||||
} finally {
|
||||
loadingReport.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const exportReport = () => {
|
||||
if (!canGenerate.value) return
|
||||
const workerIds = selectedWorkers.value.map((w) => w.id).join(',')
|
||||
const url = `http://localhost:3000/api/managers/attendance-records?workerIds=${workerIds}&startDate=${filters.value.startDate}&endDate=${filters.value.endDate}&format=csv`
|
||||
window.open(url, '_blank')
|
||||
// --- UPDATED CSV EXPORT METHOD ---
|
||||
const exportOtSummaryCsv = () => {
|
||||
if (reportData.value.length === 0) return
|
||||
|
||||
const headers = [
|
||||
'Worker Name',
|
||||
'Event',
|
||||
'Timestamp',
|
||||
'Location',
|
||||
'Session Hours',
|
||||
'Session OT Wage (RM)',
|
||||
]
|
||||
const allRows = []
|
||||
const hourlyRate = monthlySalary.value / 26 / 8
|
||||
|
||||
for (const worker of selectedWorkers.value) {
|
||||
const workerRecords = reportData.value
|
||||
.filter((r) => r.full_name === worker.full_name)
|
||||
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
|
||||
|
||||
let clockInRecord = null
|
||||
let workerTotalWage = 0
|
||||
|
||||
for (const record of workerRecords) {
|
||||
const eventType = record.event_type === 'clock_in' ? 'Clock In' : 'Clock Out'
|
||||
const timestamp = new Date(record.timestamp).toLocaleString()
|
||||
let row = [
|
||||
`"${worker.full_name}"`,
|
||||
`"${eventType}"`,
|
||||
`"${timestamp}"`,
|
||||
`"${record.qrCodeUsedName}"`,
|
||||
]
|
||||
|
||||
if (record.event_type === 'clock_in') {
|
||||
if (!clockInRecord) {
|
||||
clockInRecord = record
|
||||
}
|
||||
row.push('', '') // Push empty cells for hours and wage on clock-in
|
||||
allRows.push(row.join(','))
|
||||
} else if (record.event_type === 'clock_out') {
|
||||
if (clockInRecord) {
|
||||
const clockOutTime = new Date(record.timestamp)
|
||||
const clockInTime = new Date(clockInRecord.timestamp)
|
||||
const hours = (clockOutTime - clockInTime) / (1000 * 60 * 60)
|
||||
|
||||
if (hours > 0) {
|
||||
const date = clockInTime.toISOString().split('T')[0]
|
||||
const isPublicHoliday = overtimeSettings.value.publicHolidays.includes(date)
|
||||
const factor = isPublicHoliday
|
||||
? overtimeSettings.value.publicHolidayFactor
|
||||
: overtimeSettings.value.restDayFactor
|
||||
const otPay = hours * hourlyRate * factor
|
||||
workerTotalWage += otPay // Accumulate wage for summary
|
||||
|
||||
row.push(hours.toFixed(2), otPay.toFixed(2))
|
||||
} else {
|
||||
row.push('0.00', '0.00')
|
||||
}
|
||||
clockInRecord = null
|
||||
} else {
|
||||
row.push('N/A', 'N/A')
|
||||
}
|
||||
allRows.push(row.join(','))
|
||||
}
|
||||
}
|
||||
|
||||
// Add summary row for the worker
|
||||
const summaryRow = [`"${worker.full_name} Total"`, '', '', '', '', workerTotalWage.toFixed(2)]
|
||||
allRows.push(summaryRow.join(','))
|
||||
|
||||
// Add empty row for separation
|
||||
allRows.push('')
|
||||
}
|
||||
|
||||
// Remove the last empty row
|
||||
if (allRows.length > 0) {
|
||||
allRows.pop()
|
||||
}
|
||||
|
||||
// --- CHANGED SECTION FOR FILENAME ---
|
||||
// Create a formatted timestamp for the filename
|
||||
const now = new Date()
|
||||
const timestamp = now.toISOString().slice(0, 19).replace('T', '_').replace(/:/g, '-')
|
||||
const fileName = `${timestamp}_report.csv` // e.g., 2025-06-16_16-46-25_report.csv
|
||||
// --- END OF CHANGED SECTION ---
|
||||
|
||||
let csvContent = headers.join(',') + '\n' + allRows.join('\n')
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', fileName)
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
// --- CALENDAR METHODS ---
|
||||
const changeMonth = (offset) => {
|
||||
const newDate = new Date(calendarDate.value)
|
||||
newDate.setMonth(newDate.getMonth() + offset)
|
||||
calendarDate.value = newDate
|
||||
}
|
||||
|
||||
const toggleHoliday = (dateString) => {
|
||||
const index = overtimeSettings.value.publicHolidays.indexOf(dateString)
|
||||
if (index === -1) {
|
||||
overtimeSettings.value.publicHolidays.push(dateString)
|
||||
} else {
|
||||
overtimeSettings.value.publicHolidays.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const today = new Date()
|
||||
filters.value.endDate = today.toISOString().split('T')[0]
|
||||
const sevenDaysAgo = new Date()
|
||||
sevenDaysAgo.setDate(today.getDate() - 7)
|
||||
filters.value.endDate = today.toISOString().split('T')[0]
|
||||
filters.value.startDate = sevenDaysAgo.toISOString().split('T')[0]
|
||||
calendarDate.value = new Date(filters.value.startDate + 'T00:00:00')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* --- Existing Styles --- */
|
||||
.attendance-reporting-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 350px 1fr;
|
||||
@@ -260,8 +571,6 @@ onMounted(() => {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.selected-workers-list li {
|
||||
display: flex;
|
||||
@@ -286,25 +595,129 @@ onMounted(() => {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* --- Refactored & New Styles --- */
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: flex-end;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
padding-bottom: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.action-buttons {
|
||||
margin-left: auto;
|
||||
.overtime-settings-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.settings-header {
|
||||
margin: 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--c-border-light);
|
||||
}
|
||||
.worker-salaries h5,
|
||||
.factor-input h5,
|
||||
.holiday-picker h5 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--c-text-secondary);
|
||||
margin-top: -0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.factors-and-holidays {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: 2rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.factor-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.action-buttons button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* --- Calendar --- */
|
||||
.calendar {
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem;
|
||||
max-width: 400px;
|
||||
}
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.calendar-header button {
|
||||
background: none;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.calendar-weekdays,
|
||||
.calendar-days {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
text-align: center;
|
||||
}
|
||||
.weekday {
|
||||
font-weight: 500;
|
||||
color: var(--c-text-secondary);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
.day-cell {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.day-cell:not(.padding):hover {
|
||||
background-color: var(--c-bg-secondary);
|
||||
}
|
||||
.day-cell.holiday {
|
||||
background-color: var(--c-primary);
|
||||
color: var(--c-primary-text);
|
||||
font-weight: bold;
|
||||
}
|
||||
.day-cell.padding {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* --- Results --- */
|
||||
.results-display .report-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.results-display h4 {
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
padding-bottom: 0.75rem;
|
||||
margin: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.overtime-report table {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.worker-group {
|
||||
margin-top: 2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.worker-group-header {
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
id="password"
|
||||
v-model="newWorker.password"
|
||||
class="form-input"
|
||||
placeholder="Create a temporary password"
|
||||
placeholder="e.g., 123456"
|
||||
/>
|
||||
</div>
|
||||
<button @click="addWorker" :disabled="!isFormValid || loading" class="button-primary">
|
||||
|
||||
Reference in New Issue
Block a user