Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ios/WorkTrack/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import SwiftUI
@MainActor
final class AppState: ObservableObject {
@Published private(set) var language: Language
/// Lives here because a language change rebuilds the tab view.
@Published var tab: AppTab = .work

private static let key = "worktrack.language"

Expand All @@ -22,3 +24,7 @@ final class AppState: ObservableObject {
UserDefaults.standard.set(next.rawValue, forKey: Self.key)
}
}

enum AppTab: Hashable {
case work, attendance, leave, pay, profile
}
12 changes: 10 additions & 2 deletions ios/WorkTrack/App/WorkTrackApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ struct WorkTrackApp: App {

struct RootView: View {
@EnvironmentObject private var auth: AuthStore
@EnvironmentObject private var app: AppState
@ObservedObject var lock: AppLock

var body: some View {
Expand All @@ -51,7 +52,8 @@ struct RootView: View {
case .loading:
ProgressView()
case .signedOut:
SignInView()
// The next person starts on Work, not the last one's tab.
SignInView().onAppear { app.tab = .work }
case .signedIn:
// The lock sits OVER a live session. It gates who may look,
// not whether the session survives — signing out instead would
Expand All @@ -75,6 +77,7 @@ struct RootView: View {
/// "everything sent" while a punch sat in the other one.
struct SignedInTabs: View {
let client: ApiClient
@EnvironmentObject private var app: AppState
@ObservedObject var lock: AppLock

@StateObject private var location = LocationProvider()
Expand All @@ -94,20 +97,25 @@ struct SignedInTabs: View {
}

var body: some View {
TabView {
TabView(selection: $app.tab) {
MyWorkView(client: client, attendance: attendance, cache: cache)
// A checklist, not a hammer. This is an office product that
// happens to be used on sites; a tool icon narrows it to
// manual trades and reads wrong to every other customer.
.tabItem { Label(L.t("tab_work"), systemImage: "checklist") }
.tag(AppTab.work)
AttendanceHistoryView(client: client)
.tabItem { Label(L.t("tab_history"), systemImage: "clock.fill") }
.tag(AppTab.attendance)
LeaveView(client: client)
.tabItem { Label(L.t("tab_leave"), systemImage: "calendar") }
.tag(AppTab.leave)
PayslipsView(client: client)
.tabItem { Label(L.t("tab_pay"), systemImage: "doc.text.fill") }
.tag(AppTab.pay)
ProfileView(attendance: attendance, lock: lock)
.tabItem { Label(L.t("tab_profile"), systemImage: "person.crop.circle.fill") }
.tag(AppTab.profile)
}
}
}
2 changes: 2 additions & 0 deletions ios/WorkTrack/Auth/AuthStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ final class AuthStore: ObservableObject {

/// Restores a session from the Keychain, or reports signed out.
func start() async {
// A language change re-fires this; re-restoring a live session could sign the worker out.
guard state == .loading else { return }
guard let refresh = Keychain.get(Self.refreshKey) else {
state = .signedOut
return
Expand Down
12 changes: 12 additions & 0 deletions ios/WorkTrack/Core/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ enum L {
"pay_earnings": "عواید",
"pay_deductions": "کسرات",
"pay_income_tax": "مالیهٔ معاش",
"pay_basic": "معاش اساسی",
"pay_absence": "کسر غیرحاضری",
"leave_type_annual": "رخصتی سالانه",
"leave_type_sick": "رخصتی مریضی",
"pay_total_deductions": "مجموع کسرات",
"pay_days": "روزها",
"pay_worked_days": "روزهای کاری",
Expand Down Expand Up @@ -284,6 +288,10 @@ enum L {
"pay_earnings": "عواید",
"pay_deductions": "کسرونه",
"pay_income_tax": "د معاش مالیه",
"pay_basic": "اساسي معاش",
"pay_absence": "د غیرحاضرۍ کسر",
"leave_type_annual": "کلنۍ رخصتي",
"leave_type_sick": "د ناروغۍ رخصتي",
"pay_total_deductions": "د کسرونو ټولګه",
"pay_days": "ورځې",
"pay_worked_days": "کاري ورځې",
Expand Down Expand Up @@ -437,6 +445,10 @@ enum L {
"pay_earnings": "Earnings",
"pay_deductions": "Deductions",
"pay_income_tax": "Income tax",
"pay_basic": "Basic salary",
"pay_absence": "Absence deduction",
"leave_type_annual": "Annual leave",
"leave_type_sick": "Sick leave",
"pay_total_deductions": "Total deductions",
"pay_days": "Days",
"pay_worked_days": "Days worked",
Expand Down
2 changes: 1 addition & 1 deletion ios/WorkTrack/Leave/LeaveApplyView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct LeaveApplyView: View {
Form {
Section {
Picker(L.t("leave_type"), selection: $typeId) {
ForEach(types) { type in Text(type.name).tag(type.id) }
ForEach(types) { type in Text(type.displayName).tag(type.id) }
}
DatePicker(
L.t("leave_from"), selection: $from, displayedComponents: .date
Expand Down
9 changes: 9 additions & 0 deletions ios/WorkTrack/Leave/LeaveModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ struct LeaveType: Codable, Identifiable, Equatable {
let code: String
let colorHex: String?
let isPaid: Bool?

/// Only the untouched Dari names signup seeds are translated; a company's own name stays as typed.
var displayName: String {
switch (code, name) {
case ("ANNUAL", "رخصتی سالانه"): return L.t("leave_type_annual")
case ("SICK", "رخصتی مریضی"): return L.t("leave_type_sick")
default: return name
}
}
}

/// One person's balance for one leave type, for one year.
Expand Down
2 changes: 1 addition & 1 deletion ios/WorkTrack/Leave/LeaveViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final class LeaveViewModel: ObservableObject {
let types: [LeaveType]

func typeName(_ id: String) -> String {
types.first { $0.id == id }?.name ?? id
types.first { $0.id == id }?.displayName ?? id
}
}

Expand Down
10 changes: 10 additions & 0 deletions ios/WorkTrack/Payslips/PayslipModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ struct PayslipLine: Codable, Identifiable, Equatable {

var id: String { componentCode }
var isEarning: Bool { type == .earning }

/// Payroll names these three lines in Dari itself; company components keep their own names.
var displayName: String {
switch componentCode {
case "BASIC": return L.t("pay_basic")
case "LOP": return L.t("pay_absence")
case "TAX": return L.t("pay_income_tax")
default: return componentName
}
}
}

/// One month's pay.
Expand Down
6 changes: 3 additions & 3 deletions ios/WorkTrack/Payslips/PayslipsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ struct PayslipDetailView: View {

Section(L.t("pay_earnings")) {
ForEach(slip.earnings) { line in
amount(line.componentName, line.amount)
amount(line.displayName, line.amount)
}
amount(L.t("pay_gross"), slip.gross, emphasised: true)
}
Expand All @@ -90,15 +90,15 @@ struct PayslipDetailView: View {
// adding it again from `incomeTax` listed it twice and made the
// column stop adding up.
ForEach(slip.deductions) { line in
amount(line.componentName, line.amount)
amount(line.displayName, line.amount)
}
amount(L.t("pay_total_deductions"), slip.totalDeductions, emphasised: true)
}

if !slip.employerCosts.isEmpty {
Section {
ForEach(slip.employerCosts) { line in
amount(line.componentName, line.amount)
amount(line.displayName, line.amount)
}
} header: {
Text(L.t("pay_employer_cost"))
Expand Down
34 changes: 34 additions & 0 deletions ios/WorkTrackTests/LeaveAndPayTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,40 @@ final class LeaveAndPayTests: XCTestCase {
XCTAssertTrue(slip.earnings.isEmpty)
}

// MARK: names in the chosen language

override func tearDown() {
L.language = .dari
super.tearDown()
}

func testBuiltInLeaveTypesFollowTheLanguage() {
L.language = .english
let annual = LeaveType(id: "annual", name: "رخصتی سالانه", code: "ANNUAL", colorHex: nil, isPaid: true)
let sick = LeaveType(id: "sick", name: "رخصتی مریضی", code: "SICK", colorHex: nil, isPaid: true)
XCTAssertEqual(annual.displayName, "Annual leave")
XCTAssertEqual(sick.displayName, "Sick leave")
}

func testALeaveTypeTheCompanyNamedKeepsItsName() {
L.language = .english
let renamed = LeaveType(id: "annual", name: "رخصتی تفریحی", code: "ANNUAL", colorHex: nil, isPaid: true)
let custom = LeaveType(id: "hajj", name: "رخصتی حج", code: "HAJJ", colorHex: nil, isPaid: true)
XCTAssertEqual(renamed.displayName, "رخصتی تفریحی")
XCTAssertEqual(custom.displayName, "رخصتی حج")
}

func testPayrollLinesFollowTheLanguageButCompanyComponentsDoNot() {
L.language = .english
func line(_ code: String, _ name: String) -> PayslipLine {
PayslipLine(componentCode: code, componentName: name, type: .earning, amount: 1)
}
XCTAssertEqual(line("BASIC", "معاش اساسی").displayName, "Basic salary")
XCTAssertEqual(line("LOP", "کسر غیرحاضری").displayName, "Absence deduction")
XCTAssertEqual(line("TAX", "مالیهٔ معاش").displayName, "Income tax")
XCTAssertEqual(line("TRANSPORT", "کمک ترانسپورت").displayName, "کمک ترانسپورت")
}

// MARK: money

func testMoneyIsLocalisedAndNamed() {
Expand Down
2 changes: 1 addition & 1 deletion ios/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ options:
settings:
base:
MARKETING_VERSION: "1.0"
CURRENT_PROJECT_VERSION: "2"
CURRENT_PROJECT_VERSION: "3"
SWIFT_VERSION: "5.9"
# Simulator builds are signed ad-hoc ("-"). That is not cosmetic: the
# Keychain refuses an app with no entitlements and SecItemAdd fails with
Expand Down
Loading